Add k-9 style avatars to group conversations

Closes #1107
This commit is contained in:
Lukas Barth 2014-03-09 14:00:39 +01:00 committed by Moxie Marlinspike
parent 2f6cefca8a
commit e5e5b93884
5 changed files with 154 additions and 1 deletions

Binary file not shown.

View File

@ -39,6 +39,20 @@
<item>Ukrainian Український</item> <item>Ukrainian Український</item>
</string-array> </string-array>
<array name="avatar_colors">
<item>#6dcaec</item>
<item>#cf9fe7</item>
<item>#b6db49</item>
<item>#ffd060</item>
<item>#ff7979</item>
<item>#2cb1e1</item>
<item>#c182e0</item>
<item>#92c500</item>
<item>#ffb61c</item>
<item>#f83a3a</item>
</array>
<string-array name="language_values"> <string-array name="language_values">
<item>zz</item> <item>zz</item>
<item>en</item> <item>en</item>

View File

@ -22,6 +22,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
@ -40,6 +41,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@ -405,7 +407,15 @@ public class ConversationItem extends LinearLayout {
private void setContactPhotoForRecipient(final Recipient recipient) { private void setContactPhotoForRecipient(final Recipient recipient) {
if (contactPhoto == null) return; if (contactPhoto == null) return;
contactPhoto.setImageBitmap(recipient.getCircleCroppedContactPhoto()); Bitmap contactPhotoBitmap;
if ((recipient.getContactPhoto() == ContactPhotoFactory.getDefaultContactPhoto(context)) && (groupThread)) {
contactPhotoBitmap = recipient.getGeneratedAvatar(context);
} else {
contactPhotoBitmap = recipient.getCircleCroppedContactPhoto();
}
contactPhoto.setImageBitmap(contactPhotoBitmap);
contactPhoto.setOnClickListener(new View.OnClickListener() { contactPhoto.setOnClickListener(new View.OnClickListener() {
@Override @Override

View File

@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.util.BitmapUtil;
/**
* Utility class to generate avatars for contacts who don't have a contact
* picture set.
*
* @author Lukas Barth
*/
public class AvatarGenerator {
public static Bitmap generateFor(Context context, Recipient recipient) {
if ((recipient == null) || (recipient.getName() == null)) {
return BitmapUtil.getCircleCroppedBitmap(ContactPhotoFactory.getDefaultContactPhoto(context));
}
final int size = ContactPhotoFactory.getDefaultContactPhoto(context).getHeight();
final Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final int color = getColorForRecipient(recipient, context);
final Paint paint = new Paint();
final int innerRectOffset = (int) Math.ceil((size - Math.sqrt(2) * (size / 2)) / 2);
final Rect innerRect = new Rect(innerRectOffset, innerRectOffset,
size - innerRectOffset, size - innerRectOffset);
paint.setAntiAlias(true);
paint.setColor(color);
canvas.drawCircle(size / 2, size / 2, size / 2, paint);
paint.setColor(Color.WHITE);
Typeface robotoLightTypeface = Typeface.createFromAsset(context.getAssets(), "fonts/Roboto-Light.ttf");
paint.setTypeface(robotoLightTypeface);
setFontSize(innerRect, paint);
paint.setTextAlign(Paint.Align.CENTER);
int initialIndex = 0;
char[] contactName = recipient.getName().toCharArray();
if (contactName.length == 0) {
contactName = new char[]{'?'};
initialIndex = 0;
} else {
while ((! Character.isLetter(contactName[initialIndex]))) {
initialIndex ++;
if (initialIndex >= contactName.length) {
contactName[0] = '?';
initialIndex = 0;
break;
}
}
}
Rect textBounds = new Rect();
paint.getTextBounds(contactName, initialIndex, 1, textBounds);
int bottomOffset = (innerRect.height() - textBounds.height()) / 2;
canvas.drawText(Character.toString(contactName[initialIndex]),
innerRect.centerX(), innerRect.bottom - bottomOffset, paint);
return output;
}
private static int getColorForRecipient(Recipient recipient, Context context) {
if ((recipient == null) || (recipient.getName() == null)) {
return Color.WHITE;
}
long nameHash = recipient.getName().hashCode();
Resources res = context.getResources();
TypedArray colorArray = res.obtainTypedArray(R.array.avatar_colors);
int index = Math.abs((int) (nameHash % colorArray.length()));
int color = colorArray.getColor(index, Color.BLACK);
colorArray.recycle();
return color;
}
private static int setFontSize(Rect textRect, Paint paint) {
boolean overflow = false;
int currentSize = 0;
while (!overflow) {
currentSize++;
paint.setTextSize(currentSize);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
int textHeight = fontMetrics.descent - fontMetrics.ascent;
if (textHeight > textRect.height()) {
overflow = true;
}
}
currentSize--;
currentSize *= 1.2;
paint.setTextSize(currentSize);
return currentSize;
}
}

View File

@ -54,6 +54,7 @@ public class Recipient implements Parcelable {
private Bitmap contactPhoto; private Bitmap contactPhoto;
private Bitmap circleCroppedContactPhoto; private Bitmap circleCroppedContactPhoto;
private Bitmap generatedAvatar;
private Uri contactUri; private Uri contactUri;
@ -64,6 +65,7 @@ public class Recipient implements Parcelable {
this.circleCroppedContactPhoto = circleCroppedContactPhoto; this.circleCroppedContactPhoto = circleCroppedContactPhoto;
this.contactPhoto = contactPhoto; this.contactPhoto = contactPhoto;
this.recipientId = recipientId; this.recipientId = recipientId;
this.generatedAvatar = null;
future.addListener(new FutureTaskListener<RecipientDetails>() { future.addListener(new FutureTaskListener<RecipientDetails>() {
@Override @Override
@ -187,6 +189,13 @@ public class Recipient implements Parcelable {
return this.circleCroppedContactPhoto; return this.circleCroppedContactPhoto;
} }
public synchronized Bitmap getGeneratedAvatar(Context context) {
if (this.generatedAvatar == null)
this.generatedAvatar = AvatarGenerator.generateFor(context, this);
return this.generatedAvatar;
}
public static Recipient getUnknownRecipient(Context context) { public static Recipient getUnknownRecipient(Context context) {
return new Recipient("Unknown", "Unknown", -1, null, return new Recipient("Unknown", "Unknown", -1, null,
ContactPhotoFactory.getDefaultContactPhoto(context), ContactPhotoFactory.getDefaultContactPhoto(context),