diff --git a/twidere/src/main/java/org/mariotaku/twidere/adapter/StaggeredGridParcelableStatusesAdapter.java b/twidere/src/main/java/org/mariotaku/twidere/adapter/StaggeredGridParcelableStatusesAdapter.java index 42041da1b..e5f38201b 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/adapter/StaggeredGridParcelableStatusesAdapter.java +++ b/twidere/src/main/java/org/mariotaku/twidere/adapter/StaggeredGridParcelableStatusesAdapter.java @@ -32,6 +32,7 @@ import com.commonsware.cwac.layouts.AspectLockedFrameLayout; import org.mariotaku.twidere.R; import org.mariotaku.twidere.adapter.iface.IStatusesAdapter; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; import org.mariotaku.twidere.model.ParcelableMedia; import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.util.ParcelableMediaUtils; @@ -91,11 +92,7 @@ public class StaggeredGridParcelableStatusesAdapter extends AbsParcelableStatuse final ParcelableMedia[] media = status.media; if (media == null || media.length < 1) return; final ParcelableMedia firstMedia = media[0]; - if (status.text_plain.codePointCount(0, status.text_plain.length()) == firstMedia.end) { - mediaTextView.setText(status.text_unescaped.substring(0, firstMedia.start)); - } else { - mediaTextView.setText(status.text_unescaped); - } + mediaTextView.setText(status.text_unescaped); aspectRatioSource.setSize(firstMedia.width, firstMedia.height); mediaImageContainer.setTag(firstMedia); mediaImageContainer.requestLayout(); @@ -153,6 +150,11 @@ public class StaggeredGridParcelableStatusesAdapter extends AbsParcelableStatuse } + @Override + public void playLikeAnimation(LikeAnimationDrawable.OnLikedListener listener) { + + } + public void setOnClickListeners() { setStatusClickListener(adapter.getStatusClickListener()); } diff --git a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsActivitiesFragment.java b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsActivitiesFragment.java index 194a37fec..2c8ba852e 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsActivitiesFragment.java +++ b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsActivitiesFragment.java @@ -47,6 +47,7 @@ import org.mariotaku.twidere.adapter.AbsActivitiesAdapter; import org.mariotaku.twidere.adapter.decorator.DividerItemDecoration; import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition; import org.mariotaku.twidere.annotation.ReadPositionTag; +import org.mariotaku.twidere.fragment.support.AbsStatusesFragment.DefaultOnLikedListener; import org.mariotaku.twidere.loader.iface.IExtendedLoader; import org.mariotaku.twidere.model.ParcelableActivity; import org.mariotaku.twidere.model.ParcelableMedia; @@ -155,11 +156,11 @@ public abstract class AbsActivitiesFragment extends AbsContentListRecycler if (recyclerView == null || layoutManager == null) return false; final View focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView, layoutManager.getFocusedChild()); - int position = -1; + int position = RecyclerView.NO_POSITION; if (focusedChild != null && focusedChild.getParent() == recyclerView) { position = recyclerView.getChildLayoutPosition(focusedChild); } - if (position != -1) { + if (position != RecyclerView.NO_POSITION) { final ParcelableActivity activity = getAdapter().getActivity(position); if (activity == null) return false; if (keyCode == KeyEvent.KEYCODE_ENTER) { @@ -188,7 +189,9 @@ public abstract class AbsActivitiesFragment extends AbsContentListRecycler if (status.is_favorite) { twitter.destroyFavoriteAsync(activity.account_id, status.id); } else { - twitter.createFavoriteAsync(activity.account_id, status.id); + final IStatusViewHolder holder = (IStatusViewHolder) + recyclerView.findViewHolderForLayoutPosition(position); + holder.playLikeAnimation(new DefaultOnLikedListener(twitter, status)); } return true; } @@ -362,7 +365,7 @@ public abstract class AbsActivitiesFragment extends AbsContentListRecycler if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { - twitter.createFavoriteAsync(status.account_id, status.id); + holder.playLikeAnimation(new DefaultOnLikedListener(twitter, status)); } break; } diff --git a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsStatusesFragment.java b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsStatusesFragment.java index 84fe9619e..19a0c66e4 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsStatusesFragment.java +++ b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/AbsStatusesFragment.java @@ -47,6 +47,7 @@ import org.mariotaku.twidere.adapter.AbsStatusesAdapter; import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition; import org.mariotaku.twidere.adapter.iface.IStatusesAdapter.StatusAdapterListener; import org.mariotaku.twidere.annotation.ReadPositionTag; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; import org.mariotaku.twidere.loader.iface.IExtendedLoader; import org.mariotaku.twidere.model.ParcelableMedia; import org.mariotaku.twidere.model.ParcelableStatus; @@ -184,7 +185,9 @@ public abstract class AbsStatusesFragment extends AbsContentListRecyclerVi if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { - twitter.createFavoriteAsync(status.account_id, status.id); + final IStatusViewHolder holder = (IStatusViewHolder) + recyclerView.findViewHolderForLayoutPosition(position); + holder.playLikeAnimation(new DefaultOnLikedListener(twitter, status)); } return true; } @@ -351,7 +354,7 @@ public abstract class AbsStatusesFragment extends AbsContentListRecyclerVi if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { - twitter.createFavoriteAsync(status.account_id, status.id); + holder.playLikeAnimation(new DefaultOnLikedListener(twitter, status)); } break; } @@ -547,6 +550,23 @@ public abstract class AbsStatusesFragment extends AbsContentListRecyclerVi return Utils.getReadPositionTagWithAccounts(getReadPositionTag(), getAccountIds()); } + public static final class DefaultOnLikedListener implements LikeAnimationDrawable.OnLikedListener { + private final ParcelableStatus mStatus; + private final AsyncTwitterWrapper mTwitter; + + public DefaultOnLikedListener(final AsyncTwitterWrapper twitter, final ParcelableStatus status) { + mStatus = status; + mTwitter = twitter; + } + + @Override + public boolean onLiked() { + if (mStatus.is_favorite) return false; + mTwitter.createFavoriteAsync(mStatus.account_id, mStatus.id); + return true; + } + } + protected final class StatusesBusCallback { protected StatusesBusCallback() { diff --git a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/StatusFragment.java b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/StatusFragment.java index d4f1e4ab4..49d6fc429 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/fragment/support/StatusFragment.java +++ b/twidere/src/main/java/org/mariotaku/twidere/fragment/support/StatusFragment.java @@ -47,7 +47,10 @@ import android.support.v4.app.FragmentManagerAccessor; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.AsyncTaskLoader; +import android.support.v4.content.ContextCompat; import android.support.v4.content.Loader; +import android.support.v4.view.ActionProvider; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.CardView; import android.support.v7.widget.FixedLinearLayoutManager; @@ -97,6 +100,7 @@ import org.mariotaku.twidere.api.twitter.model.TranslationResult; import org.mariotaku.twidere.constant.IntentConstants; import org.mariotaku.twidere.loader.support.ConversationLoader; import org.mariotaku.twidere.loader.support.ParcelableStatusLoader; +import org.mariotaku.twidere.menu.support.FavoriteItemProvider; import org.mariotaku.twidere.model.ParcelableActivity; import org.mariotaku.twidere.model.ParcelableActivityCursorIndices; import org.mariotaku.twidere.model.ParcelableActivityValuesCreator; @@ -1231,7 +1235,22 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac final StatusFragment fragment = adapter.getFragment(); final FragmentActivity activity = fragment.getActivity(); final MenuInflater inflater = activity.getMenuInflater(); - inflater.inflate(R.menu.menu_detail_status, menuBar.getMenu()); + final Menu menu = menuBar.getMenu(); + inflater.inflate(R.menu.menu_detail_status, menu); + final MenuItem favoriteItem = menu.findItem(R.id.favorite); + final ActionProvider provider = MenuItemCompat.getActionProvider(favoriteItem); + if (provider instanceof FavoriteItemProvider) { + final int defaultColor = ThemeUtils.getActionIconColor(activity); + final FavoriteItemProvider itemProvider = (FavoriteItemProvider) provider; + itemProvider.setDefaultColor(defaultColor); + final int favoriteHighlight = ContextCompat.getColor(activity, R.color.highlight_favorite); + final int likeHighlight = ContextCompat.getColor(activity, R.color.highlight_like); + final boolean useStar = adapter.shouldUseStarsForLikes(); + itemProvider.setActivatedColor(useStar ? favoriteHighlight : likeHighlight); + itemProvider.setIcon(useStar ? R.drawable.ic_action_star : R.drawable.ic_action_heart); + itemProvider.setUseStar(useStar); + itemProvider.init(menuBar, favoriteItem); + } ThemeUtils.wrapMenuIcon(menuBar, MENU_GROUP_STATUS_SHARE); mediaPreviewLoad.setOnClickListener(this); profileContainer.setOnClickListener(this); diff --git a/twidere/src/main/java/org/mariotaku/twidere/graphic/LikeAnimationDrawable.java b/twidere/src/main/java/org/mariotaku/twidere/graphic/LikeAnimationDrawable.java new file mode 100644 index 000000000..be9802e08 --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/graphic/LikeAnimationDrawable.java @@ -0,0 +1,804 @@ +package org.mariotaku.twidere.graphic; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.support.annotation.IntDef; +import android.support.v4.content.ContextCompat; +import android.util.Property; +import android.view.Gravity; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import org.mariotaku.twidere.graphic.iface.DoNotWrapDrawable; + +import java.lang.ref.WeakReference; + +/** + * Created by mariotaku on 15/11/4. + */ +public class LikeAnimationDrawable extends LayerDrawable implements Animatable, DoNotWrapDrawable { + + private static final Property ICON_SCALE = new Property(Float.class, "icon_scale") { + @Override + public void set(IconLayer object, Float value) { + object.setScale(value); + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public Float get(IconLayer object) { + return object.getScale(); + } + }; + private static final Property LAYER_PROGRESS = new Property(Float.class, "layer_progress") { + @Override + public void set(Layer object, Float value) { + object.setProgress(value); + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public Float get(Layer object) { + return object.getProgress(); + } + }; + private final int mDefaultColor, mLikeColor; + @Style + private final int mStyle; + private long mDuration = 500; + + private AnimatorSet mCurrentAnimator; + private WeakReference mListenerRef; + + public LikeAnimationDrawable(final Context context, final int likeIcon, final int defaultColor, + final int likeColor, @Style final int style) { + super(createLayers(context, likeIcon, defaultColor, style)); + mDefaultColor = defaultColor; + mLikeColor = likeColor; + mStyle = style; + } + + @Override + public void start() { + if (mCurrentAnimator != null) return; + + final AnimatorSet animatorSet = new AnimatorSet(); + + final AbsLayer particleLayer = getParticleShineLayer(); + final AbsLayer circleLayer = getCircleLayer(); + final IconLayer iconLayer = getIconLayer(); + + switch (mStyle) { + case Style.LIKE: { + setupLikeAnimation(animatorSet, particleLayer, circleLayer, iconLayer); + break; + } + case Style.FAVORITE: { + setupFavoriteAnimation(animatorSet, particleLayer, circleLayer, iconLayer); + break; + } + } + + + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + resetState(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentAnimator = null; + } + + @Override + public void onAnimationEnd(Animator animation) { + mCurrentAnimator = null; + if (mListenerRef == null) return; + final OnLikedListener listener = mListenerRef.get(); + if (listener == null) return; + if (!listener.onLiked()) { + resetState(); + } + } + + private void resetState() { + iconLayer.setColorFilter(mDefaultColor, PorterDuff.Mode.SRC_ATOP); + particleLayer.setProgress(-1); + } + }); + animatorSet.start(); + mCurrentAnimator = animatorSet; + } + + + private void setupFavoriteAnimation(final AnimatorSet animatorSet, final Layer particleLayer, + final Layer circleLayer, final IconLayer iconLayer) { + setupLikeAnimation(animatorSet, particleLayer, circleLayer, iconLayer); + } + + private void setupLikeAnimation(final AnimatorSet animatorSet, final Layer particleLayer, + final Layer circleLayer, final IconLayer iconLayer) { + final long scaleDownDuration = Math.round(1f / 24f * mDuration); + final long ovalExpandDuration = Math.round(4f / 24f * mDuration); + final long iconExpandOffset = Math.round(6f / 24f * mDuration); + final long iconExpandDuration = Math.round(8f / 24f * mDuration); + final long iconNormalDuration = Math.round(4f / 24f * mDuration); + final long particleExpandDuration = Math.round(12f / 24f * mDuration); + final long circleExplodeDuration = Math.round(5f / 24f * mDuration); + + final ObjectAnimator iconScaleDown = ObjectAnimator.ofFloat(iconLayer, ICON_SCALE, 1, 0); + iconScaleDown.setDuration(scaleDownDuration); + iconScaleDown.setInterpolator(new AccelerateInterpolator(2)); + iconScaleDown.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + iconLayer.setColorFilter(mDefaultColor, PorterDuff.Mode.SRC_ATOP); + } + + }); + + final ObjectAnimator ovalExpand = ObjectAnimator.ofFloat(circleLayer, LAYER_PROGRESS, 0, 0.5f); + ovalExpand.setDuration(ovalExpandDuration); + + + final ObjectAnimator iconExpand = ObjectAnimator.ofFloat(iconLayer, ICON_SCALE, 0, 1.25f); + iconExpand.setDuration(iconExpandDuration); + iconExpand.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + iconLayer.setColorFilter(mLikeColor, PorterDuff.Mode.SRC_ATOP); + } + + }); + + final ObjectAnimator particleExplode = ObjectAnimator.ofFloat(particleLayer, LAYER_PROGRESS, 0, 0.5f); + particleExplode.setDuration(iconExpandDuration); + + final ObjectAnimator iconNormal = ObjectAnimator.ofFloat(iconLayer, ICON_SCALE, 1.25f, 1); + iconNormal.setDuration(iconNormalDuration); + final ObjectAnimator circleExplode = ObjectAnimator.ofFloat(circleLayer, LAYER_PROGRESS, 0.5f, 0.95f, 0.95f, 1); + circleExplode.setDuration(circleExplodeDuration); + circleExplode.setInterpolator(new DecelerateInterpolator()); + + + final ObjectAnimator particleFade = ObjectAnimator.ofFloat(particleLayer, LAYER_PROGRESS, 0.5f, 1); + particleFade.setDuration(particleExpandDuration); + + + animatorSet.play(iconScaleDown); + animatorSet.play(ovalExpand).after(iconScaleDown); + animatorSet.play(iconExpand).after(iconExpandOffset); + animatorSet.play(particleExplode).after(iconExpandOffset); + animatorSet.play(circleExplode).after(iconExpandOffset); + + animatorSet.play(iconNormal).after(iconExpand); + animatorSet.play(particleFade).after(iconExpand); + } + + private IconLayer getIconLayer() { + return (IconLayer) getDrawable(2); + } + + private AbsLayer getCircleLayer() { + return (AbsLayer) getDrawable(0); + } + + private AbsLayer getParticleShineLayer() { + return (AbsLayer) getDrawable(1); + } + + @Override + public void stop() { + if (mCurrentAnimator == null) return; + mCurrentAnimator.cancel(); + } + + + @Override + public boolean isRunning() { + return mCurrentAnimator != null && mCurrentAnimator.isRunning(); + } + + public long getDuration() { + return mDuration; + } + + public void setDuration(long duration) { + mDuration = duration; + } + + public void setOnLikedListener(OnLikedListener listener) { + mListenerRef = new WeakReference<>(listener); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + getIconLayer().setColorFilter(colorFilter); + } + + private static Drawable[] createLayers(final Context context, final int likeIcon, int defaultColor, int style) { + final IconLayer iconDrawable = new IconLayer(ContextCompat.getDrawable(context, likeIcon)); + iconDrawable.setColorFilter(defaultColor, PorterDuff.Mode.SRC_ATOP); + final AbsLayer particleLayer; + final Palette palette; + switch (style) { + case Style.FAVORITE: { + palette = new FavoritePalette(); + particleLayer = new ShineLayer(iconDrawable.getIntrinsicWidth(), + iconDrawable.getIntrinsicHeight(), palette); + break; + } + case Style.LIKE: { + palette = new LikePalette(); + particleLayer = new ParticleLayer(iconDrawable.getIntrinsicWidth(), + iconDrawable.getIntrinsicHeight(), palette); + break; + } + default: { + throw new IllegalArgumentException(); + } + } + particleLayer.setProgress(-1); + final Drawable circleLayer = new CircleLayer(iconDrawable.getIntrinsicWidth(), iconDrawable.getIntrinsicHeight(), palette); + return new Drawable[]{circleLayer, particleLayer, iconDrawable}; + } + + private interface Layer { + + float getProgress(); + + void setProgress(float progress); + } + + public interface OnLikedListener { + boolean onLiked(); + } + + public interface Palette { + int getParticleColor(int count, int index, float progress); + + int getCircleColor(float progress); + } + + @IntDef({Style.LIKE, Style.FAVORITE}) + public @interface Style { + int LIKE = 1; + int FAVORITE = 2; + } + + private static class ShineLayer extends AbsLayer { + + private static final int PARTICLES_PIVOTS_COUNT = 5; + + private final Paint mPaint; + private int mFullRadius; + private float mLineWidth; + + public ShineLayer(final int intrinsicWidth, final int intrinsicHeight, final Palette palette) { + super(intrinsicWidth, intrinsicHeight, palette); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStrokeCap(Paint.Cap.ROUND); + setProgress(-1); + } + + @Override + protected ConstantState createConstantState(final int intrinsicWidth, + final int intrinsicHeight, + final Palette palette) { + return new AbsLayerState() { + @Override + public Drawable newDrawable() { + return new ShineLayer(intrinsicWidth, intrinsicHeight, palette); + } + }; + } + + private void calculateLineStartEnd(float[] startEnd, float progress) { + + } + + @Override + public void draw(Canvas canvas) { + final float progress = getProgress(); + if (progress < 0) return; + final int particleColor = palette.getParticleColor(0, 0, progress); + final Rect bounds = getBounds(); + mPaint.setColor(particleColor); + mPaint.setStrokeWidth(mLineWidth); + final float[] startEnd = new float[2]; + mPaint.setAlpha(0xFF); + if (progress < 0.25f) { + calcPhase1(startEnd, progress); + } else if (progress < 0.5f) { + calcPhase2(startEnd, progress); + } else if (progress < 0.75f) { + calcPhase3(startEnd, progress); + } else { + calcPhase4(startEnd, progress); + mPaint.setAlpha(Math.round(0xFF * (1 - (progress - 0.75f) * 4))); + } + + for (int i = 0; i < PARTICLES_PIVOTS_COUNT; i++) { + final double degree = 360.0 / PARTICLES_PIVOTS_COUNT * i; + final double mainParticleAngle = Math.toRadians(degree + 18); + final float startX = (float) (bounds.centerX() + startEnd[0] * Math.cos(mainParticleAngle)); + final float startY = (float) (bounds.centerY() + startEnd[0] * Math.sin(mainParticleAngle)); + final float stopX = (float) (bounds.centerX() + startEnd[1] * Math.cos(mainParticleAngle)); + final float stopY = (float) (bounds.centerY() + startEnd[1] * Math.sin(mainParticleAngle)); + if (startEnd[1] - startEnd[0] <= 0) { + canvas.drawPoint(startX, startY, mPaint); + } else { + canvas.drawLine(startX, startY, stopX, stopY, mPaint); + } + } + } + + private void calcPhase4(float[] startEnd, float progress) { + calcPhase3(startEnd, 0.75f); + } + + private void calcPhase3(float[] startEnd, float progress) { + calcPhase2(startEnd, 0.5f); + final float length = (startEnd[1] - startEnd[0]) * (1 - (progress - 0.5f) * 4); + startEnd[0] = startEnd[1] - length; + } + + private void calcPhase2(float[] startEnd, float progress) { + calcPhase1(startEnd, 0.25f); + final float length = startEnd[1] - startEnd[0]; + final float initialStart = startEnd[0]; + startEnd[0] = initialStart + mFullRadius / 3 * (progress - 0.25f) * 4; + startEnd[1] = startEnd[0] + length; + } + + private void calcPhase1(float[] startEnd, float progress) { + // Start point: 1/4 of icon radius + startEnd[0] = mFullRadius / 3; + startEnd[1] = startEnd[0] + (mFullRadius / 4 * progress * 4); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mFullRadius = Math.min(bounds.width(), bounds.height()) / 2; + mLineWidth = mFullRadius / 10f; + } + + } + + private static class ParticleLayer extends AbsLayer { + + private static final int PARTICLES_PIVOTS_COUNT = 7; + private final Paint mPaint; + private float mFullRadius; + private float mParticleSize; + + public ParticleLayer(final int intrinsicWidth, final int intrinsicHeight, + final Palette palette) { + super(intrinsicWidth, intrinsicHeight, palette); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStrokeCap(Paint.Cap.ROUND); + setProgress(-1); + } + + @Override + protected ConstantState createConstantState(final int intrinsicWidth, + final int intrinsicHeight, + final Palette palette) { + return new AbsLayerState() { + @Override + public Drawable newDrawable() { + return new ParticleLayer(intrinsicWidth, intrinsicHeight, palette); + } + }; + } + + @Override + public void draw(final Canvas canvas) { + final float progress = getProgress(); + if (progress < 0) return; + final Rect bounds = getBounds(); + final float expandSpinProgress = Math.min(0.5f, progress); + final float currentRadius = mFullRadius + (mFullRadius * expandSpinProgress); + final float distance = mParticleSize + (mParticleSize * progress); + final float mainStrokeWidth, subStrokeWidth; + if (progress < 0.5) { + // Scale factor: [1, 0.5) + mainStrokeWidth = mParticleSize * (1 - progress); + // Scale factor: [1, 1.25) + subStrokeWidth = mParticleSize * (1 + progress / 2); + } else { + mainStrokeWidth = mParticleSize * (1 - progress); + subStrokeWidth = mParticleSize * 1.25f * (1 - (progress - 0.5f) * 2); + } + + for (int i = 0; i < PARTICLES_PIVOTS_COUNT; i++) { + final double degree = 360.0 / PARTICLES_PIVOTS_COUNT * i; + final int color = palette.getParticleColor(PARTICLES_PIVOTS_COUNT, i, progress); + + final double mainParticleAngle = Math.toRadians(degree - 115); + final float mainParticleX = (float) (bounds.centerX() + currentRadius * Math.cos(mainParticleAngle)); + final float mainParticleY = (float) (bounds.centerY() + currentRadius * Math.sin(mainParticleAngle)); + + mPaint.setColor(color); + mPaint.setStrokeWidth(mainStrokeWidth); + if (mainStrokeWidth > 0) { + canvas.drawPoint(mainParticleX, mainParticleY, mPaint); + } + + + final double particleAngle = Math.toRadians(90.0 * -expandSpinProgress + degree + 15); + final float subParticleX = (float) (mainParticleX + distance * Math.cos(particleAngle)); + final float subParticleY = (float) (mainParticleY + distance * Math.sin(particleAngle)); + mPaint.setAlpha(Math.round(255f * (1 - progress / 2f))); + + mPaint.setStrokeWidth(subStrokeWidth); + if (subStrokeWidth > 0) { + canvas.drawPoint(subParticleX, subParticleY, mPaint); + } + } + + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mFullRadius = Math.min(bounds.width(), bounds.height()) / 2; + mParticleSize = mFullRadius / 4f; + } + + } + + private static class CircleLayer extends AbsLayer { + private final Paint mPaint; + + private int mFullRadius; + + public CircleLayer(final int intrinsicWidth, final int intrinsicHeight, + final Palette palette) { + super(intrinsicWidth, intrinsicHeight, palette); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + @Override + protected ConstantState createConstantState(final int intrinsicWidth, + final int intrinsicHeight, + final Palette palette) { + return new AbsLayerState() { + @Override + public Drawable newDrawable() { + return new CircleLayer(intrinsicWidth, intrinsicHeight, palette); + } + }; + } + + @Override + public void draw(final Canvas canvas) { + final float progress = getProgress(); + final Rect bounds = getBounds(); + final float radius; + if (progress < 0.5f) { + mPaint.setStyle(Paint.Style.FILL); + final float sizeProgress = Math.min(1, progress * 2); + radius = sizeProgress * mFullRadius; + } else { + mPaint.setStyle(Paint.Style.STROKE); + final float innerLeftRatio = 1 - (progress - 0.5f) * 2f; + final float strokeWidth = mFullRadius * innerLeftRatio; + mPaint.setStrokeWidth(strokeWidth); + radius = mFullRadius - strokeWidth / 2; + if (strokeWidth <= 0) return; + } + mPaint.setColor(palette.getCircleColor(progress)); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, mPaint); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mFullRadius = Math.min(bounds.width(), bounds.height()) / 2; + } + + } + + private static abstract class AbsLayer extends Drawable implements Layer { + protected final int intrinsicWidth; + protected final int intrinsicHeight; + protected final Palette palette; + private float mProgress; + private ConstantState mState; + + public AbsLayer(final int intrinsicWidth, final int intrinsicHeight, final Palette palette) { + this.intrinsicWidth = intrinsicWidth; + this.intrinsicHeight = intrinsicHeight; + this.palette = palette; + mState = createConstantState(intrinsicWidth, intrinsicHeight, palette); + } + + protected abstract ConstantState createConstantState(int intrinsicWidth, int intrinsicHeight, final Palette palette); + + @Override + public void setAlpha(final int alpha) { + + } + + @Override + public final float getProgress() { + return mProgress; + } + + @Override + public final void setProgress(float progress) { + mProgress = progress; + invalidateSelf(); + } + + @Override + public void setColorFilter(final ColorFilter colorFilter) { + + } + + @Override + public final int getIntrinsicHeight() { + return intrinsicHeight; + } + + @Override + public final int getIntrinsicWidth() { + return intrinsicWidth; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public ConstantState getConstantState() { + return mState; + } + + static abstract class AbsLayerState extends ConstantState { + + @Override + public int getChangingConfigurations() { + return 0; + } + } + } + + private static class FavoritePalette implements Palette { + + private final ArgbEvaluator evaluator = new ArgbEvaluator(); + + @Override + public int getParticleColor(int count, int index, float progress) { + return (Integer) evaluator.evaluate(progress, 0xFFFF7020, 0xFFFD9050); + } + + @Override + public int getCircleColor(float progress) { + return (Integer) evaluator.evaluate(progress, 0xFFFF9C00, 0xFFFFB024); + } + } + + private static class LikePalette implements Palette { + + private final ArgbEvaluator evaluator = new ArgbEvaluator(); + private final float[] hsv = new float[3]; + + @Override + public int getParticleColor(int count, int index, float progress) { + final double degree = 360.0 / count * index; + hsv[0] = (float) degree; + hsv[1] = 0.4f; + hsv[2] = 1f; + return Color.HSVToColor(hsv); + } + + @Override + public int getCircleColor(float progress) { + return (Integer) evaluator.evaluate(progress, 0xFFDE4689, 0xFFCD8FF5); + } + } + + static class IconLayer extends Drawable implements Callback { + private final Drawable mDrawable; + private final Rect mTmpRect = new Rect(); + private float mScale; + private boolean mMutated; + private ConstantState mState; + + public IconLayer(Drawable drawable) { + if (drawable == null) throw new NullPointerException(); + mState = new ScaleConstantState(drawable); + mDrawable = drawable; + drawable.setCallback(this); + setScale(1); + } + + /** + * Returns the drawable scaled by this ScaleDrawable. + */ + public Drawable getDrawable() { + return mDrawable; + } + + // overrides from Drawable.Callback + @Override + public void invalidateDrawable(Drawable who) { + if (getCallback() != null) { + getCallback().invalidateDrawable(this); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + if (getCallback() != null) { + getCallback().scheduleDrawable(this, what, when); + } + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + if (getCallback() != null) { + getCallback().unscheduleDrawable(this, what); + } + } + + // overrides from Drawable + @Override + public void draw(Canvas canvas) { + if (mScale <= 0) return; + mDrawable.draw(canvas); + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() + | mDrawable.getChangingConfigurations(); + } + + @Override + public boolean getPadding(Rect padding) { + // XXX need to adjust padding! + return mDrawable.getPadding(padding); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + mDrawable.setVisible(visible, restart); + return super.setVisible(visible, restart); + } + + @Override + public void setAlpha(int alpha) { + mDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return mDrawable.getOpacity(); + } + + @Override + public boolean isStateful() { + return mDrawable.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = mDrawable.setState(state); + onBoundsChange(getBounds()); + return changed; + } + + @Override + protected boolean onLevelChange(int level) { + mDrawable.setLevel(level); + onBoundsChange(getBounds()); + invalidateSelf(); + return true; + } + + @Override + protected void onBoundsChange(Rect bounds) { + updateBounds(bounds); + } + + @Override + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mDrawable.mutate(); + mMutated = true; + } + return this; + } + + public float getScale() { + return mScale; + } + + public void setScale(float scale) { + mScale = scale; + updateBounds(getBounds()); + } + + @Override + public ConstantState getConstantState() { + return mState; + } + + + static class ScaleConstantState extends ConstantState { + + private final Drawable mIcon; + + public ScaleConstantState(Drawable icon) { + mIcon = icon; + } + + @Override + public Drawable newDrawable() { + return new IconLayer(mIcon.mutate()); + } + + @Override + public int getChangingConfigurations() { + return mIcon.getChangingConfigurations(); + } + } + + private void updateBounds(Rect bounds) { + final Rect r = mTmpRect; + final int w = Math.round(mDrawable.getIntrinsicWidth() * mScale); + final int h = Math.round(mDrawable.getIntrinsicHeight() * mScale); + Gravity.apply(Gravity.CENTER, w, h, bounds, r); + + if (w > 0 && h > 0) { + mDrawable.setBounds(r.left, r.top, r.right, r.bottom); + } + invalidateSelf(); + } + + } +} diff --git a/twidere/src/main/java/org/mariotaku/twidere/graphic/iface/DoNotWrapDrawable.java b/twidere/src/main/java/org/mariotaku/twidere/graphic/iface/DoNotWrapDrawable.java new file mode 100644 index 000000000..b108e35c8 --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/graphic/iface/DoNotWrapDrawable.java @@ -0,0 +1,7 @@ +package org.mariotaku.twidere.graphic.iface; + +/** + * Created by mariotaku on 16/2/18. + */ +public interface DoNotWrapDrawable { +} diff --git a/twidere/src/main/java/org/mariotaku/twidere/menu/support/FavoriteItemProvider.java b/twidere/src/main/java/org/mariotaku/twidere/menu/support/FavoriteItemProvider.java new file mode 100644 index 000000000..c62b7cd07 --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/menu/support/FavoriteItemProvider.java @@ -0,0 +1,99 @@ +package org.mariotaku.twidere.menu.support; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.v4.view.ActionProvider; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.ActionMenuView; +import android.view.MenuItem; +import android.view.View; + +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable.Style; + +import java.lang.ref.WeakReference; + +/** + * Created by mariotaku on 16/2/18. + */ +public class FavoriteItemProvider extends ActionProvider { + private int mDefaultColor, mActivatedColor; + private boolean mUseStar; + private int mIcon; + + /** + * Creates a new instance. + * + * @param context Context for accessing resources. + */ + public FavoriteItemProvider(Context context) { + super(context); + } + + @Override + public View onCreateActionView() { + return null; + } + + public void setUseStar(boolean useStar) { + mUseStar = useStar; + } + + public void setDefaultColor(int defaultColor) { + mDefaultColor = defaultColor; + } + + public void setActivatedColor(int activatedColor) { + mActivatedColor = activatedColor; + } + + public void invokeItem(MenuItem item, LikeAnimationDrawable.OnLikedListener listener) { + if (MenuItemCompat.getActionProvider(item) != this) throw new IllegalArgumentException(); + final Drawable icon = item.getIcon(); + if (icon instanceof LikeAnimationDrawable) { + ((LikeAnimationDrawable) icon).setOnLikedListener(listener); + ((LikeAnimationDrawable) icon).start(); + } + } + + public void setIcon(int icon) { + mIcon = icon; + } + + public void init(final ActionMenuView menuBar, MenuItem item) { + if (MenuItemCompat.getActionProvider(item) != this) throw new IllegalArgumentException(); + final LikeAnimationDrawable drawable = new LikeAnimationDrawable(getContext(), mIcon, + mDefaultColor, mActivatedColor, mUseStar ? Style.FAVORITE : Style.LIKE); + drawable.setCallback(new ViewCallback(menuBar)); + item.setIcon(drawable); + } + + private static class ViewCallback implements Drawable.Callback { + private final WeakReference mViewRef; + + public ViewCallback(View view) { + mViewRef = new WeakReference<>(view); + } + + @Override + public void invalidateDrawable(Drawable who) { + final View view = mViewRef.get(); + if (view == null) return; + view.invalidate(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + final View view = mViewRef.get(); + if (view == null) return; + view.postDelayed(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + final View view = mViewRef.get(); + if (view == null) return; + view.post(what); + } + } +} \ No newline at end of file diff --git a/twidere/src/main/java/org/mariotaku/twidere/preference/CardPreviewPreference.java b/twidere/src/main/java/org/mariotaku/twidere/preference/CardPreviewPreference.java index 0ed83b878..d3833807d 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/preference/CardPreviewPreference.java +++ b/twidere/src/main/java/org/mariotaku/twidere/preference/CardPreviewPreference.java @@ -24,6 +24,7 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.preference.Preference; import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -31,7 +32,9 @@ import android.view.ViewGroup; import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.R; import org.mariotaku.twidere.adapter.DummyStatusHolderAdapter; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; import org.mariotaku.twidere.view.holder.StatusViewHolder; +import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder; public class CardPreviewPreference extends Preference implements Constants, OnSharedPreferenceChangeListener { @@ -87,6 +90,19 @@ public class CardPreviewPreference extends Preference implements Constants, OnSh mCompactModeChanged = false; mHolder.setupViewOptions(); mHolder.displaySampleStatus(); + mHolder.setStatusClickListener(new IStatusViewHolder.SimpleStatusClickListener() { + @Override + public void onItemActionClick(RecyclerView.ViewHolder holder, int id, int position) { + if (id == R.id.favorite_count) { + ((StatusViewHolder) holder).playLikeAnimation(new LikeAnimationDrawable.OnLikedListener() { + @Override + public boolean onLiked() { + return false; + } + }); + } + } + }); super.onBindView(view); } diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/AsyncTwitterWrapper.java b/twidere/src/main/java/org/mariotaku/twidere/util/AsyncTwitterWrapper.java index f787c205e..d67f64f22 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/AsyncTwitterWrapper.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/AsyncTwitterWrapper.java @@ -945,8 +945,6 @@ public class AsyncTwitterWrapper extends TwitterWrapper { protected void onPreExecute() { super.onPreExecute(); mCreatingFavoriteIds.put(account_id, status_id); - - bus.post(new StatusListChangedEvent()); } @@ -957,14 +955,11 @@ public class AsyncTwitterWrapper extends TwitterWrapper { final ParcelableStatus status = result.getData(); // BEGIN HotMobi - final TweetEvent event = TweetEvent.create(getContext(), status, TimelineType.OTHER); event.setAction(TweetEvent.Action.FAVORITE); HotMobiLogger.getInstance(getContext()).log(account_id, event); - // END HotMobi - bus.post(new FavoriteCreatedEvent(status)); Utils.showOkMessage(mContext, R.string.status_favorited, false); } else { diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/ThemeUtils.java b/twidere/src/main/java/org/mariotaku/twidere/util/ThemeUtils.java index 42ae385be..e4fd9f257 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/ThemeUtils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/ThemeUtils.java @@ -67,6 +67,7 @@ import org.mariotaku.twidere.R; import org.mariotaku.twidere.activity.iface.IThemedActivity; import org.mariotaku.twidere.graphic.ActionBarColorDrawable; import org.mariotaku.twidere.graphic.ActionIconDrawable; +import org.mariotaku.twidere.graphic.iface.DoNotWrapDrawable; import org.mariotaku.twidere.preference.ThemeBackgroundPreference; import org.mariotaku.twidere.util.menu.TwidereMenuInfo; import org.mariotaku.twidere.util.support.ViewSupport; @@ -1005,15 +1006,23 @@ public class ThemeUtils implements Constants { } public static void wrapMenuIcon(ActionMenuView view, int... excludeGroups) { - final Resources resources = view.getResources(); - final int colorDark = resources.getColor(R.color.action_icon_dark); - final int colorLight = resources.getColor(R.color.action_icon_light); + final Context context = view.getContext(); + final int colorDark = ContextCompat.getColor(context, R.color.action_icon_dark); + final int colorLight = ContextCompat.getColor(context, R.color.action_icon_light); wrapMenuIcon(view, colorDark, colorLight, excludeGroups); } + public static int getActionIconColor(Context context) { + final int colorDark = ContextCompat.getColor(context, R.color.action_icon_dark); + final int colorLight = ContextCompat.getColor(context, R.color.action_icon_light); + final int itemBackgroundColor = ThemeUtils.getThemeBackgroundColor(context); + return TwidereColorUtils.getContrastYIQ(itemBackgroundColor, colorDark, colorLight); + } + public static void wrapMenuIcon(ActionMenuView view, int colorDark, int colorLight, int... excludeGroups) { - final int itemBackgroundColor = ThemeUtils.getThemeBackgroundColor(view.getContext()); - final int popupItemBackgroundColor = ThemeUtils.getThemeBackgroundColor(view.getContext(), view.getPopupTheme()); + final Context context = view.getContext(); + final int itemBackgroundColor = ThemeUtils.getThemeBackgroundColor(context); + final int popupItemBackgroundColor = ThemeUtils.getThemeBackgroundColor(context, view.getPopupTheme()); final int itemColor = TwidereColorUtils.getContrastYIQ(itemBackgroundColor, colorDark, colorLight); final int popupItemColor = TwidereColorUtils.getContrastYIQ(popupItemBackgroundColor, colorDark, colorLight); final Menu menu = view.getMenu(); @@ -1034,7 +1043,7 @@ public class ThemeUtils implements Constants { public static void wrapMenuItemIcon(@NonNull MenuItem item, int itemColor, int... excludeGroups) { if (ArrayUtils.contains(excludeGroups, item.getGroupId())) return; final Drawable icon = item.getIcon(); - if (icon == null) return; + if (icon == null || icon instanceof DoNotWrapDrawable) return; if (icon instanceof ActionIconDrawable) { ((ActionIconDrawable) icon).setDefaultColor(itemColor); item.setIcon(icon); diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/ThemedLayoutInflaterFactory.java b/twidere/src/main/java/org/mariotaku/twidere/util/ThemedLayoutInflaterFactory.java index 307d2b5ed..e3ec67365 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/ThemedLayoutInflaterFactory.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/ThemedLayoutInflaterFactory.java @@ -235,12 +235,12 @@ public class ThemedLayoutInflaterFactory implements LayoutInflaterFactory { } else if (tintable instanceof EditText) { tintable.setSupportBackgroundTintList(ColorStateList.valueOf(backgroundTintColor)); } else if (isColorTint) { - final int[][] states = {{android.R.attr.state_selected}, {android.R.attr.state_focused}, - {android.R.attr.state_pressed}, {0}}; - final int[] colors = {accentColor, accentColor, accentColor, noTintColor}; - tintable.setSupportBackgroundTintList(new ColorStateList(states, colors)); +// final int[][] states = {{android.R.attr.state_selected}, {android.R.attr.state_focused}, +// {android.R.attr.state_pressed}, {0}}; +// final int[] colors = {accentColor, accentColor, accentColor, noTintColor}; +// tintable.setSupportBackgroundTintList(new ColorStateList(states, colors)); } else { - tintable.setSupportBackgroundTintList(ColorStateList.valueOf(accentColor)); +// tintable.setSupportBackgroundTintList(ColorStateList.valueOf(accentColor)); } } diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java index 7acaa48dc..1af16887c 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java @@ -138,6 +138,7 @@ import org.mariotaku.twidere.api.twitter.model.Relationship; import org.mariotaku.twidere.api.twitter.model.Status; import org.mariotaku.twidere.api.twitter.model.UserMentionEntity; import org.mariotaku.twidere.fragment.iface.IBaseFragment.SystemWindowsInsetsCallback; +import org.mariotaku.twidere.fragment.support.AbsStatusesFragment.DefaultOnLikedListener; import org.mariotaku.twidere.fragment.support.AccountsManagerFragment; import org.mariotaku.twidere.fragment.support.AddStatusFilterDialogFragment; import org.mariotaku.twidere.fragment.support.DestroyStatusDialogFragment; @@ -174,6 +175,7 @@ import org.mariotaku.twidere.fragment.support.UsersListFragment; import org.mariotaku.twidere.graphic.ActionIconDrawable; import org.mariotaku.twidere.graphic.PaddingDrawable; import org.mariotaku.twidere.menu.SupportStatusShareProvider; +import org.mariotaku.twidere.menu.support.FavoriteItemProvider; import org.mariotaku.twidere.model.AccountPreferences; import org.mariotaku.twidere.model.ParcelableAccount; import org.mariotaku.twidere.model.ParcelableCredentials; @@ -215,7 +217,6 @@ import java.nio.charset.Charset; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; @@ -2251,27 +2252,37 @@ public final class Utils implements Constants { } final MenuItem favorite = menu.findItem(R.id.favorite); if (favorite != null) { - final boolean is_favorite; + final boolean isFavorite; if (twitter.isCreatingFavorite(status.account_id, status.id)) { - is_favorite = true; + isFavorite = true; } else if (twitter.isDestroyingFavorite(status.account_id, status.id)) { - is_favorite = false; + isFavorite = false; } else { - is_favorite = status.is_favorite; + isFavorite = status.is_favorite; } - if (preferences.getBoolean(KEY_I_WANT_MY_STARS_BACK)) { - final Drawable oldIcon = favorite.getIcon(); - if (oldIcon instanceof ActionIconDrawable) { - final Drawable starIcon = ContextCompat.getDrawable(context, R.drawable.ic_action_star); - favorite.setIcon(new ActionIconDrawable(starIcon, ((ActionIconDrawable) oldIcon).getDefaultColor())); - } else { - favorite.setIcon(R.drawable.ic_action_star); - } - ActionIconDrawable.setMenuHighlight(favorite, new TwidereMenuInfo(is_favorite, favoriteHighlight)); - favorite.setTitle(is_favorite ? R.string.unfavorite : R.string.favorite); + ActionProvider provider = MenuItemCompat.getActionProvider(favorite); + final boolean useStar = preferences.getBoolean(KEY_I_WANT_MY_STARS_BACK); + if (provider instanceof FavoriteItemProvider) { + + } else { - ActionIconDrawable.setMenuHighlight(favorite, new TwidereMenuInfo(is_favorite, likeHighlight)); - favorite.setTitle(is_favorite ? R.string.undo_like : R.string.like); + if (useStar) { + final Drawable oldIcon = favorite.getIcon(); + if (oldIcon instanceof ActionIconDrawable) { + final Drawable starIcon = ContextCompat.getDrawable(context, R.drawable.ic_action_star); + favorite.setIcon(new ActionIconDrawable(starIcon, ((ActionIconDrawable) oldIcon).getDefaultColor())); + } else { + favorite.setIcon(R.drawable.ic_action_star); + } + ActionIconDrawable.setMenuHighlight(favorite, new TwidereMenuInfo(isFavorite, favoriteHighlight)); + } else { + ActionIconDrawable.setMenuHighlight(favorite, new TwidereMenuInfo(isFavorite, likeHighlight)); + } + } + if (useStar) { + favorite.setTitle(isFavorite ? R.string.unfavorite : R.string.favorite); + } else { + favorite.setTitle(isFavorite ? R.string.undo_like : R.string.like); } } final MenuItem translate = menu.findItem(R.id.translate); @@ -2561,11 +2572,13 @@ public final class Utils implements Constants { return pm.getDrawable(info.packageName, info.metaData.getInt(key), info.applicationInfo); } - public static boolean handleMenuItemClick(@NonNull Context context, @Nullable Fragment fragment, - @NonNull FragmentManager fm, - @NonNull UserColorNameManager colorNameManager, - @NonNull AsyncTwitterWrapper twitter, - @NonNull ParcelableStatus status, @NonNull MenuItem item) { + public static boolean handleMenuItemClick(@NonNull final Context context, + @Nullable final Fragment fragment, + @NonNull final FragmentManager fm, + @NonNull final UserColorNameManager colorNameManager, + @NonNull final AsyncTwitterWrapper twitter, + @NonNull final ParcelableStatus status, + @NonNull final MenuItem item) { switch (item.getItemId()) { case R.id.copy: { if (ClipboardUtils.setText(context, status.text_plain)) { @@ -2597,7 +2610,13 @@ public final class Utils implements Constants { if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { - twitter.createFavoriteAsync(status.account_id, status.id); + ActionProvider provider = MenuItemCompat.getActionProvider(item); + if (provider instanceof FavoriteItemProvider) { + ((FavoriteItemProvider) provider).invokeItem(item, + new DefaultOnLikedListener(twitter, status)); + } else { + twitter.createFavoriteAsync(status.account_id, status.id); + } } break; } diff --git a/twidere/src/main/java/org/mariotaku/twidere/view/holder/StatusViewHolder.java b/twidere/src/main/java/org/mariotaku/twidere/view/holder/StatusViewHolder.java index f91a54a8a..21697faf5 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/view/holder/StatusViewHolder.java +++ b/twidere/src/main/java/org/mariotaku/twidere/view/holder/StatusViewHolder.java @@ -1,6 +1,7 @@ package org.mariotaku.twidere.view.holder; import android.content.Context; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v4.text.BidiFormatter; @@ -18,6 +19,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.R; import org.mariotaku.twidere.adapter.iface.IStatusesAdapter; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; import org.mariotaku.twidere.model.ParcelableLocation; import org.mariotaku.twidere.model.ParcelableMedia; import org.mariotaku.twidere.model.ParcelableStatus; @@ -401,15 +403,40 @@ public class StatusViewHolder extends ViewHolder implements Constants, IStatusVi nameView.setNameFirst(nameFirst); quotedNameView.setNameFirst(nameFirst); + final int likeIcon, likeStyle; if (adapter.shouldUseStarsForLikes()) { favoriteCountView.setActivatedColor(ContextCompat.getColor(adapter.getContext(), R.color.highlight_favorite)); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(favoriteCountView, - R.drawable.ic_action_star, 0, 0, 0); + likeIcon = R.drawable.ic_action_star; + likeStyle = LikeAnimationDrawable.Style.FAVORITE; + } else { + likeIcon = R.drawable.ic_action_heart; + likeStyle = LikeAnimationDrawable.Style.LIKE; } + final LikeAnimationDrawable drawable = new LikeAnimationDrawable(adapter.getContext(), + likeIcon, favoriteCountView.getColor(), favoriteCountView.getActivatedColor(), + likeStyle); + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(favoriteCountView, + drawable, null, null, null); + drawable.setCallback(favoriteCountView); timeView.setShowAbsoluteTime(adapter.isShowAbsoluteTime()); } + @Override + public void playLikeAnimation(@NonNull LikeAnimationDrawable.OnLikedListener listener) { + boolean handled = false; + for (Drawable drawable : favoriteCountView.getCompoundDrawables()) { + if (drawable instanceof LikeAnimationDrawable) { + ((LikeAnimationDrawable) drawable).setOnLikedListener(listener); + ((LikeAnimationDrawable) drawable).start(); + handled = true; + } + } + if (!handled) { + listener.onLiked(); + } + } + void displayExtraTypeIcon(String cardName, ParcelableMedia[] media, ParcelableLocation location, String placeFullName, boolean sensitive) { if (TwitterCardUtils.CARD_NAME_AUDIO.equals(cardName)) { extraTypeView.setImageResource(sensitive ? R.drawable.ic_action_warning : R.drawable.ic_action_music); diff --git a/twidere/src/main/java/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.java b/twidere/src/main/java/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.java index 2a724da05..20aa7aeb7 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.java +++ b/twidere/src/main/java/org/mariotaku/twidere/view/holder/iface/IStatusViewHolder.java @@ -21,10 +21,12 @@ package org.mariotaku.twidere.view.holder.iface; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import org.mariotaku.twidere.adapter.iface.ContentCardClickListener; +import org.mariotaku.twidere.graphic.LikeAnimationDrawable; import org.mariotaku.twidere.model.ParcelableMedia; import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.view.CardMediaContainer; @@ -51,6 +53,8 @@ public interface IStatusViewHolder extends CardMediaContainer.OnMediaClickListen void setTextSize(float textSize); + void playLikeAnimation(LikeAnimationDrawable.OnLikedListener listener); + interface StatusClickListener extends ContentCardClickListener { void onMediaClick(IStatusViewHolder holder, View view, ParcelableMedia media, int statusPosition); @@ -61,4 +65,33 @@ public interface IStatusViewHolder extends CardMediaContainer.OnMediaClickListen void onUserProfileClick(IStatusViewHolder holder, int position); } + + abstract class SimpleStatusClickListener implements StatusClickListener { + + public void onMediaClick(IStatusViewHolder holder, View view, ParcelableMedia media, int statusPosition) { + + } + + public void onStatusClick(IStatusViewHolder holder, int position) { + + } + + public boolean onStatusLongClick(IStatusViewHolder holder, int position) { + return false; + } + + public void onUserProfileClick(IStatusViewHolder holder, int position) { + + } + + @Override + public void onItemActionClick(RecyclerView.ViewHolder holder, int id, int position) { + + } + + @Override + public void onItemMenuClick(RecyclerView.ViewHolder holder, View menuView, int position) { + + } + } } diff --git a/twidere/src/main/res/layout/card_item_status_common.xml b/twidere/src/main/res/layout/card_item_status_common.xml index 185c9672d..ec3d7c1c8 100644 --- a/twidere/src/main/res/layout/card_item_status_common.xml +++ b/twidere/src/main/res/layout/card_item_status_common.xml @@ -28,6 +28,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" + android:clipChildren="false" android:focusable="true" android:paddingTop="@dimen/element_spacing_small" app:ignorePadding="true"> @@ -272,6 +273,7 @@ android:layout_alignStart="@+id/profile_container" android:layout_below="@+id/status_content_space" android:layout_marginTop="@dimen/element_spacing_minus_mlarge" + android:clipChildren="false" android:gravity="center_vertical|start" android:orientation="horizontal"> diff --git a/twidere/src/main/res/layout/header_status_common.xml b/twidere/src/main/res/layout/header_status_common.xml index c412e2ffa..2dadb6016 100644 --- a/twidere/src/main/res/layout/header_status_common.xml +++ b/twidere/src/main/res/layout/header_status_common.xml @@ -396,6 +396,7 @@ android:id="@+id/menu_bar" android:layout_width="match_parent" android:layout_height="?android:actionBarSize" - android:layout_below="@+id/counts_users_height_holder"/> + android:layout_below="@+id/counts_users_height_holder" + android:clipChildren="false"/> \ No newline at end of file diff --git a/twidere/src/main/res/layout/layout_action_item_favorite.xml b/twidere/src/main/res/layout/layout_action_item_favorite.xml new file mode 100644 index 000000000..3f4158581 --- /dev/null +++ b/twidere/src/main/res/layout/layout_action_item_favorite.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/twidere/src/main/res/menu/menu_detail_status.xml b/twidere/src/main/res/menu/menu_detail_status.xml index d2da3cabf..52c5ea2e5 100644 --- a/twidere/src/main/res/menu/menu_detail_status.xml +++ b/twidere/src/main/res/menu/menu_detail_status.xml @@ -18,6 +18,7 @@ android:id="@id/favorite" android:icon="@drawable/ic_action_heart" android:title="@string/like" + app:actionProviderClass="org.mariotaku.twidere.menu.support.FavoriteItemProvider" app:showAsAction="always"/>