feat: add GIF supported AvatarView

This commit is contained in:
nivl4 2016-05-23 13:00:16 +08:00
parent 2fd059d578
commit e2e24317ca
6 changed files with 545 additions and 0 deletions

View file

@ -117,6 +117,13 @@ dependencies {
//Analytics
compile 'com.amplitude:android-sdk:2.7.1'
// Fresco Image Management Library
compile('com.facebook.fresco:fresco:0.10.0') {
exclude module: 'bolts-android'
}
compile('com.facebook.fresco:animated-gif:0.10.0') {
exclude module: 'bolts-android'
}
//Tests
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
testCompile 'org.robolectric:robolectric:3.0'

View file

@ -0,0 +1,7 @@
<resources>
<declare-styleable name="AvatarView">
<attr name="showBackground" format="boolean" />
<attr name="showMount" format="boolean" />
<attr name="showPet" format="boolean" />
</declare-styleable>
</resources>

View file

@ -0,0 +1,452 @@
package com.habitrpg.android.habitica.ui;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.drawee.view.MultiDraweeHolder;
import com.facebook.imagepipeline.image.ImageInfo;
import com.habitrpg.android.habitica.R;
import com.magicmicky.habitrpgwrapper.lib.models.HabitRPGUser;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class AvatarView extends View {
public static final String IMAGE_URI_ROOT = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/";
private static final String TAG = "AvatarView";
private static final Map<String, String> sFilenameMap;
private static final Rect sFullHeroRect = new Rect(0, 0, 140, 147);
private static final Rect sCompactHeroRect = new Rect(0, 0, 114, 114);
private static final Rect sHeroOnlyRect = new Rect(0, 0, 90, 90);
static {
Map<String, String> tempMap = new HashMap<>();
tempMap.put("head_special_1", "ContributorOnly-Equip-CrystalHelmet.gif");
tempMap.put("armor_special_1", "ContributorOnly-Equip-CrystalArmor.gif");
tempMap.put("weapon_special_critical", "weapon_special_critical.gif");
tempMap.put("Pet-Wolf-Cerberus", "Pet-Wolf-Cerberus.gif");
sFilenameMap = Collections.unmodifiableMap(tempMap);
}
private boolean mShowBackground = true;
private boolean mShowMount = true;
private boolean mShowPet = true;
private boolean mHasBackground;
private boolean mHasMount;
private boolean mHasPet;
private boolean mIsOrphan;
private MultiDraweeHolder<GenericDraweeHierarchy> mMultiDraweeHolder = new MultiDraweeHolder<>();
private HabitRPGUser mUser;
private RectF mAvatarRectF;
private Matrix mMatrix = new Matrix();
private AtomicInteger mNumberLayersInProcess = new AtomicInteger(0);
private Consumer<Bitmap> mAvatarImageConsumer;
private Bitmap mAvatarBitmap;
private Canvas mAvatarCanvas;
public AvatarView(Context context) {
super(context);
init(null, 0);
}
public AvatarView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public AvatarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
public AvatarView(Context context, boolean showBackground, boolean showMount, boolean showPet) {
super(context);
mShowBackground = showBackground;
mShowMount = showMount;
mShowPet = showPet;
mIsOrphan = true;
}
private void init(AttributeSet attrs, int defStyle) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.AvatarView, defStyle, 0);
try {
mShowBackground = a.getBoolean(R.styleable.AvatarView_showBackground, true);
mShowMount = a.getBoolean(R.styleable.AvatarView_showMount, true);
mShowPet = a.getBoolean(R.styleable.AvatarView_showPet, true);
} finally {
a.recycle();
}
}
private void showLayers(@NonNull Map<LayerType, String> layerMap) {
if (mMultiDraweeHolder.size() > 0) return;
int i = 0;
mNumberLayersInProcess.set(layerMap.size());
for (Map.Entry<LayerType, String> entry : layerMap.entrySet()) {
final LayerType layerKey = entry.getKey();
final String layerName = entry.getValue();
final int layerNumber = i++;
GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources())
.setFadeDuration(0)
.build();
DraweeHolder<GenericDraweeHierarchy> draweeHolder = DraweeHolder.create(hierarchy, getContext());
draweeHolder.getTopLevelDrawable().setCallback(this);
mMultiDraweeHolder.add(draweeHolder);
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(IMAGE_URI_ROOT + getFileName(layerName))
.setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(
String id,
ImageInfo imageInfo,
Animatable anim) {
if (imageInfo != null) {
mMultiDraweeHolder.get(layerNumber).getTopLevelDrawable().setBounds(getLayerBounds(layerKey, layerName, imageInfo));
onLayerComplete();
}
}
@Override
public void onFailure(String id, Throwable throwable) {
Log.e(TAG, "Error loading layer: " + layerName, throwable);
onLayerComplete();
}
})
.setAutoPlayAnimations(!mIsOrphan)
.build();
draweeHolder.setController(controller);
}
if (mIsOrphan) mMultiDraweeHolder.onAttach();
}
private Map<LayerType, String> getLayerMap() {
assert mUser != null;
return getLayerMap(mUser, true);
}
private Map<LayerType, String> getLayerMap(@NonNull HabitRPGUser user, boolean resetHasAttributes) {
EnumMap<LayerType, String> layerMap = user.getAvatarLayerMap();
if (resetHasAttributes) mHasBackground = mHasMount = mHasPet = false;
String mountName = user.getItems().getCurrentMount();
if (mShowMount && !TextUtils.isEmpty(mountName)) {
layerMap.put(LayerType.MOUNT_BODY, "Mount_Body_" + mountName);
layerMap.put(LayerType.MOUNT_HEAD, "Mount_Head_" + mountName);
if (resetHasAttributes) mHasMount = true;
}
String petName = user.getItems().getCurrentPet();
if (mShowPet && !TextUtils.isEmpty(petName)) {
layerMap.put(LayerType.PET, "Pet-" + petName);
if (resetHasAttributes) mHasPet = true;
}
String backgroundName = user.getPreferences().getBackground();
if (mShowBackground && !TextUtils.isEmpty(backgroundName)) {
layerMap.put(LayerType.BACKGROUND, "background_" + backgroundName);
if (resetHasAttributes) mHasBackground = true;
}
return layerMap;
}
private Rect getLayerBounds(@NonNull LayerType layerType, @NonNull String layerName, @NonNull ImageInfo layerImageInfo) {
PointF offset = null;
Rect bounds = new Rect(0, 0, layerImageInfo.getWidth(), layerImageInfo.getHeight());
RectF boundsF = new RectF(bounds);
// lookup layer specific offset
switch (layerName) {
case "weapon_special_critical":
if (mShowMount || mShowPet) {
// full hero box
if (mHasMount) {
offset = new PointF(13.0f, 12.0f);
} else if (mHasPet) {
offset = new PointF(13.0f, 24.5f + 12.0f);
} else {
offset = new PointF(13.0f, 28.0f + 12.0f);
}
} else if (mShowBackground) {
// compact hero box
offset = new PointF(-12.0f, 18.0f + 12.0f);
} else {
// hero only box
offset = new PointF(-12.0f, 12.0f);
}
break;
default:
break;
}
// otherwise lookup default layer type based offset
if (offset == null) {
switch (layerType) {
case BACKGROUND:
if (!(mShowMount || mShowPet)) {
offset = new PointF(-25.0f, 0.0f); // compact hero box
}
break;
case MOUNT_BODY:
case MOUNT_HEAD:
offset = new PointF(25.0f, 18.0f); // full hero box
break;
case BACK:
case SKIN:
case SHIRT:
case ARMOR:
case BODY:
case HEAD_0:
case HAIR_BASE:
case HAIR_BANGS:
case HAIR_MUSTACHE:
case HAIR_BEARD:
case EYEWEAR:
case HEAD:
case HEAD_ACCESSORY:
case HAIR_FLOWER:
case SHIELD:
case WEAPON:
case ZZZ:
if (mShowMount || mShowPet) {
// full hero box
if (mHasMount) {
offset = new PointF(25.0f, 0);
} else if (mHasPet) {
offset = new PointF(25.0f, 24.5f);
} else {
offset = new PointF(25.0f, 28.0f);
}
} else if (mShowBackground) {
// compact hero box
offset = new PointF(0.0f, 18.0f);
}
break;
case PET:
offset = new PointF(0, sFullHeroRect.height() - layerImageInfo.getHeight());
break;
default:
break;
}
}
if (offset != null) {
Matrix translateMatrix = new Matrix();
translateMatrix.setTranslate(offset.x, offset.y);
translateMatrix.mapRect(boundsF);
}
// resize bounds to fit and keep original aspect ratio
mMatrix.mapRect(boundsF);
boundsF.round(bounds);
return bounds;
}
private String getFileName(@NonNull String imageName) {
if (sFilenameMap.containsKey(imageName)) {
return sFilenameMap.get(imageName);
}
return imageName + ".png";
}
private void onLayerComplete() {
if (mNumberLayersInProcess.decrementAndGet() == 0) {
if (mAvatarImageConsumer != null) {
mAvatarImageConsumer.accept(getAvatarImage());
}
}
}
public void onAvatarImageReady(@NonNull Consumer<Bitmap> consumer) {
mAvatarImageConsumer = consumer;
if (mMultiDraweeHolder.size() > 0 && mNumberLayersInProcess.get() == 0) {
mAvatarImageConsumer.accept(getAvatarImage());
} else {
initAvatarRectMatrix();
showLayers(getLayerMap());
}
}
public void setUser(@NonNull HabitRPGUser user) {
HabitRPGUser oldUser = mUser;
mUser = user;
if (oldUser != null) {
Map<LayerType, String> currentLayerMap = getLayerMap(oldUser, false);
Map<LayerType, String> newLayerMap = getLayerMap(user, false);
if (!currentLayerMap.equals(newLayerMap)) {
mMultiDraweeHolder.clear();
mNumberLayersInProcess.set(0);
}
}
invalidate();
}
private Rect getOriginalRect() {
return (mShowMount || mShowPet) ? sFullHeroRect : ((mShowBackground) ? sCompactHeroRect : sHeroOnlyRect);
}
private Bitmap getAvatarImage() {
assert mUser != null;
assert mAvatarRectF != null;
Rect canvasRect = new Rect();
mAvatarRectF.round(canvasRect);
mAvatarBitmap = Bitmap.createBitmap(canvasRect.width(), canvasRect.height(), Bitmap.Config.ARGB_8888);
mAvatarCanvas = new Canvas(mAvatarBitmap);
draw(mAvatarCanvas);
return mAvatarBitmap;
}
private void initAvatarRectMatrix() {
if (mAvatarRectF == null) {
Rect srcRect = getOriginalRect();
if (mIsOrphan) {
mAvatarRectF = new RectF(srcRect);
// change scale to not be 1:1
// a quick fix as fresco AnimatedDrawable/ScaleTypeDrawable
// will not translate matrix properly
mMatrix.setScale(1.2f, 1.2f);
mMatrix.mapRect(mAvatarRectF);
} else {
// full hero box when showMount and showPet is enabled (140w * 147h)
// compact hero box when only showBackground is enabled (114w * 114h)
// hero only box when all show settings disabled (90w * 90h)
mAvatarRectF = new RectF(0, 0, getWidth(), getHeight());
mMatrix.setRectToRect(new RectF(srcRect), mAvatarRectF, Matrix.ScaleToFit.START); // TODO support other ScaleToFit
mAvatarRectF = new RectF(srcRect);
mMatrix.mapRect(mAvatarRectF);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initAvatarRectMatrix();
// draw only when user is set
if (mUser == null) return;
// request image layers if not yet processed
if (mMultiDraweeHolder.size() == 0) {
showLayers(getLayerMap());
}
// manually call onAttach/onDetach if view is without parent as they will never be called otherwise
if (mIsOrphan) mMultiDraweeHolder.onAttach();
mMultiDraweeHolder.draw(canvas);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mMultiDraweeHolder.onDetach();
}
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
mMultiDraweeHolder.onDetach();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mMultiDraweeHolder.onAttach();
}
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
mMultiDraweeHolder.onAttach();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mMultiDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event);
}
@Override
protected boolean verifyDrawable(Drawable who) {
return mMultiDraweeHolder.verifyDrawable(who) || super.verifyDrawable(who);
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
invalidate();
if (mAvatarCanvas != null) draw(mAvatarCanvas);
}
public enum LayerType {
BACKGROUND(0),
MOUNT_BODY(1),
BACK(2),
SKIN(3),
SHIRT(4),
ARMOR(5),
BODY(6),
HEAD_0(7),
HAIR_BASE(8),
HAIR_BANGS(9),
HAIR_MUSTACHE(10),
HAIR_BEARD(11),
EYEWEAR(12),
HEAD(13),
HEAD_ACCESSORY(14),
HAIR_FLOWER(15),
SHIELD(16),
WEAPON(17),
MOUNT_HEAD(18),
ZZZ(19),
PET(20);
final int order;
LayerType(int order) {
this.order = order;
}
}
public interface Consumer<T> {
void accept(T t);
}
}

View file

@ -3,6 +3,7 @@ package com.magicmicky.habitrpgwrapper.lib.models;
import com.google.gson.annotations.SerializedName;
import com.habitrpg.android.habitica.HabitDatabase;
import com.habitrpg.android.habitica.ui.AvatarView;
import com.magicmicky.habitrpgwrapper.lib.models.tasks.Task;
import com.magicmicky.habitrpgwrapper.lib.models.tasks.TasksOrder;
import com.raizlabs.android.dbflow.annotation.Column;
@ -15,8 +16,12 @@ import com.raizlabs.android.dbflow.sql.builder.Condition;
import com.raizlabs.android.dbflow.sql.language.Select;
import com.raizlabs.android.dbflow.structure.BaseModel;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
@Table(databaseName = HabitDatabase.NAME)
public class HabitRPGUser extends BaseModel {
@ -365,4 +370,69 @@ public class HabitRPGUser extends BaseModel {
return layerNames;
}
public EnumMap<AvatarView.LayerType, String> getAvatarLayerMap() {
EnumMap<AvatarView.LayerType, String> layerMap = new EnumMap<>(AvatarView.LayerType.class);
Preferences prefs = getPreferences();
Outfit outfit = (prefs.getCostume()) ? getItems().getGear().getCostume() : getItems().getGear().getEquipped();
if (outfit != null) {
if (!TextUtils.isEmpty(outfit.getBack())) {
layerMap.put(AvatarView.LayerType.BACK, outfit.getBack());
}
if (outfit.isAvailable(outfit.getArmor())) {
layerMap.put(AvatarView.LayerType.ARMOR, prefs.getSize() + "_" + outfit.getArmor());
}
if (outfit.isAvailable(outfit.getBody())) {
layerMap.put(AvatarView.LayerType.BODY, outfit.getBody());
}
if (outfit.isAvailable(outfit.getEyeWear())) {
layerMap.put(AvatarView.LayerType.EYEWEAR, outfit.getEyeWear());
}
if (outfit.isAvailable(outfit.getHead())) {
layerMap.put(AvatarView.LayerType.HEAD, outfit.getHead());
}
if (outfit.isAvailable(outfit.getHeadAccessory())) {
layerMap.put(AvatarView.LayerType.HEAD_ACCESSORY, outfit.getHeadAccessory());
}
if (outfit.isAvailable(outfit.getShield())) {
layerMap.put(AvatarView.LayerType.SHIELD, outfit.getShield());
}
if (outfit.isAvailable(outfit.getWeapon())) {
layerMap.put(AvatarView.LayerType.WEAPON, outfit.getWeapon());
}
}
layerMap.put(AvatarView.LayerType.SKIN, "skin_" + prefs.getSkin() + ((prefs.getSleep()) ? "_sleep" : ""));
layerMap.put(AvatarView.LayerType.SHIRT, prefs.getSize() + "_shirt_" + prefs.getShirt());
layerMap.put(AvatarView.LayerType.HEAD_0, "head_0");
Hair hair = prefs.getHair();
if (hair != null) {
String hairColor = hair.getColor();
if (hair.isAvailable(hair.getBase())) {
layerMap.put(AvatarView.LayerType.HAIR_BASE, "hair_base_" + hair.getBase() + "_" + hairColor);
}
if (hair.isAvailable(hair.getBangs())) {
layerMap.put(AvatarView.LayerType.HAIR_BANGS, "hair_bangs_" + hair.getBangs() + "_" + hairColor);
}
if (hair.isAvailable(hair.getMustache())) {
layerMap.put(AvatarView.LayerType.HAIR_MUSTACHE, "hair_mustache_" + hair.getMustache() + "_" + hairColor);
}
if (hair.isAvailable(hair.getBeard())) {
layerMap.put(AvatarView.LayerType.HAIR_BEARD, "hair_beard_" + hair.getBeard() + "_" + hairColor);
}
if (hair.isAvailable(hair.getFlower())) {
layerMap.put(AvatarView.LayerType.HAIR_FLOWER, "hair_flower_" + hair.getFlower());
}
}
if (prefs.getSleep()) {
layerMap.put(AvatarView.LayerType.ZZZ, "zzz");
}
return layerMap;
}
}

View file

@ -75,4 +75,8 @@ public class Hair extends BaseModel {
public int getFlower() { return flower; }
public void setFlower(int flower) { this.flower = flower; }
public boolean isAvailable(int hairId) {
return hairId > 0;
}
}

View file

@ -9,6 +9,8 @@ import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
import android.text.TextUtils;
/**
* Created by viirus on 20/07/15.
*/
@ -54,4 +56,7 @@ public class Outfit extends BaseModel {
public String getWeapon() {return weapon;}
public void setWeapon(String weapon) {this.weapon = weapon;}
public boolean isAvailable(String outfit) {
return !TextUtils.isEmpty(outfit) && !outfit.endsWith("base_0");
}
}