Refactor ContactListAdapter to be reusable for other use cases.

This commit introduces an abstract `BaseContactListAdapter` which provides
most of the adapter logic. The original `ContactListAdapter` extends it to
show date and online status of the contacts.

The new `ContactChooserAdapter` which is used for introductions extends
the `ContactListAdapter` and adds logic for graying out contacts from
different identities than the currently used one.

A new `ContactSelectorAdapter` extends the `BaseContactListAdapter` and
allows to select multiple contacts. It offers a method to return a
collection of all selected `ContactId`s.

This commit also sneaks in an animation when the 'Share Forum' button
is clicked.

Closes #292
This commit is contained in:
Torsten Grote
2016-04-22 20:14:47 -03:00
parent b17575bfa4
commit 9ea7113423
15 changed files with 580 additions and 460 deletions

View File

@@ -0,0 +1,189 @@
package org.briarproject.android.contact;
import android.content.Context;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.Author;
import java.util.List;
import im.delight.android.identicons.IdenticonDrawable;
import static android.support.v7.util.SortedList.INVALID_POSITION;
public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.BaseContactHolder>
extends RecyclerView.Adapter<VH> {
protected SortedList<ContactListItem> contacts;
protected final OnItemClickListener listener;
protected Context ctx;
public BaseContactListAdapter(Context context, OnItemClickListener listener) {
this.ctx = context;
this.listener = listener;
this.contacts = new SortedList<ContactListItem>(ContactListItem.class,
new SortedListCallBacks());
}
@Override
public void onBindViewHolder(final VH ui, final int position) {
final ContactListItem item = getItem(position);
Author author = item.getContact().getAuthor();
ui.avatar.setImageDrawable(
new IdenticonDrawable(author.getId().getBytes()));
String contactName = author.getName();
ui.name.setText(contactName);
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) listener.onItemClick(ui.avatar, item);
}
});
}
@Override
public int getItemCount() {
return contacts.size();
}
public ContactListItem getItem(int position) {
if (position == INVALID_POSITION || contacts.size() <= position) {
return null; // Not found
}
return contacts.get(position);
}
public void updateItem(int position, ContactListItem item) {
contacts.updateItemAt(position, item);
}
public int findItemPosition(ContactListItem c) {
return contacts.indexOf(c);
}
public int findItemPosition(ContactId c) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
ContactListItem item = getItem(i);
if (item.getContact().getId().equals(c)) return i;
}
return INVALID_POSITION; // Not found
}
public void addAll(List<ContactListItem> contacts) {
this.contacts.addAll(contacts);
}
public void add(ContactListItem contact) {
contacts.add(contact);
}
public void remove(ContactListItem contact) {
contacts.remove(contact);
}
public void clear() {
contacts.clear();
}
public static class BaseContactHolder extends RecyclerView.ViewHolder {
public final ViewGroup layout;
public final ImageView avatar;
public final TextView name;
public BaseContactHolder(View v) {
super(v);
layout = (ViewGroup) v;
avatar = (ImageView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.nameView);
}
}
public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
return compareByName(c1, c2);
}
protected int compareByName(ContactListItem c1, ContactListItem c2) {
int authorCompare = c1.getLocalAuthor().getName()
.compareTo(c2.getLocalAuthor().getName());
if (authorCompare == 0) {
// if names are equal, compare by time instead
return compareByTime(c1, c2);
} else {
return authorCompare;
}
}
protected int compareByTime(ContactListItem c1, ContactListItem c2) {
long time1 = c1.getTimestamp();
long time2 = c2.getTimestamp();
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
}
protected class SortedListCallBacks extends SortedList.Callback<ContactListItem> {
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public int compare(ContactListItem c1, ContactListItem c2) {
return compareContactListItems(c1, c2);
}
@Override
public boolean areItemsTheSame(ContactListItem c1, ContactListItem c2) {
return c1.getContact().getId().equals(c2.getContact().getId());
}
@Override
public boolean areContentsTheSame(ContactListItem c1,
ContactListItem c2) {
// check for all properties that influence visual
// representation of contact
if (c1.isConnected() != c2.isConnected()) {
return false;
}
if (c1.getUnreadCount() != c2.getUnreadCount()) {
return false;
}
if (c1.getTimestamp() != c2.getTimestamp()) {
return false;
}
return true;
}
}
public interface OnItemClickListener {
void onItemClick(View view, ContactListItem item);
}
}

View File

@@ -1,14 +1,7 @@
package org.briarproject.android.contact;
import android.content.Context;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -17,98 +10,12 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
import java.util.List;
import im.delight.android.identicons.IdenticonDrawable;
import static android.support.v7.util.SortedList.INVALID_POSITION;
public class ContactListAdapter
extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> {
extends BaseContactListAdapter<ContactListAdapter.ContactHolder> {
private final SortedList<ContactListItem> contacts =
new SortedList<ContactListItem>(ContactListItem.class,
new SortedList.Callback<ContactListItem>() {
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onChanged(int position, int count) {
notifyItemRangeChanged(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onRemoved(int position, int count) {
notifyItemRangeRemoved(position, count);
}
@Override
public int compare(ContactListItem c1,
ContactListItem c2) {
int authorCompare = 0;
if (chooser) {
authorCompare = c1.getLocalAuthor().getName()
.compareTo(
c2.getLocalAuthor().getName());
}
if (authorCompare == 0) {
// sort items by time
// and do not take unread messages into account
long time1 = c1.getTimestamp();
long time2 = c2.getTimestamp();
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
} else {
return authorCompare;
}
}
@Override
public boolean areItemsTheSame(ContactListItem c1,
ContactListItem c2) {
return c1.getContact().getId()
.equals(c2.getContact().getId());
}
@Override
public boolean areContentsTheSame(ContactListItem c1,
ContactListItem c2) {
// check for all properties that influence visual
// representation of contact
if (c1.isConnected() != c2.isConnected()) {
return false;
}
if (c1.getUnreadCount() != c2.getUnreadCount()) {
return false;
}
if (c1.getTimestamp() != c2.getTimestamp()) {
return false;
}
return true;
}
});
private final OnItemClickListener listener;
private final boolean chooser;
private Context ctx;
private AuthorId localAuthorId;
public ContactListAdapter(Context context, OnItemClickListener listener,
boolean chooser) {
ctx = context;
this.listener = listener;
this.chooser = chooser;
public ContactListAdapter(Context context, OnItemClickListener listener) {
super(context, listener);
}
@Override
@@ -121,38 +28,25 @@ public class ContactListAdapter
@Override
public void onBindViewHolder(final ContactHolder ui, final int position) {
super.onBindViewHolder(ui, position);
final ContactListItem item = getItem(position);
// name and unread count
String contactName = item.getContact().getAuthor().getName();
int unread = item.getUnreadCount();
if (!chooser && unread > 0) {
ui.layout.setBackgroundColor(
ContextCompat.getColor(ctx, R.color.unread_background));
}
if (item.isConnected()) {
ui.bulb.setImageResource(R.drawable.contact_connected);
} else {
ui.bulb.setImageResource(R.drawable.contact_disconnected);
}
Author author = item.getContact().getAuthor();
ui.avatar.setImageDrawable(
new IdenticonDrawable(author.getId().getBytes()));
String contactName = author.getName();
if (!chooser && unread > 0) {
if (unread > 0) {
// TODO show these in a bubble on top of the avatar
ui.name.setText(contactName + " (" + unread + ")");
// different background for contacts with unread messages
ui.layout.setBackgroundColor(
ContextCompat.getColor(ctx, R.color.unread_background));
} else {
ui.name.setText(contactName);
}
if (chooser) {
ui.identity.setText(item.getLocalAuthor().getName());
} else {
ui.identity.setVisibility(View.GONE);
}
// date of last message
if (item.isEmpty()) {
ui.date.setText(R.string.no_private_messages);
} else {
@@ -162,115 +56,33 @@ public class ContactListAdapter
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
}
if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) {
grayOutItem(ui);
}
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(ui.avatar, item);
}
});
}
@Override
public int getItemCount() {
return contacts.size();
}
/**
* Set the identity from whose perspective the contact shall be chosen.
* This is only used if chooser is true.
* @param authorId The ID of the local Author
*/
public void setLocalAuthor(AuthorId authorId) {
localAuthorId = authorId;
notifyDataSetChanged();
}
private void grayOutItem(final ContactHolder ui) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
float alpha = 0.25f;
ui.bulb.setAlpha(alpha);
ui.avatar.setAlpha(alpha);
ui.name.setAlpha(alpha);
ui.date.setAlpha(alpha);
ui.identity.setAlpha(alpha);
// online/offline
if (item.isConnected()) {
ui.bulb.setImageResource(R.drawable.contact_connected);
} else {
ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
PorterDuff.Mode.MULTIPLY);
ui.bulb.setColorFilter(colorFilter);
ui.avatar.setColorFilter(colorFilter);
ui.name.setEnabled(false);
ui.date.setEnabled(false);
ui.bulb.setImageResource(R.drawable.contact_disconnected);
}
}
public ContactListItem getItem(int position) {
if (position == INVALID_POSITION || contacts.size() <= position) {
return null; // Not found
}
return contacts.get(position);
}
protected static class ContactHolder
extends BaseContactListAdapter.BaseContactHolder {
public void updateItem(int position, ContactListItem item) {
contacts.updateItemAt(position, item);
}
public int findItemPosition(ContactId c) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
ContactListItem item = getItem(i);
if (item.getContact().getId().equals(c)) return i;
}
return INVALID_POSITION; // Not found
}
public void addAll(List<ContactListItem> contacts) {
this.contacts.addAll(contacts);
}
public void add(ContactListItem contact) {
contacts.add(contact);
}
public void remove(ContactListItem contact) {
contacts.remove(contact);
}
public void clear() {
contacts.beginBatchedUpdates();
while(contacts.size() != 0) {
contacts.removeItemAt(0);
}
contacts.endBatchedUpdates();
}
public static class ContactHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public ImageView bulb;
public ImageView avatar;
public TextView name;
public TextView identity;
public TextView date;
public final ImageView bulb;
public final TextView date;
public final TextView identity;
public ContactHolder(View v) {
super(v);
layout = (ViewGroup) v;
bulb = (ImageView) v.findViewById(R.id.bulbView);
avatar = (ImageView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.nameView);
identity = (TextView) v.findViewById(R.id.identityView);
date = (TextView) v.findViewById(R.id.dateView);
identity = (TextView) v.findViewById(R.id.identityView);
}
}
public interface OnItemClickListener {
void onItemClick(View view, ContactListItem item);
@Override
public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
return compareByTime(c1, c2);
}
}

View File

@@ -101,7 +101,7 @@ public class ContactListFragment extends BaseEventFragment {
inflater.inflate(R.layout.activity_contact_list, container,
false);
ContactListAdapter.OnItemClickListener onItemClickListener =
BaseContactListAdapter.OnItemClickListener onItemClickListener =
new ContactListAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, ContactListItem item) {
@@ -124,8 +124,7 @@ public class ContactListFragment extends BaseEventFragment {
}
};
adapter = new ContactListAdapter(getContext(), onItemClickListener,
false);
adapter = new ContactListAdapter(getContext(), onItemClickListener);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);

View File

@@ -1,77 +0,0 @@
package org.briarproject.android.contact;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.support.v7.app.AlertDialog;
import org.briarproject.R;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SelectContactsDialog implements OnMultiChoiceClickListener {
private Listener listener = null;
private List<Contact> contacts = null;
private Set<ContactId> selected = null;
public void setListener(Listener listener) {
this.listener = listener;
}
public void setContacts(Collection<Contact> contacts) {
this.contacts = new ArrayList<Contact>(contacts);
}
public void setSelected(Collection<ContactId> selected) {
this.selected = new HashSet<ContactId>(selected);
}
public Dialog build(Context ctx) {
if (listener == null || contacts == null || selected == null)
throw new IllegalStateException();
AlertDialog.Builder builder = new AlertDialog.Builder(ctx,
R.style.BriarDialogTheme);
int size = contacts.size();
String[] names = new String[size];
boolean[] checked = new boolean[size];
for (int i = 0; i < size; i++) {
Contact c = contacts.get(i);
names[i] = c.getAuthor().getName();
checked[i] = selected.contains(c.getId());
}
builder.setMultiChoiceItems(names, checked, this);
builder.setPositiveButton(R.string.done_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactsSelected(selected);
}
});
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactSelectionCancelled();
}
});
return builder.create();
}
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if (isChecked) selected.add(contacts.get(which).getId());
else selected.remove(contacts.get(which).getId());
}
public interface Listener {
void contactsSelected(Collection<ContactId> selected);
void contactSelectionCancelled();
}
}