correctly display contributor gear. Fixes #485

This commit is contained in:
Phillip Thelen 2018-02-12 16:11:52 +01:00
parent e58cb400b4
commit 27dfb7e342
21 changed files with 779 additions and 1082 deletions

View file

@ -24,7 +24,7 @@ abstract class ContentRepositoryImpl<T : ContentLocalRepository>(localRepository
val now = Date().time
return if (forced || now - this.lastContentSync > 3600000) {
lastContentSync = now
apiClient.content.doOnNext({ localRepository.saveContent(it) })
apiClient.getContent().doOnNext({ localRepository.saveContent(it) })
} else {
Observable.just(null)
}
@ -34,7 +34,7 @@ abstract class ContentRepositoryImpl<T : ContentLocalRepository>(localRepository
val now = Date().time
return if (now - this.lastWorldStateSync > 3600000) {
lastWorldStateSync = now
apiClient.worldState.doOnNext({ localRepository.saveWorldState(it) })
apiClient.getWorldState().doOnNext({ localRepository.saveWorldState(it) })
} else {
Observable.just(null)
}

View file

@ -279,7 +279,7 @@ class InventoryRepositoryImpl(localRepository: InventoryLocalRepository, apiClie
override fun togglePinnedItem(item: ShopItem): Observable<List<ShopItem>> {
return if (!item.isValid) {
Observable.just(null)
} else apiClient.togglePinnedItem(item.pinType, item.path)
} else apiClient.togglePinnedItem(item.pinType ?: "", item.path ?: "")
.flatMap { retrieveInAppRewards() }
}
}

View file

@ -43,7 +43,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
override fun flagMessage(chatMessage: ChatMessage): Observable<Void> {
return if (chatMessage.id == "") {
Observable.just(null)
} else apiClient.flagMessage(chatMessage.groupId, chatMessage.id)
} else apiClient.flagMessage(chatMessage.groupId ?: "", chatMessage.id)
}
override fun likeMessage(chatMessage: ChatMessage): Observable<ChatMessage> {
@ -52,12 +52,12 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
}
val liked = chatMessage.userLikesMessage(userId)
localRepository.likeMessage(chatMessage, userId, !liked)
return apiClient.likeMessage(chatMessage.groupId, chatMessage.id)
return apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id)
.doOnError { localRepository.likeMessage(chatMessage, userId, liked) }
}
override fun deleteMessage(chatMessage: ChatMessage): Observable<Void> {
return apiClient.deleteMessage(chatMessage.groupId, chatMessage.id)
return apiClient.deleteMessage(chatMessage.groupId ?: "", chatMessage.id)
.doOnNext { localRepository.deleteMessage(chatMessage.id) }
}
@ -159,7 +159,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
override fun inviteToGroup(id: String, inviteData: Map<String, Any>): Observable<List<String>> = apiClient.inviteToGroup(id, inviteData)
override fun getUserChallenges(): Observable<List<Challenge>> = apiClient.userChallenges
override fun getUserChallenges(): Observable<List<Challenge>> = apiClient.getUserChallenges()
override fun getMember(userId: String?): Observable<Member> {
return if (userId == null) {

View file

@ -36,7 +36,7 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
}
override fun retrieveTasks(userId: String, tasksOrder: TasksOrder): Observable<TaskList> {
return this.apiClient.tasks
return this.apiClient.getTasks()
.doOnNext { res -> this.localRepository.saveTasks(userId, tasksOrder, res) }
}
@ -58,11 +58,15 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
override fun taskChecked(user: User?, task: Task, up: Boolean, force: Boolean): Observable<TaskScoringResult?> {
val now = Date().time
val id = task.id
if (lastTaskAction > now - 500 && !force) {
return Observable.just(null)
}
if (id == null) {
return Observable.just(null)
}
lastTaskAction = now
return this.apiClient.postTaskDirection(task.id, (if (up) TaskDirection.up else TaskDirection.down).toString())
return this.apiClient.postTaskDirection(id, (if (up) TaskDirection.up else TaskDirection.down).toString())
.map { res ->
// save local task changes
val result = TaskScoringResult()
@ -152,8 +156,12 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
return Observable.just(task)
}
lastTaskAction = now
return localRepository.getTaskCopy(task.id ?: "").first()
.flatMap { task1 -> apiClient.updateTask(task1.id, task1) }
val id = task.id
if (id == null) {
return Observable.just(task)
}
return localRepository.getTaskCopy(id).first()
.flatMap { task1 -> apiClient.updateTask(id, task1) }
.map { task1 ->
task1.position = task.position
task1
@ -193,7 +201,7 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
.first()
.flatMap { task ->
if (task.isValid) {
return@flatMap apiClient.postTaskNewPosition(task.id, newPosition)
return@flatMap apiClient.postTaskNewPosition(task.id ?: "", newPosition)
}
return@flatMap Observable.just<List<String>>(ArrayList())
}

View file

@ -1,106 +0,0 @@
package com.habitrpg.android.habitica.models.user;
import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Outfit extends RealmObject {
@PrimaryKey
private String userId;
Gear gear;
String armor, back, body, head, shield, weapon;
@SerializedName("eyewear")
String eyeWear;
String headAccessory;
public String getArmor() {
return armor;
}
public void setArmor(String armor) {
this.armor = armor;
}
public String getBack() {
return back;
}
public void setBack(String back) {
this.back = back;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getEyeWear() {
return eyeWear;
}
public void setEyeWear(String eyeWear) {
this.eyeWear = eyeWear;
}
public String getHead() {
return head;
}
public void setHead(String head) {
this.head = head;
}
public String getHeadAccessory() {
return headAccessory;
}
public void setHeadAccessory(String headAccessory) {
this.headAccessory = headAccessory;
}
public String getShield() {
return shield;
}
public void setShield(String shield) {
this.shield = shield;
}
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");
}
public void updateWith(Outfit newOutfit) {
this.setArmor(newOutfit.getArmor());
this.setBack(newOutfit.getBack());
this.setBody(newOutfit.getBody());
this.setEyeWear(newOutfit.getEyeWear());
this.setHead(newOutfit.getHead());
this.setHeadAccessory(newOutfit.getHeadAccessory());
this.setShield(newOutfit.getShield());
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}

View file

@ -0,0 +1,39 @@
package com.habitrpg.android.habitica.models.user
import android.text.TextUtils
import com.google.gson.annotations.SerializedName
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class Outfit : RealmObject() {
@PrimaryKey
var userId: String? = null
internal var gear: Gear? = null
var armor: String = ""
var back: String = ""
var body: String = ""
var head: String = ""
var shield: String = ""
var weapon: String = ""
@SerializedName("eyewear")
var eyeWear: String = ""
var headAccessory: String = ""
fun isAvailable(outfit: String): Boolean {
return !TextUtils.isEmpty(outfit) && !outfit.endsWith("base_0")
}
fun updateWith(newOutfit: Outfit) {
this.armor = newOutfit.armor
this.back = newOutfit.back
this.body = newOutfit.body
this.eyeWear = newOutfit.eyeWear
this.head = newOutfit.head
this.headAccessory = newOutfit.headAccessory
this.shield = newOutfit.shield
}
}

View file

@ -1,599 +0,0 @@
package com.habitrpg.android.habitica.ui;
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 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.habitrpg.android.habitica.models.Avatar;
import com.habitrpg.android.habitica.models.AvatarPreferences;
import com.habitrpg.android.habitica.models.user.Buffs;
import com.habitrpg.android.habitica.models.user.Hair;
import com.habitrpg.android.habitica.models.user.Outfit;
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> FILENAME_MAP;
private static final Rect FULL_HERO_RECT = new Rect(0, 0, 140, 147);
private static final Rect COMPACT_HERO_RECT = new Rect(0, 0, 114, 114);
private static final Rect HERO_ONLY_RECT = 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");
FILENAME_MAP = Collections.unmodifiableMap(tempMap);
}
private boolean showBackground = true;
private boolean showMount = true;
private boolean showPet = true;
private boolean showSleeping = true;
private boolean hasBackground;
private boolean hasMount;
private boolean hasPet;
private boolean isOrphan;
private MultiDraweeHolder<GenericDraweeHierarchy> multiDraweeHolder = new MultiDraweeHolder<>();
private Avatar avatar;
private RectF avatarRectF;
private Matrix matrix = new Matrix();
private AtomicInteger numberLayersInProcess = new AtomicInteger(0);
private Consumer<Bitmap> avatarImageConsumer;
private Bitmap avatarBitmap;
private Canvas avatarCanvas;
private Map<LayerType, String> currentLayers;
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);
this.showBackground = showBackground;
this.showMount = showMount;
this.showPet = showPet;
isOrphan = true;
}
public void configureView(boolean showBackground, boolean showMount, boolean showPet) {
this.showBackground = showBackground;
this.showMount = showMount;
this.showPet = showPet;
}
private void init(AttributeSet attrs, int defStyle) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.AvatarView, defStyle, 0);
try {
showBackground = a.getBoolean(R.styleable.AvatarView_showBackground, true);
showMount = a.getBoolean(R.styleable.AvatarView_showMount, true);
showPet = a.getBoolean(R.styleable.AvatarView_showPet, true);
showSleeping = a.getBoolean(R.styleable.AvatarView_showSleeping, true);
} finally {
a.recycle();
}
}
private void showLayers(@NonNull Map<LayerType, String> layerMap) {
if (multiDraweeHolder.size() > 0) return;
int i = 0;
currentLayers = layerMap;
numberLayersInProcess.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);
multiDraweeHolder.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) {
if (multiDraweeHolder.size() > layerNumber) {
multiDraweeHolder.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(!isOrphan)
.build();
draweeHolder.setController(controller);
}
if (isOrphan) multiDraweeHolder.onAttach();
}
private Map<LayerType, String> getLayerMap() {
assert avatar != null;
return getLayerMap(avatar, true);
}
private Map<LayerType, String> getLayerMap(@NonNull Avatar avatar, boolean resetHasAttributes) {
EnumMap<LayerType, String> layerMap = getAvatarLayerMap(avatar);
if (resetHasAttributes) hasBackground = hasMount = hasPet = false;
String mountName = avatar.getCurrentMount();
if (showMount && !TextUtils.isEmpty(mountName)) {
layerMap.put(LayerType.MOUNT_BODY, "Mount_Body_" + mountName);
layerMap.put(LayerType.MOUNT_HEAD, "Mount_Head_" + mountName);
if (resetHasAttributes) hasMount = true;
}
String petName = avatar.getCurrentPet();
if (showPet && !TextUtils.isEmpty(petName)) {
layerMap.put(LayerType.PET, "Pet-" + petName);
if (resetHasAttributes) hasPet = true;
}
String backgroundName = avatar.getBackground();
if (showBackground && !TextUtils.isEmpty(backgroundName)) {
layerMap.put(LayerType.BACKGROUND, "background_" + backgroundName);
if (resetHasAttributes) hasBackground = true;
}
if (showSleeping && avatar.getSleep()) {
layerMap.put(AvatarView.LayerType.ZZZ, "zzz");
}
return layerMap;
}
public EnumMap<AvatarView.LayerType, String> getAvatarLayerMap(Avatar avatar) {
EnumMap<AvatarView.LayerType, String> layerMap = new EnumMap<>(AvatarView.LayerType.class);
if (!avatar.isValid()) {
return layerMap;
}
AvatarPreferences prefs = avatar.getPreferences();
if (prefs == null) {
return layerMap;
}
Outfit outfit;
if (prefs.getCostume()) {
outfit = avatar.getCostume();
} else {
outfit = avatar.getEquipped();
}
boolean hasVisualBuffs = false;
if (avatar.getStats() != null && avatar.getStats().getBuffs() != null) {
Buffs buffs = avatar.getStats().getBuffs();
if (buffs.getSnowball()) {
layerMap.put(AvatarView.LayerType.VISUAL_BUFF, "snowman");
hasVisualBuffs = true;
}
if (buffs.getSeafoam()) {
layerMap.put(AvatarView.LayerType.VISUAL_BUFF, "seafoam_star");
hasVisualBuffs = true;
}
if (buffs.getShinySeed()) {
layerMap.put(AvatarView.LayerType.VISUAL_BUFF, "avatar_floral_" + avatar.getStats().getHabitClass());
hasVisualBuffs = true;
}
if (buffs.getSpookySparkles()) {
layerMap.put(AvatarView.LayerType.VISUAL_BUFF, "ghost");
hasVisualBuffs = true;
}
}
if (!hasVisualBuffs) {
if (!TextUtils.isEmpty(prefs.getChair())) {
layerMap.put(AvatarView.LayerType.CHAIR, prefs.getChair());
}
if (outfit != null) {
if (!TextUtils.isEmpty(outfit.getBack()) && !"back_base_0".equals(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());
}
}
} else {
Hair hair = prefs.getHair();
// Show flower all the time!
if (hair != null && hair.isAvailable(hair.getFlower())) {
layerMap.put(AvatarView.LayerType.HAIR_FLOWER, "hair_flower_" + hair.getFlower());
}
}
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 (showMount || showPet) {
// full hero box
if (hasMount) {
offset = new PointF(13.0f, 12.0f);
} else if (hasPet) {
offset = new PointF(13.0f, 24.5f + 12.0f);
} else {
offset = new PointF(13.0f, 28.0f + 12.0f);
}
} else if (showBackground) {
// 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 (!(showMount || showPet)) {
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 CHAIR:
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 VISUAL_BUFF:
case HEAD:
case HEAD_ACCESSORY:
case HAIR_FLOWER:
case SHIELD:
case WEAPON:
case ZZZ:
if (showMount || showPet) {
// full hero box
if (hasMount) {
offset = new PointF(25.0f, 0);
} else if (hasPet) {
offset = new PointF(25.0f, 24.5f);
} else {
offset = new PointF(25.0f, 28.0f);
}
} else if (showBackground) {
// compact hero box
offset = new PointF(0.0f, 18.0f);
}
break;
case PET:
offset = new PointF(0, FULL_HERO_RECT.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
matrix.mapRect(boundsF);
boundsF.round(bounds);
return bounds;
}
private String getFileName(@NonNull String imageName) {
if (FILENAME_MAP.containsKey(imageName)) {
return FILENAME_MAP.get(imageName);
}
return imageName + ".png";
}
private void onLayerComplete() {
if (numberLayersInProcess.decrementAndGet() == 0) {
if (avatarImageConsumer != null) {
avatarImageConsumer.accept(getAvatarImage());
}
}
}
public void onAvatarImageReady(@NonNull Consumer<Bitmap> consumer) {
avatarImageConsumer = consumer;
if (multiDraweeHolder.size() > 0 && numberLayersInProcess.get() == 0) {
avatarImageConsumer.accept(getAvatarImage());
} else {
initAvatarRectMatrix();
showLayers(getLayerMap());
}
}
public void setAvatar(@NonNull Avatar avatar) {
Avatar oldUser = this.avatar;
this.avatar = avatar;
if (oldUser != null) {
Map<LayerType, String> newLayerMap = getLayerMap(avatar, false);
boolean equals = currentLayers != null && currentLayers.equals(newLayerMap);
if (!equals) {
multiDraweeHolder.clear();
numberLayersInProcess.set(0);
}
}
invalidate();
}
private Rect getOriginalRect() {
return (showMount || showPet) ? FULL_HERO_RECT : ((showBackground) ? COMPACT_HERO_RECT : HERO_ONLY_RECT);
}
private Bitmap getAvatarImage() {
assert avatar != null;
assert avatarRectF != null;
Rect canvasRect = new Rect();
avatarRectF.round(canvasRect);
avatarBitmap = Bitmap.createBitmap(canvasRect.width(), canvasRect.height(), Bitmap.Config.ARGB_8888);
avatarCanvas = new Canvas(avatarBitmap);
draw(avatarCanvas);
return avatarBitmap;
}
private void initAvatarRectMatrix() {
if (avatarRectF == null) {
Rect srcRect = getOriginalRect();
if (isOrphan) {
avatarRectF = new RectF(srcRect);
// change scale to not be 1:1
// a quick fix as fresco AnimatedDrawable/ScaleTypeDrawable
// will not translate matrix properly
matrix.setScale(1.2f, 1.2f);
matrix.mapRect(avatarRectF);
} 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)
avatarRectF = new RectF(0, 0, getWidth(), getHeight());
matrix.setRectToRect(new RectF(srcRect), avatarRectF, Matrix.ScaleToFit.START); // TODO support other ScaleToFit
avatarRectF = new RectF(srcRect);
matrix.mapRect(avatarRectF);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initAvatarRectMatrix();
// draw only when user is set
if (avatar == null || !avatar.isValid()) return;
// request image layers if not yet processed
if (multiDraweeHolder.size() == 0) {
showLayers(getLayerMap());
}
// manually call onAttach/onDetach if view is without parent as they will never be called otherwise
if (isOrphan) multiDraweeHolder.onAttach();
multiDraweeHolder.draw(canvas);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
multiDraweeHolder.onDetach();
}
@Override
public void onStartTemporaryDetach() {
super.onStartTemporaryDetach();
multiDraweeHolder.onDetach();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
multiDraweeHolder.onAttach();
}
@Override
public void onFinishTemporaryDetach() {
super.onFinishTemporaryDetach();
multiDraweeHolder.onAttach();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return multiDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event);
}
@Override
public boolean performClick() {
return super.performClick();
}
@Override
protected boolean verifyDrawable(Drawable who) {
return multiDraweeHolder.verifyDrawable(who) || super.verifyDrawable(who);
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
invalidate();
if (avatarCanvas != null) draw(avatarCanvas);
}
public enum LayerType {
BACKGROUND(0),
MOUNT_BODY(1),
CHAIR(2),
BACK(3),
SKIN(4),
SHIRT(5),
ARMOR(6),
BODY(7),
HEAD_0(8),
HAIR_BASE(9),
HAIR_BANGS(10),
HAIR_MUSTACHE(11),
HAIR_BEARD(12),
EYEWEAR(13),
VISUAL_BUFF(14),
HEAD(15),
HEAD_ACCESSORY(16),
HAIR_FLOWER(17),
SHIELD(18),
WEAPON(19),
MOUNT_HEAD(20),
ZZZ(21),
PET(22);
final int order;
LayerType(int order) {
this.order = order;
}
}
public interface Consumer<T> {
void accept(T t);
}
}

View file

@ -0,0 +1,540 @@
package com.habitrpg.android.habitica.ui
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.View
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.view.DraweeHolder
import com.facebook.drawee.view.MultiDraweeHolder
import com.facebook.imagepipeline.image.ImageInfo
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.Avatar
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
class AvatarView : View {
private var showBackground = true
private var showMount = true
private var showPet = true
private var showSleeping = true
private var hasBackground: Boolean = false
private var hasMount: Boolean = false
private var hasPet: Boolean = false
private var isOrphan: Boolean = false
private val multiDraweeHolder = MultiDraweeHolder<GenericDraweeHierarchy>()
private var avatar: Avatar? = null
private var avatarRectF: RectF? = null
private val avatarMatrix = Matrix()
private val numberLayersInProcess = AtomicInteger(0)
private var avatarImageConsumer: Consumer<Bitmap?>? = null
private var avatarBitmap: Bitmap? = null
private var avatarCanvas: Canvas? = null
private var currentLayers: Map<LayerType, String>? = null
private val layerMap: Map<LayerType, String>
get() {
assert(avatar != null)
return getLayerMap(avatar!!, true)
}
private val originalRect: Rect
get() = if (showMount || showPet) FULL_HERO_RECT else if (showBackground) COMPACT_HERO_RECT else HERO_ONLY_RECT
private val avatarImage: Bitmap?
get() {
assert(avatar != null)
assert(avatarRectF != null)
val canvasRect = Rect()
avatarRectF?.round(canvasRect)
avatarBitmap = Bitmap.createBitmap(canvasRect.width(), canvasRect.height(), Bitmap.Config.ARGB_8888)
avatarCanvas = Canvas(avatarBitmap)
draw(avatarCanvas)
return avatarBitmap
}
constructor(context: Context) : super(context) {
init(null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
init(attrs, defStyle)
}
constructor(context: Context, showBackground: Boolean, showMount: Boolean, showPet: Boolean) : super(context) {
this.showBackground = showBackground
this.showMount = showMount
this.showPet = showPet
isOrphan = true
}
fun configureView(showBackground: Boolean, showMount: Boolean, showPet: Boolean) {
this.showBackground = showBackground
this.showMount = showMount
this.showPet = showPet
}
private fun init(attrs: AttributeSet?, defStyle: Int) {
// Load attributes
val a = context.obtainStyledAttributes(
attrs, R.styleable.AvatarView, defStyle, 0)
try {
showBackground = a.getBoolean(R.styleable.AvatarView_showBackground, true)
showMount = a.getBoolean(R.styleable.AvatarView_showMount, true)
showPet = a.getBoolean(R.styleable.AvatarView_showPet, true)
showSleeping = a.getBoolean(R.styleable.AvatarView_showSleeping, true)
} finally {
a.recycle()
}
}
private fun showLayers(layerMap: Map<LayerType, String>) {
if (multiDraweeHolder.size() > 0) return
var i = 0
currentLayers = layerMap
numberLayersInProcess.set(layerMap.size)
for ((layerKey, layerName) in layerMap) {
val layerNumber = i++
val hierarchy = GenericDraweeHierarchyBuilder(resources)
.setFadeDuration(0)
.build()
val draweeHolder = DraweeHolder.create(hierarchy, context)
draweeHolder.topLevelDrawable.callback = this
multiDraweeHolder.add(draweeHolder)
val controller = Fresco.newDraweeControllerBuilder()
.setUri(IMAGE_URI_ROOT + getFileName(layerName))
.setControllerListener(object : BaseControllerListener<ImageInfo>() {
override fun onFinalImageSet(
id: String?,
imageInfo: ImageInfo?,
anim: Animatable?) {
if (imageInfo != null) {
if (multiDraweeHolder.size() > layerNumber) {
multiDraweeHolder.get(layerNumber).topLevelDrawable.bounds = getLayerBounds(layerKey, layerName, imageInfo)
}
onLayerComplete()
}
}
override fun onFailure(id: String?, throwable: Throwable?) {
Log.e(TAG, "Error loading layer: " + layerName, throwable)
onLayerComplete()
}
})
.setAutoPlayAnimations(!isOrphan)
.build()
draweeHolder.controller = controller
}
if (isOrphan) multiDraweeHolder.onAttach()
}
private fun getLayerMap(avatar: Avatar, resetHasAttributes: Boolean): Map<LayerType, String> {
val layerMap = getAvatarLayerMap(avatar)
if (resetHasAttributes) {
hasPet = false
hasMount = hasPet
hasBackground = hasMount
}
val mountName = avatar.currentMount
if (showMount && !TextUtils.isEmpty(mountName)) {
layerMap[LayerType.MOUNT_BODY] = "Mount_Body_" + mountName
layerMap[LayerType.MOUNT_HEAD] = "Mount_Head_" + mountName
if (resetHasAttributes) hasMount = true
}
val petName = avatar.currentPet
if (showPet && !TextUtils.isEmpty(petName)) {
layerMap[LayerType.PET] = "Pet-" + petName
if (resetHasAttributes) hasPet = true
}
val backgroundName = avatar.background
if (showBackground && !TextUtils.isEmpty(backgroundName)) {
layerMap[LayerType.BACKGROUND] = "background_" + backgroundName
if (resetHasAttributes) hasBackground = true
}
if (showSleeping && avatar.sleep) {
layerMap[AvatarView.LayerType.ZZZ] = "zzz"
}
return layerMap
}
private fun getAvatarLayerMap(avatar: Avatar): EnumMap<AvatarView.LayerType, String> {
val layerMap = EnumMap<AvatarView.LayerType, String>(AvatarView.LayerType::class.java)
if (!avatar.isValid) {
return layerMap
}
val prefs = avatar.preferences ?: return layerMap
val outfit = if (prefs.costume) {
avatar.costume
} else {
avatar.equipped
}
var hasVisualBuffs = false
if (avatar.stats != null && avatar.stats.getBuffs() != null) {
val buffs = avatar.stats.getBuffs()
if (buffs.snowball) {
layerMap[AvatarView.LayerType.VISUAL_BUFF] = "snowman"
hasVisualBuffs = true
}
if (buffs.seafoam) {
layerMap[AvatarView.LayerType.VISUAL_BUFF] = "seafoam_star"
hasVisualBuffs = true
}
if (buffs.shinySeed) {
layerMap[AvatarView.LayerType.VISUAL_BUFF] = "avatar_floral_" + avatar.stats.getHabitClass()
hasVisualBuffs = true
}
if (buffs.spookySparkles) {
layerMap[AvatarView.LayerType.VISUAL_BUFF] = "ghost"
hasVisualBuffs = true
}
}
if (!hasVisualBuffs) {
if (!TextUtils.isEmpty(prefs.chair)) {
layerMap[AvatarView.LayerType.CHAIR] = prefs.chair
}
if (outfit != null) {
if (!TextUtils.isEmpty(outfit.back) && "back_base_0" != outfit.back) {
layerMap[AvatarView.LayerType.BACK] = outfit.back
}
if (outfit.isAvailable(outfit.armor)) {
layerMap[AvatarView.LayerType.ARMOR] = prefs.size + "_" + outfit.armor
}
if (outfit.isAvailable(outfit.body)) {
layerMap[AvatarView.LayerType.BODY] = outfit.body
}
if (outfit.isAvailable(outfit.eyeWear)) {
layerMap[AvatarView.LayerType.EYEWEAR] = outfit.eyeWear
}
if (outfit.isAvailable(outfit.head)) {
layerMap[AvatarView.LayerType.HEAD] = outfit.head
}
if (outfit.isAvailable(outfit.headAccessory)) {
layerMap[AvatarView.LayerType.HEAD_ACCESSORY] = outfit.headAccessory
}
if (outfit.isAvailable(outfit.shield)) {
layerMap[AvatarView.LayerType.SHIELD] = outfit.shield
}
if (outfit.isAvailable(outfit.weapon)) {
layerMap[AvatarView.LayerType.WEAPON] = outfit.weapon
}
}
layerMap[AvatarView.LayerType.SKIN] = "skin_" + prefs.skin + if (prefs.sleep) "_sleep" else ""
layerMap[AvatarView.LayerType.SHIRT] = prefs.size + "_shirt_" + prefs.shirt
layerMap[AvatarView.LayerType.HEAD_0] = "head_0"
val hair = prefs.hair
if (hair != null) {
val hairColor = hair.color
if (hair.isAvailable(hair.base)) {
layerMap[AvatarView.LayerType.HAIR_BASE] = "hair_base_" + hair.base + "_" + hairColor
}
if (hair.isAvailable(hair.bangs)) {
layerMap[AvatarView.LayerType.HAIR_BANGS] = "hair_bangs_" + hair.bangs + "_" + hairColor
}
if (hair.isAvailable(hair.mustache)) {
layerMap[AvatarView.LayerType.HAIR_MUSTACHE] = "hair_mustache_" + hair.mustache + "_" + hairColor
}
if (hair.isAvailable(hair.beard)) {
layerMap[AvatarView.LayerType.HAIR_BEARD] = "hair_beard_" + hair.beard + "_" + hairColor
}
if (hair.isAvailable(hair.flower)) {
layerMap[AvatarView.LayerType.HAIR_FLOWER] = "hair_flower_" + hair.flower
}
}
} else {
val hair = prefs.hair
// Show flower all the time!
if (hair != null && hair.isAvailable(hair.flower)) {
layerMap[AvatarView.LayerType.HAIR_FLOWER] = "hair_flower_" + hair.flower
}
}
return layerMap
}
private fun getLayerBounds(layerType: LayerType, layerName: String, layerImageInfo: ImageInfo): Rect {
var offset: PointF? = null
val bounds = Rect(0, 0, layerImageInfo.width, layerImageInfo.height)
val boundsF = RectF(bounds)
// lookup layer specific offset
when (layerName) {
"weapon_special_critical" -> offset = if (showMount || showPet) {
// full hero box
when {
hasMount -> PointF(13.0f, 12.0f)
hasPet -> PointF(13.0f, 24.5f + 12.0f)
else -> PointF(13.0f, 28.0f + 12.0f)
}
} else if (showBackground) {
// compact hero box
PointF(-12.0f, 18.0f + 12.0f)
} else {
// hero only box
PointF(-12.0f, 12.0f)
}
}
// otherwise lookup default layer type based offset
if (offset == null) {
when (layerType) {
AvatarView.LayerType.BACKGROUND -> if (!(showMount || showPet)) {
offset = PointF(-25.0f, 0.0f) // compact hero box
}
AvatarView.LayerType.MOUNT_BODY, AvatarView.LayerType.MOUNT_HEAD -> offset = PointF(25.0f, 18.0f) // full hero box
AvatarView.LayerType.CHAIR, AvatarView.LayerType.BACK, AvatarView.LayerType.SKIN, AvatarView.LayerType.SHIRT, AvatarView.LayerType.ARMOR, AvatarView.LayerType.BODY, AvatarView.LayerType.HEAD_0, AvatarView.LayerType.HAIR_BASE, AvatarView.LayerType.HAIR_BANGS, AvatarView.LayerType.HAIR_MUSTACHE, AvatarView.LayerType.HAIR_BEARD, AvatarView.LayerType.EYEWEAR, AvatarView.LayerType.VISUAL_BUFF, AvatarView.LayerType.HEAD, AvatarView.LayerType.HEAD_ACCESSORY, AvatarView.LayerType.HAIR_FLOWER, AvatarView.LayerType.SHIELD, AvatarView.LayerType.WEAPON, AvatarView.LayerType.ZZZ -> if (showMount || showPet) {
// full hero box
offset = when {
hasMount -> PointF(25.0f, 0f)
hasPet -> PointF(25.0f, 24.5f)
else -> PointF(25.0f, 28.0f)
}
} else if (showBackground) {
// compact hero box
offset = PointF(0.0f, 18.0f)
}
AvatarView.LayerType.PET -> offset = PointF(0f, (FULL_HERO_RECT.height() - layerImageInfo.height).toFloat())
}
}
if (offset != null) {
when (layerName) {
"head_special_0" -> offset = PointF(offset.x, offset.y+3)
"head_special_1" -> offset = PointF(offset.x, offset.y+3)
}
val translateMatrix = Matrix()
translateMatrix.setTranslate(offset.x, offset.y)
translateMatrix.mapRect(boundsF)
}
// resize bounds to fit and keep original aspect ratio
avatarMatrix.mapRect(boundsF)
boundsF.round(bounds)
return bounds
}
private fun getFileName(imageName: String): String {
val name = if (FILENAME_MAP.containsKey(imageName)) {
FILENAME_MAP[imageName]
} else {
imageName
}
return name + if (FILEFORMAT_MAP.containsKey(imageName)) {
"." + FILEFORMAT_MAP[imageName]
} else {
".png"
}
}
private fun onLayerComplete() {
if (numberLayersInProcess.decrementAndGet() == 0) {
avatarImageConsumer?.accept(avatarImage)
}
}
fun onAvatarImageReady(consumer: Consumer<Bitmap?>) {
avatarImageConsumer = consumer
if (multiDraweeHolder.size() > 0 && numberLayersInProcess.get() == 0) {
avatarImageConsumer?.accept(avatarImage)
} else {
initAvatarRectMatrix()
showLayers(layerMap)
}
}
fun setAvatar(avatar: Avatar) {
val oldUser = this.avatar
this.avatar = avatar
if (oldUser != null) {
val newLayerMap = getLayerMap(avatar, false)
val equals = currentLayers != null && currentLayers == newLayerMap
if (!equals) {
multiDraweeHolder.clear()
numberLayersInProcess.set(0)
}
}
invalidate()
}
private fun initAvatarRectMatrix() {
if (avatarRectF == null) {
val srcRect = originalRect
if (isOrphan) {
avatarRectF = RectF(srcRect)
// change scale to not be 1:1
// a quick fix as fresco AnimatedDrawable/ScaleTypeDrawable
// will not translate matrix properly
avatarMatrix.setScale(1.2f, 1.2f)
avatarMatrix.mapRect(avatarRectF)
} 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)
avatarRectF = RectF(0f, 0f, width.toFloat(), height.toFloat())
avatarMatrix.setRectToRect(RectF(srcRect), avatarRectF, Matrix.ScaleToFit.START) // TODO support other ScaleToFit
avatarRectF = RectF(srcRect)
avatarMatrix.mapRect(avatarRectF)
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
initAvatarRectMatrix()
// draw only when user is set
if (avatar == null || !avatar!!.isValid) return
// request image layers if not yet processed
if (multiDraweeHolder.size() == 0) {
showLayers(layerMap)
}
// manually call onAttach/onDetach if view is without parent as they will never be called otherwise
if (isOrphan) multiDraweeHolder.onAttach()
multiDraweeHolder.draw(canvas)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
multiDraweeHolder.onDetach()
}
override fun onStartTemporaryDetach() {
super.onStartTemporaryDetach()
multiDraweeHolder.onDetach()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
multiDraweeHolder.onAttach()
}
override fun onFinishTemporaryDetach() {
super.onFinishTemporaryDetach()
multiDraweeHolder.onAttach()
}
override fun verifyDrawable(who: Drawable): Boolean {
return multiDraweeHolder.verifyDrawable(who) || super.verifyDrawable(who)
}
override fun invalidateDrawable(drawable: Drawable) {
invalidate()
if (avatarCanvas != null) draw(avatarCanvas)
}
enum class LayerType(internal val order: Int) {
BACKGROUND(0),
MOUNT_BODY(1),
CHAIR(2),
BACK(3),
SKIN(4),
SHIRT(5),
ARMOR(6),
BODY(7),
HEAD_0(8),
HAIR_BASE(9),
HAIR_BANGS(10),
HAIR_MUSTACHE(11),
HAIR_BEARD(12),
EYEWEAR(13),
VISUAL_BUFF(14),
HEAD(15),
HEAD_ACCESSORY(16),
HAIR_FLOWER(17),
SHIELD(18),
WEAPON(19),
MOUNT_HEAD(20),
ZZZ(21),
PET(22)
}
interface Consumer<in T> {
fun accept(t: T)
}
companion object {
const val IMAGE_URI_ROOT = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"
private const val TAG = "AvatarView"
val FILEFORMAT_MAP: Map<String, String>
val FILENAME_MAP: Map<String, String>
private val FULL_HERO_RECT = Rect(0, 0, 140, 147)
private val COMPACT_HERO_RECT = Rect(0, 0, 114, 114)
private val HERO_ONLY_RECT = Rect(0, 0, 90, 90)
init {
val tempMap = HashMap<String, String>()
tempMap["head_special_1"] = "gif"
tempMap["broad_armor_special_1"] = "gif"
tempMap["slim_armor_special_1"] = "gif"
tempMap["head_special_0"] = "gif"
tempMap["slim_armor_special_0"] = "gif"
tempMap["broad_armor_special_0"] = "gif"
tempMap["weapon_special_critical"] = "gif"
tempMap["weapon_special_0"] = "gif"
tempMap["shield_special_0"] = "gif"
tempMap["Pet-Wolf-Cerberus"] = "gif"
FILEFORMAT_MAP = Collections.unmodifiableMap(tempMap)
val tempNameMap = HashMap<String, String>()
tempNameMap["head_special_1"] = "ContributorOnly-Equip-CrystalHelmet"
tempNameMap["armor_special_1"] = "ContributorOnly-Equip-CrystalArmor"
tempNameMap["weapon_special_critical"] = "weapon_special_critical"
tempNameMap["Pet-Wolf-Cerberus"] = "Pet-Wolf-Cerberus"
FILENAME_MAP = Collections.unmodifiableMap(tempNameMap)
}
}
}

View file

@ -116,6 +116,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
@ -426,8 +427,12 @@ public class MainActivity extends BaseActivity implements TutorialView.OnTutoria
Preferences preferences = user.getPreferences();
if (preferences != null) {
apiClient.setLanguageCode(preferences.getLanguage());
soundManager.setSoundTheme(preferences.getSound());
if (preferences.getLanguage() != null) {
apiClient.setLanguageCode(preferences.getLanguage());
}
if (preferences.getSound() != null) {
soundManager.setSoundTheme(preferences.getSound());
}
}
runOnUiThread(() -> {
updateHeader();
@ -594,7 +599,7 @@ public class MainActivity extends BaseActivity implements TutorialView.OnTutoria
return;
}
if (rewardKey.equals("potion")) {
if (Objects.equals(rewardKey, "potion")) {
int currentHp = user.getStats().getHp().intValue();
int maxHp = user.getStats().getMaxHealth();

View file

@ -1,114 +0,0 @@
package com.habitrpg.android.habitica.ui.adapter.inventory;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.habitrpg.android.habitica.R;
import com.habitrpg.android.habitica.models.inventory.Equipment;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.realm.OrderedRealmCollection;
import io.realm.RealmRecyclerViewAdapter;
import rx.Observable;
import rx.subjects.PublishSubject;
public class EquipmentRecyclerViewAdapter extends RealmRecyclerViewAdapter<Equipment, EquipmentRecyclerViewAdapter.GearViewHolder> {
public String equippedGear;
public Boolean isCostume;
public String type;
private PublishSubject<String> equipEvents = PublishSubject.create();
public EquipmentRecyclerViewAdapter(@Nullable OrderedRealmCollection<Equipment> data, boolean autoUpdate) {
super(data, autoUpdate);
}
@Override
public GearViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.gear_list_item, parent, false);
return new GearViewHolder(view);
}
@Override
public void onBindViewHolder(GearViewHolder holder, int position) {
if (getData() != null) {
holder.bind(getData().get(position));
}
}
public Observable<String> getEquipEvents() {
return equipEvents.asObservable();
}
class GearViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
@BindView(R.id.gear_container)
View gearContainer;
@BindView(R.id.gear_text)
TextView gearNameTextView;
@BindView(R.id.gear_notes)
TextView gearNotesTextView;
@BindView(R.id.gear_image)
SimpleDraweeView imageView;
@BindView(R.id.equippedIndicator)
View equippedIndicator;
Equipment gear;
Context context;
public GearViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
context = itemView.getContext();
itemView.setOnClickListener(this);
}
public void bind(Equipment gear) {
this.gear = gear;
this.gearNameTextView.setText(this.gear.getText());
this.gearNotesTextView.setText(this.gear.getNotes());
if (gear.getKey().equals(equippedGear)) {
this.equippedIndicator.setVisibility(View.VISIBLE);
this.gearContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.brand_700));
} else {
this.equippedIndicator.setVisibility(View.GONE);
this.gearContainer.setBackgroundResource(R.drawable.selection_highlight);
}
String imageUrl = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_" + gear.getKey() + ".png";
imageView.setImageURI(Uri.parse(imageUrl));
}
@Override
public void onClick(View v) {
equipEvents.onNext(this.gear.getKey());
if (this.gear.getKey().equals(equippedGear)) {
equippedGear = type + "_base_0";
} else {
equippedGear = this.gear.getKey();
}
notifyDataSetChanged();
}
}
}

View file

@ -0,0 +1,86 @@
package com.habitrpg.android.habitica.ui.adapter.inventory
import android.content.Context
import android.net.Uri
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.facebook.drawee.view.SimpleDraweeView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.inventory.Equipment
import butterknife.BindView
import butterknife.ButterKnife
import com.habitrpg.android.habitica.extensions.bindView
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import io.realm.OrderedRealmCollection
import io.realm.RealmRecyclerViewAdapter
import rx.Observable
import rx.subjects.PublishSubject
class EquipmentRecyclerViewAdapter(data: OrderedRealmCollection<Equipment>?, autoUpdate: Boolean) : RealmRecyclerViewAdapter<Equipment, EquipmentRecyclerViewAdapter.GearViewHolder>(data, autoUpdate) {
var equippedGear: String? = null
var isCostume: Boolean? = null
var type: String? = null
val equipEvents: PublishSubject<String> = PublishSubject.create<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GearViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.gear_list_item, parent, false)
return GearViewHolder(view)
}
override fun onBindViewHolder(holder: GearViewHolder, position: Int) {
if (data != null) {
holder.bind(data!![position])
}
}
inner class GearViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val gearContainer: View by bindView(itemView, R.id.gear_container)
private val gearNameTextView: TextView by bindView(itemView, R.id.gear_text)
private val gearNotesTextView: TextView by bindView(itemView, R.id.gear_notes)
private val imageView: SimpleDraweeView by bindView(itemView, R.id.gear_image)
private val equippedIndicator: View by bindView(itemView, R.id.equippedIndicator)
var gear: Equipment? = null
var context: Context = itemView.context
init {
context = itemView.context
itemView.setOnClickListener {
val key = gear?.key
if (key != null) {
equipEvents.onNext(key)
equippedGear = if (key == equippedGear) {
type + "_base_0"
} else {
key
}
notifyDataSetChanged()
}
}
}
fun bind(gear: Equipment) {
this.gear = gear
this.gearNameTextView.text = this.gear?.text
this.gearNotesTextView.text = this.gear?.notes
if (gear.key == equippedGear) {
this.equippedIndicator.visibility = View.VISIBLE
this.gearContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.brand_700))
} else {
this.equippedIndicator.visibility = View.GONE
this.gearContainer.setBackgroundResource(R.drawable.selection_highlight)
}
DataBindingUtils.loadImage(imageView, "shop_"+gear.key)
}
}
}

View file

@ -245,7 +245,6 @@ class ShopRecyclerAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val namePlate: TextView by bindView(itemView, R.id.namePlate)
init {
ButterKnife.bind(this, itemView)
context = itemView.context
descriptionView.movementMethod = LinkMovementMethod.getInstance()
}
@ -278,7 +277,6 @@ class ShopRecyclerAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val subscribeButton: Button? by bindView(itemView, R.id.subscribeButton)
private val textView: TextView? by bindView(itemView, R.id.textView)
init {
ButterKnife.bind(this, view)
subscribeButton?.setOnClickListener { EventBus.getDefault().post(OpenGemPurchaseFragmentCommand()) }
}

View file

@ -19,7 +19,7 @@ class AvatarOverviewFragment : BaseMainFragment(), AdapterView.OnItemSelectedLis
super.onCreate(savedInstanceState)
if (apiClient != null) {
apiClient.content
apiClient.getContent()
.subscribe(Action1 { }, RxErrorHandler.handleEmptyError())
}
}

View file

@ -1,76 +0,0 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.equipment;
import android.os.Bundle;
import android.support.v7.widget.DividerItemDecoration;
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 com.habitrpg.android.habitica.R;
import com.habitrpg.android.habitica.components.AppComponent;
import com.habitrpg.android.habitica.data.InventoryRepository;
import com.habitrpg.android.habitica.helpers.RxErrorHandler;
import com.habitrpg.android.habitica.ui.adapter.inventory.EquipmentRecyclerViewAdapter;
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment;
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
public class EquipmentDetailFragment extends BaseMainFragment {
@Inject
InventoryRepository inventoryRepository;
public String type;
public String equippedGear;
public Boolean isCostume;
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
EquipmentRecyclerViewAdapter adapter;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View v = inflater.inflate(R.layout.fragment_recyclerview, container, false);
setUnbinder(ButterKnife.bind(this, v));
this.adapter = new EquipmentRecyclerViewAdapter(null, true);
this.adapter.equippedGear = this.equippedGear;
this.adapter.isCostume = this.isCostume;
this.adapter.type = this.type;
this.adapter.getEquipEvents()
.flatMap(key -> inventoryRepository.equipGear(user, key, isCostume))
.subscribe(items -> {}, RxErrorHandler.handleEmptyError());
this.recyclerView.setAdapter(this.adapter);
this.recyclerView.setLayoutManager(new LinearLayoutManager(activity));
this.recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));
recyclerView.setItemAnimator(new SafeDefaultItemAnimator());
inventoryRepository.getOwnedEquipment(type).first().subscribe(this.adapter::updateData, RxErrorHandler.handleEmptyError());
return v;
}
@Override
public void onDestroy() {
inventoryRepository.close();
super.onDestroy();
}
@Override
public void injectFragment(AppComponent component) {
component.inject(this);
}
}

View file

@ -0,0 +1,66 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.equipment
import android.os.Bundle
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.ui.adapter.inventory.EquipmentRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import io.realm.RealmResults
import kotlinx.android.synthetic.main.fragment_recyclerview.*
import rx.functions.Action1
import javax.inject.Inject
class EquipmentDetailFragment : BaseMainFragment() {
@Inject
lateinit var inventoryRepository: InventoryRepository
var type: String? = null
var equippedGear: String? = null
var isCostume: Boolean? = null
private var adapter: EquipmentRecyclerViewAdapter = EquipmentRecyclerViewAdapter(null, true)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val v = inflater.inflate(R.layout.fragment_recyclerview, container, false)
this.adapter.equippedGear = this.equippedGear
this.adapter.isCostume = this.isCostume
this.adapter.type = this.type
this.adapter.equipEvents.flatMap<Items> { key -> inventoryRepository.equipGear(user, key, isCostume!!) }
.subscribe(Action1 { }, RxErrorHandler.handleEmptyError())
return v
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.adapter = this.adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.addItemDecoration(DividerItemDecoration(getActivity()!!, DividerItemDecoration.VERTICAL))
recyclerView.itemAnimator = SafeDefaultItemAnimator()
inventoryRepository.getOwnedEquipment(type).first().subscribe(Action1<RealmResults<Equipment>> { this.adapter.updateData(it) }, RxErrorHandler.handleEmptyError())
}
override fun onDestroy() {
inventoryRepository.close()
super.onDestroy()
}
override fun injectFragment(component: AppComponent) {
component.inject(this)
}
}

View file

@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import kotlinx.android.synthetic.main.fragment_equipment_overview.*
import rx.functions.Action1
@ -56,7 +57,7 @@ class EquipmentOverviewFragment : BaseMainFragment() {
if (this.nameMapping.isEmpty()) {
compositeSubscription.add(inventoryRepository.ownedEquipment.subscribe(Action1 {
for (gear in it) {
this.nameMapping.put(gear.key ?: "", gear.text)
this.nameMapping[gear.key ?: ""] = gear.text
}
setEquipmentNames()
@ -120,9 +121,7 @@ class EquipmentOverviewFragment : BaseMainFragment() {
fragment.type = type
fragment.isCostume = isCostume
fragment.equippedGear = equipped
if (activity != null) {
activity!!.displayFragment(fragment)
}
activity?.displayFragment(fragment)
}
override fun customTitle(): String {

View file

@ -24,15 +24,23 @@ import com.habitrpg.android.habitica.R
object DataBindingUtils {
fun loadImage(view: SimpleDraweeView?, imageName: String?) {
fun loadImage(view: SimpleDraweeView?, imageName: String) {
loadImage(view, imageName, "png")
}
fun loadImage(view: SimpleDraweeView?, imageName: String?, imageFormat: String = "png") {
if (view != null && imageName != null && view.visibility == View.VISIBLE) {
view.setImageURI("https://habitica-assets.s3.amazonaws.com/mobileApp/images/$imageName.png")
view.setImageURI("https://habitica-assets.s3.amazonaws.com/mobileApp/images/$imageName.$imageFormat")
}
}
fun loadImage(imageName: String, imageResult: (Bitmap) -> Unit) {
loadImage(imageName, "png", imageResult)
}
fun loadImage(imageName: String, imageFormat: String = "png", imageResult: (Bitmap) -> Unit) {
val imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse("https://habitica-assets.s3.amazonaws.com/mobileApp/images/$imageName.png"))
.newBuilderWithSource(Uri.parse("https://habitica-assets.s3.amazonaws.com/mobileApp/images/$imageName.$imageFormat"))
.build()
val imagePipeline = Fresco.getImagePipeline()

View file

@ -1,53 +0,0 @@
package com.habitrpg.android.habitica.ui.helpers;
import java.util.Timer;
import java.util.TimerTask;
// from https://code.google.com/p/tarsius/source/browse/branches/tarsius_0.00_dev2/src/org/tarsius/util/Debounce.java
public abstract class Debounce {
private Timer timer = null;
private long lastHit = 0;
private long debounceDelay = 0;
private long checkDelay = 0;
public Debounce(long debounceDelay, long checkDelay) {
this.debounceDelay = debounceDelay;
this.checkDelay = checkDelay;
}
public abstract void execute();
public void hit() {
lastHit = System.currentTimeMillis();
if (this.timer != null) {
this.timer.cancel();
this.timer = null;
}
this.timer = new Timer("Debounce", true);
this.timer.schedule(new DebounceTask(this), 0, checkDelay);
}
private void checkExecute() {
if ((System.currentTimeMillis() - lastHit) > debounceDelay) {
this.timer.cancel();
this.timer = null;
execute();
}
}
private class DebounceTask extends TimerTask {
private Debounce debounceInstance = null;
public DebounceTask(Debounce debounceInstance) {
this.debounceInstance = debounceInstance;
}
@Override
public void run() {
debounceInstance.checkExecute();
}
}
}

View file

@ -1,105 +0,0 @@
package com.habitrpg.android.habitica.ui.helpers;
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
import com.habitrpg.android.habitica.R;
import net.pherth.android.emoji_library.EmojiEditText;
import net.pherth.android.emoji_library.EmojiPopup;
/**
* @author data5tream
*/
public class EmojiKeyboard {
/**
* Create a Emoji keyboard
*
* @param itemView Must contain views with the following IDs:
* 'emoji.toggle.btn' for the ImageButton that is used to enable/disable the
* emoji keyboard
* 'edit.new.message.text' for the EmojiEditText where the emojis are put into
* @param context The context from the calling Activity
*/
public static void createKeyboard(View itemView, final Context context) {
final ImageButton emojiButton = (ImageButton) itemView.findViewById(R.id.emoji_toggle_btn);
final EmojiEditText emojiEditText = (EmojiEditText) itemView.findViewById(R.id.edit_new_message_text);
final EmojiPopup popup = new EmojiPopup(itemView.getRootView(), context, ContextCompat.getColor(context, R.color.brand));
popup.setSizeForSoftKeyboard();
popup.setOnDismissListener(() -> changeEmojiKeyboardIcon(emojiButton, context, false));
popup.setOnSoftKeyboardOpenCloseListener(new EmojiPopup.OnSoftKeyboardOpenCloseListener() {
@Override
public void onKeyboardOpen(int keyBoardHeight) {
}
@Override
public void onKeyboardClose() {
if (popup.isShowing())
popup.dismiss();
}
});
popup.setOnEmojiconClickedListener(emojicon -> {
if (emojiEditText == null || emojicon == null) {
return;
}
int start = emojiEditText.getSelectionStart();
int end = emojiEditText.getSelectionEnd();
if (start < 0) {
emojiEditText.append(emojicon.getEmoji());
} else {
emojiEditText.getText().replace(Math.min(start, end),
Math.max(start, end), emojicon.getEmoji(), 0,
emojicon.getEmoji().length());
}
});
popup.setOnEmojiconBackspaceClickedListener(v -> {
KeyEvent event = new KeyEvent(
0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL);
emojiEditText.dispatchKeyEvent(event);
});
emojiButton.setOnClickListener(v -> {
if (!popup.isShowing()) {
if (popup.isKeyBoardOpen()) {
popup.showAtBottom();
changeEmojiKeyboardIcon(emojiButton, context, true);
} else {
emojiEditText.setFocusableInTouchMode(true);
emojiEditText.requestFocus();
popup.showAtBottomPending();
final InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(emojiEditText, InputMethodManager.SHOW_IMPLICIT);
changeEmojiKeyboardIcon(emojiButton, context, true);
}
} else {
popup.dismiss();
changeEmojiKeyboardIcon(emojiButton, context, false);
}
});
}
private static void changeEmojiKeyboardIcon(ImageButton view, Context context, Boolean keyboardOpened) {
if (keyboardOpened) {
view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_keyboard_grey600_24dp));
} else {
view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_emoticon_grey600_24dp));
}
}
}

View file

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import kotlinx.android.synthetic.main.item_image_row.view.*
@ -13,14 +14,14 @@ class EquipmentItemRow(context: Context?, attrs: AttributeSet?) : LinearLayout(c
var equipmentIdentifier: String? = null
set(value) {
field = value
val imageName = if (equipmentIdentifier != null && equipmentIdentifier != "") "shop_"+equipmentIdentifier else "head_0"
val imageName = if (equipmentIdentifier?.isNotEmpty() == true && equipmentIdentifier?.endsWith("base_0") == false) "shop_"+equipmentIdentifier else "head_0"
DataBindingUtils.loadImage(imageView, imageName)
}
var customizationIdentifier: String? = null
set(value) {
field = value
val imageName = if (customizationIdentifier != null && customizationIdentifier != "") customizationIdentifier else "head_0"
val imageName = if (customizationIdentifier?.isNotEmpty() == true) customizationIdentifier else "head_0"
DataBindingUtils.loadImage(imageView, imageName)
}

View file

@ -10,6 +10,7 @@ import android.view.View
import android.widget.FrameLayout
import butterknife.ButterKnife
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.backgroundCompat
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import kotlinx.android.synthetic.main.fixvalues_edittext.view.*
@ -28,12 +29,11 @@ class FixValuesEditText(context: Context, attrs: AttributeSet) : FrameLayout(con
val backgroundDrawable = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg)
backgroundDrawable?.setColorFilter(field, PorterDuff.Mode.MULTIPLY)
backgroundDrawable?.alpha = 50
iconBackgroundView.background = backgroundDrawable
iconBackgroundView.backgroundCompat = backgroundDrawable
}
init {
View.inflate(context, R.layout.fixvalues_edittext, this)
ButterKnife.bind(this)
val attributes = context.theme.obtainStyledAttributes(
attrs,