contact selection reeemix

1) RecyclerView-based, with better long scroller
   and more material-inspired look.
2) Add badge for Signal users to contact selection
   list.

// FREEBIE
This commit is contained in:
Jake McGinty
2015-11-02 17:40:41 -08:00
committed by Moxie Marlinspike
parent 7bec5efe1a
commit fb8d6cb538
31 changed files with 761 additions and 136 deletions

View File

@@ -18,30 +18,36 @@ package org.thoughtcrime.securesms;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
/**
* Fragment for selecting a one or more contacts from a list.
*
@@ -65,9 +71,10 @@ public class ContactSelectionListFragment extends Fragment
private Map<Long, String> selectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private StickyListHeadersListView listView;
private SwipeRefreshLayout swipeRefresh;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
@Override
public void onActivityCreated(Bundle icicle) {
@@ -89,13 +96,12 @@ public class ContactSelectionListFragment extends Fragment
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = (TextView) view.findViewById(android.R.id.empty);
swipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh);
listView = (StickyListHeadersListView) view.findViewById(android.R.id.list);
listView.setFocusable(true);
listView.setFastScrollEnabled(true);
listView.setDrawingListUnderStickyHeader(false);
listView.setOnItemClickListener(new ListClickListener());
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
fastScroller.setRecyclerView(recyclerView);
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true) &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN);
@@ -117,9 +123,13 @@ public class ContactSelectionListFragment extends Fragment
}
private void initializeCursor() {
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), null, isMulti());
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
null,
new ListClickListener(),
isMulti());
selectedContacts = adapter.getSelectedContacts();
listView.setAdapter(adapter);
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true));
this.getLoaderManager().initLoader(0, null, this);
}
@@ -147,19 +157,18 @@ public class ContactSelectionListFragment extends Fragment
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
((CursorAdapter) listView.getAdapter()).changeCursor(data);
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((CursorAdapter) listView.getAdapter()).changeCursor(null);
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null);
}
private class ListClickListener implements AdapterView.OnItemClickListener {
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override
public void onItemClick(AdapterView<?> l, View v, int position, long id) {
ContactSelectionListItem contact = (ContactSelectionListItem)v;
public void onItemClick(ContactSelectionListItem contact) {
if (!isMulti() || !selectedContacts.containsKey(contact.getContactId())) {
selectedContacts.put(contact.getContactId(), contact.getNumber());
@@ -185,4 +194,181 @@ public class ContactSelectionListFragment extends Fragment
void onContactSelected(String number);
void onContactDeselected(String number);
}
/**
* A sticky header decoration for android's RecyclerView.
*/
public static class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
private Map<Long, RecyclerView.ViewHolder> mHeaderCache;
private StickyHeaderAdapter mAdapter;
private boolean mRenderInline;
/**
* @param adapter the sticky header adapter to use
*/
public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
mAdapter = adapter;
mHeaderCache = new HashMap<>();
mRenderInline = renderInline;
}
/**
* {@inheritDoc}
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state)
{
int position = parent.getChildAdapterPosition(view);
int headerHeight = 0;
if (position != RecyclerView.NO_POSITION && hasHeader(position)) {
View header = getHeader(parent, position).itemView;
headerHeight = getHeaderHeightForLayout(header);
}
outRect.set(0, headerHeight, 0, 0);
}
private boolean hasHeader(int position) {
if (position == 0) {
return true;
}
int previous = position - 1;
return mAdapter.getHeaderId(position) != mAdapter.getHeaderId(previous);
}
private RecyclerView.ViewHolder getHeader(RecyclerView parent, int position) {
final long key = mAdapter.getHeaderId(position);
if (mHeaderCache.containsKey(key)) {
return mHeaderCache.get(key);
} else {
final RecyclerView.ViewHolder holder = mAdapter.onCreateHeaderViewHolder(parent);
final View header = holder.itemView;
//noinspection unchecked
mAdapter.onBindHeaderViewHolder(holder, position);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
header.measure(childWidth, childHeight);
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
mHeaderCache.put(key, holder);
return holder;
}
}
/**
* {@inheritDoc}
*/
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
final int count = parent.getChildCount();
for (int layoutPos = 0; layoutPos < count; layoutPos++) {
final View child = parent.getChildAt(layoutPos);
final int adapterPos = parent.getChildAdapterPosition(child);
if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(adapterPos))) {
View header = getHeader(parent, adapterPos).itemView;
c.save();
final int left = child.getLeft();
final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
c.translate(left, top);
header.draw(c);
c.restore();
}
}
}
private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
int layoutPos)
{
int headerHeight = getHeaderHeightForLayout(header);
int top = getChildY(parent, child) - headerHeight;
if (layoutPos == 0) {
final int count = parent.getChildCount();
final long currentId = mAdapter.getHeaderId(adapterPos);
// find next view with header and compute the offscreen push if needed
for (int i = 1; i < count; i++) {
int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(i));
if (adapterPosHere != RecyclerView.NO_POSITION) {
long nextId = mAdapter.getHeaderId(adapterPosHere);
if (nextId != currentId) {
final View next = parent.getChildAt(i);
final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapterPosHere).itemView.getHeight());
if (offset < 0) {
return offset;
} else {
break;
}
}
}
}
top = Math.max(0, top);
}
return top;
}
private int getChildY(RecyclerView parent, View child) {
if (VERSION.SDK_INT < 11) {
Rect rect = new Rect();
parent.getChildVisibleRect(child, rect, null);
return rect.top;
} else {
return (int)ViewCompat.getY(child);
}
}
private int getHeaderHeightForLayout(View header) {
return mRenderInline ? 0 : header.getHeight();
}
}
/**
* The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views.
*
* @param <T> the header view holder
*/
public interface StickyHeaderAdapter<T extends RecyclerView.ViewHolder> {
/**
* Returns the header id for the item at the given position.
*
* @param position the item position
* @return the header id
*/
long getHeaderId(int position);
/**
* Creates a new header ViewHolder.
*
* @param parent the header's view parent
* @return a view holder for the created view
*/
T onCreateHeaderViewHolder(ViewGroup parent);
/**
* Updates the header view to reflect the header data for the given position
* @param viewHolder the header view holder
* @param position the header's item position
*/
void onBindHeaderViewHolder(T viewHolder, int position);
}
}

View File

@@ -3,8 +3,13 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.util.Pair;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@@ -16,10 +21,13 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities.Capability;
public class AvatarImageView extends ImageView {
private boolean inverted;
private boolean showBadge;
public AvatarImageView(Context context) {
super(context);
@@ -33,18 +41,22 @@ public class AvatarImageView extends ImageView {
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
inverted = typedArray.getBoolean(0, false);
showBadge = typedArray.getBoolean(1, false);
typedArray.recycle();
}
}
public void setAvatar(@Nullable Recipients recipients, boolean quickContactEnabled) {
public void setAvatar(final @Nullable Recipients recipients, boolean quickContactEnabled) {
if (recipients != null) {
MaterialColor backgroundColor = recipients.getColor();
setImageDrawable(recipients.getContactPhoto().asDrawable(getContext(), backgroundColor.toConversationColor(getContext()), inverted));
setAvatarClickHandler(recipients, quickContactEnabled);
setTag(recipients);
if (showBadge) new BadgeResolutionTask(getContext()).execute(recipients);
} else {
setImageDrawable(ContactPhotoFactory.getDefaultContactPhoto(null).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted));
setOnClickListener(null);
setTag(null);
}
}
@@ -74,4 +86,29 @@ public class AvatarImageView extends ImageView {
}
}
private class BadgeResolutionTask extends AsyncTask<Recipients,Void,Pair<Recipients, Boolean>> {
private final Context context;
public BadgeResolutionTask(Context context) {
this.context = context;
}
@Override
protected Pair<Recipients, Boolean> doInBackground(Recipients... recipients) {
Capability textCapability = DirectoryHelper.getUserCapabilities(context, recipients[0]).getTextCapability();
return new Pair<>(recipients[0], textCapability == Capability.SUPPORTED);
}
@Override
protected void onPostExecute(Pair<Recipients, Boolean> result) {
if (getTag() == result.first && result.second) {
final Drawable badged = new LayerDrawable(new Drawable[] {
getDrawable(),
ContextCompat.getDrawable(context, R.drawable.badge_drawable)
});
setImageDrawable(badged);
}
}
}
}

View File

@@ -0,0 +1,206 @@
/**
* Modified version of
* https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
*
* Their license:
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RecyclerViewFastScroller extends LinearLayout {
private static final int BUBBLE_ANIMATION_DURATION = 100;
private static final int TRACK_SNAP_RANGE = 5;
private TextView bubble;
private View handle;
private RecyclerView recyclerView;
private int height;
private ObjectAnimator currentAnimator;
private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
if (bubble == null || handle.isSelected())
return;
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
setBubbleAndHandlePosition(height * proportion);
}
};
public interface FastScrollAdapter {
CharSequence getBubbleText(int pos);
}
public RecyclerViewFastScroller(final Context context) {
this(context, null);
}
public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) {
super(context, attrs);
setOrientation(HORIZONTAL);
setClipChildren(false);
inflate(context, R.layout.recycler_view_fast_scroller, this);
bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
handle = ViewUtil.findById(this, R.id.fastscroller_handle);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height = h;
}
@Override
@TargetApi(11)
public boolean onTouchEvent(@NonNull MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
{
return false;
}
if (currentAnimator != null) {
currentAnimator.cancel();
}
if (bubble.getVisibility() != VISIBLE) {
showBubble();
}
handle.setSelected(true);
case MotionEvent.ACTION_MOVE:
final float y = event.getY();
setBubbleAndHandlePosition(y);
setRecyclerViewPosition(y);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
handle.setSelected(false);
hideBubble();
return true;
}
return super.onTouchEvent(event);
}
public void setRecyclerView(final RecyclerView recyclerView) {
this.recyclerView = recyclerView;
recyclerView.addOnScrollListener(onScrollListener);
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
if (bubble == null || handle.isSelected())
return true;
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
setBubbleAndHandlePosition(height * proportion);
return true;
}
});
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (recyclerView != null)
recyclerView.removeOnScrollListener(onScrollListener);
}
private void setRecyclerViewPosition(float y) {
if (recyclerView != null) {
final int itemCount = recyclerView.getAdapter().getItemCount();
float proportion;
if (ViewUtil.getY(handle) == 0)
proportion = 0f;
else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE)
proportion = 1f;
else
proportion = y / (float) height;
final int targetPos = Util.clamp((int)(proportion * (float)itemCount), 0, itemCount - 1);
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
final CharSequence bubbleText = ((FastScrollAdapter) recyclerView.getAdapter()).getBubbleText(targetPos);
if (bubble != null)
bubble.setText(bubbleText);
}
}
private void setBubbleAndHandlePosition(float y) {
final int handleHeight = handle.getHeight();
final int bubbleHeight = bubble.getHeight();
ViewUtil.setY(handle, Util.clamp((int)(y - handleHeight / 2), 0, height - handleHeight));
ViewUtil.setY(bubble, Util.clamp((int)(y - bubbleHeight), 0, height - bubbleHeight - handleHeight / 2));
}
@TargetApi(11)
private void showBubble() {
bubble.setVisibility(VISIBLE);
if (VERSION.SDK_INT >= 11) {
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.start();
}
}
@TargetApi(11)
private void hideBubble() {
if (VERSION.SDK_INT >= 11) {
if (currentAnimator != null) currentAnimator.cancel();
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
currentAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
bubble.setVisibility(INVISIBLE);
currentAnimator = null;
}
});
currentAnimator.start();
} else {
bubble.setVisibility(INVISIBLE);
}
}
}

View File

@@ -20,101 +20,174 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ImageSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter;
import org.thoughtcrime.securesms.ContactSelectionListFragment.StickyHeaderAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import java.util.HashMap;
import java.util.Map;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
/**
* List adapter to display all contacts and their related information
*
* @author Jake McGinty
*/
public class ContactSelectionListAdapter extends CursorAdapter
implements StickyListHeadersAdapter
public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewHolder>
implements FastScrollAdapter,
StickyHeaderAdapter<HeaderViewHolder>
{
private final static String TAG = ContactSelectionListAdapter.class.getSimpleName();
private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
R.attr.contact_selection_lay_user};
private final boolean multiSelect;
private final LayoutInflater li;
private final TypedArray drawables;
private final boolean multiSelect;
private final LayoutInflater li;
private final TypedArray drawables;
private final ItemClickListener clickListener;
private final HashMap<Long, String> selectedContacts = new HashMap<>();
public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) {
super(context, cursor, 0);
this.li = LayoutInflater.from(context);
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
this.multiSelect = multiSelect;
public static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(@NonNull final View itemView,
@Nullable final ItemClickListener clickListener)
{
super(itemView);
itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickListener != null) clickListener.onItemClick(getView());
}
});
}
public ContactSelectionListItem getView() {
return (ContactSelectionListItem) itemView;
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
public HeaderViewHolder(View itemView) {
super(itemView);
}
}
public ContactSelectionListAdapter(@NonNull Context context,
@Nullable Cursor cursor,
@Nullable ItemClickListener clickListener,
boolean multiSelect)
{
super(context, cursor);
this.li = LayoutInflater.from(context);
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
this.multiSelect = multiSelect;
this.clickListener = clickListener;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return li.inflate(R.layout.contact_selection_list_item, parent, false);
public long getHeaderId(int i) {
return getHeaderString(i).hashCode();
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
}
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN));
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN));
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(),
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
int color = (contactType == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
drawables.getColor(1, 0xff000000);
drawables.getColor(1, 0xff000000);
((ContactSelectionListItem)view).unbind();
((ContactSelectionListItem)view).set(id, contactType, name, number, labelText, color, multiSelect);
((ContactSelectionListItem)view).setChecked(selectedContacts.containsKey(id));
viewHolder.getView().unbind();
viewHolder.getView().set(id, contactType, name, number, labelText, color, multiSelect);
viewHolder.getView().setChecked(selectedContacts.containsKey(id));
}
@Override
public View getHeaderView(int i, View convertView, ViewGroup viewGroup) {
Cursor cursor = getCursor();
final TextView text;
if (convertView == null) {
text = (TextView)li.inflate(R.layout.contact_selection_list_header, viewGroup, false);
} else {
text = (TextView)convertView;
}
cursor.moveToPosition(i);
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
if (contactType == ContactsDatabase.PUSH_TYPE) text.setText(R.string.contact_selection_list__header_signal_users);
else text.setText(R.string.contact_selection_list__header_other);
return text;
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
}
@Override
public long getHeaderId(int i) {
Cursor cursor = getCursor();
cursor.moveToPosition(i);
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position, R.drawable.ic_signal_grey_24dp));
}
return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN));
@Override
public CharSequence getBubbleText(int position) {
return getSpannedHeaderString(position, R.drawable.ic_signal_white_48dp);
}
public Map<Long, String> getSelectedContacts() {
return selectedContacts;
}
private CharSequence getSpannedHeaderString(int position, @DrawableRes int drawable) {
Cursor cursor = getCursorAtPositionOrThrow(position);
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) {
SpannableString spannable = new SpannableString(" ");
spannable.setSpan(new ImageSpan(getContext(), drawable, ImageSpan.ALIGN_BOTTOM), 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
} else {
return getHeaderString(position);
}
}
private String getHeaderString(int position) {
Cursor cursor = getCursorAtPositionOrThrow(position);
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) {
return getContext().getString(R.string.app_name);
} else {
String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN))
.trim()
.substring(0,1)
.toUpperCase();
if (Character.isLetterOrDigit(letter.codePointAt(0))) {
return letter;
} else {
return "#";
}
}
}
private Cursor getCursorAtPositionOrThrow(int position) {
Cursor cursor = getCursor();
if (cursor == null) {
throw new IllegalStateException("Cursor should not be null here.");
}
if (!cursor.moveToPosition(position));
return cursor;
}
public interface ItemClickListener {
void onItemClick(ContactSelectionListItem item);
}
}

View File

@@ -5,7 +5,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
public class ContactSelectionListItem extends RelativeLayout implements Recipients.RecipientsModifiedListener {
public class ContactSelectionListItem extends LinearLayout implements Recipients.RecipientsModifiedListener {
private AvatarImageView contactPhotoImage;
private TextView numberView;
@@ -34,10 +34,6 @@ public class ContactSelectionListItem extends RelativeLayout implements Recipien
super(context, attrs);
}
public ContactSelectionListItem(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();

View File

@@ -24,6 +24,7 @@ import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
@@ -33,6 +34,7 @@ import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
public class ViewUtil {
@@ -45,6 +47,42 @@ public class ViewUtil {
}
}
public static void setY(final @NonNull View v, final int y) {
if (VERSION.SDK_INT >= 11) {
ViewCompat.setY(v, y);
} else {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
params.topMargin = y;
v.setLayoutParams(params);
}
}
public static float getY(final @NonNull View v) {
if (VERSION.SDK_INT >= 11) {
return ViewCompat.getY(v);
} else {
return ((ViewGroup.MarginLayoutParams)v.getLayoutParams()).topMargin;
}
}
public static void setX(final @NonNull View v, final int x) {
if (VERSION.SDK_INT >= 11) {
ViewCompat.setX(v, x);
} else {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
params.leftMargin = x;
v.setLayoutParams(params);
}
}
public static float getX(final @NonNull View v) {
if (VERSION.SDK_INT >= 11) {
return ViewCompat.getX(v);
} else {
return ((LayoutParams)v.getLayoutParams()).leftMargin;
}
}
public static void swapChildInPlace(ViewGroup parent, View toRemove, View toAdd, int defaultIndex) {
int childIndex = parent.indexOfChild(toRemove);
if (childIndex > -1) parent.removeView(toRemove);