/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.plaidapp.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.util.Pair; import android.view.View; import android.view.ViewGroup; import java.util.List; import io.plaidapp.ui.recyclerview.SlideInItemAnimator; import io.plaidapp.ui.transitions.GravityArcMotion; import io.plaidapp.util.AnimUtils; import io.plaidapp.util.ViewUtils; /** * A {@link RecyclerView.ItemAnimator} for running animations specific to our home grid. */ public class HomeGridItemAnimator extends SlideInItemAnimator { // Constant payloads, for use with Adapter#notifyItemChanged public static final int ADD_TO_POCKET = 1; public static final int STORY_COMMENTS_RETURN = 2; // Pending animations private FeedAdapter.DesignerNewsStoryHolder pendingAddToPocket; private FeedAdapter.DesignerNewsStoryHolder pendingStoryCommentsReturn; // Currently running animations private Pair<FeedAdapter.DesignerNewsStoryHolder, AnimatorSet> runningAddToPocket; private Pair<FeedAdapter.DesignerNewsStoryHolder, AnimatorSet> runningStoryCommentsReturn; @Override public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) { return true; } @Override public ItemHolderInfo obtainHolderInfo() { return new HomeGridItemHolderInfo(); } @NonNull @Override public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state, RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) { ItemHolderInfo info = super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads); if (info instanceof HomeGridItemHolderInfo) { HomeGridItemHolderInfo dnInfo = (HomeGridItemHolderInfo) info; dnInfo.animateAddToPocket = payloads.contains(ADD_TO_POCKET); dnInfo.returnFromComments = payloads.contains(STORY_COMMENTS_RETURN); return dnInfo; } return info; } @Override public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo, ItemHolderInfo postInfo) { boolean runPending = super.animateChange(oldHolder, newHolder, preInfo, postInfo); if (preInfo instanceof HomeGridItemHolderInfo) { HomeGridItemHolderInfo info = (HomeGridItemHolderInfo) preInfo; if (info.animateAddToPocket) { pendingAddToPocket = (FeedAdapter.DesignerNewsStoryHolder) newHolder; runPending = true; } if (info.returnFromComments) { pendingStoryCommentsReturn = (FeedAdapter.DesignerNewsStoryHolder) newHolder; runPending = true; } } return runPending; } @Override public void runPendingAnimations() { super.runPendingAnimations(); if (pendingAddToPocket != null) { animateAddToPocket(pendingAddToPocket); pendingAddToPocket = null; } if (pendingStoryCommentsReturn != null) { animateStoryCommentReturn(pendingStoryCommentsReturn); pendingStoryCommentsReturn = null; } } @Override public void endAnimation(RecyclerView.ViewHolder holder) { super.endAnimation(holder); if (holder == pendingAddToPocket) { dispatchChangeFinished(pendingAddToPocket, false); pendingAddToPocket = null; } if (holder == pendingStoryCommentsReturn) { dispatchChangeFinished(pendingStoryCommentsReturn, false); pendingStoryCommentsReturn = null; } if (runningAddToPocket != null && runningAddToPocket.first == holder) { runningAddToPocket.second.cancel(); } if (runningStoryCommentsReturn != null && runningStoryCommentsReturn.first == holder) { runningStoryCommentsReturn.second.cancel(); } } @Override public void endAnimations() { super.endAnimations(); if (pendingAddToPocket != null) { dispatchChangeFinished(pendingAddToPocket, false); pendingAddToPocket = null; } if (pendingStoryCommentsReturn != null) { dispatchChangeFinished(pendingStoryCommentsReturn, false); pendingStoryCommentsReturn = null; } if (runningAddToPocket != null) { runningAddToPocket.second.cancel(); } if (runningStoryCommentsReturn != null) { runningStoryCommentsReturn.second.cancel(); } } @Override public boolean isRunning() { return super.isRunning() || (runningAddToPocket != null && runningAddToPocket.second.isRunning()) || (runningStoryCommentsReturn != null && runningStoryCommentsReturn.second.isRunning()); } private void animateAddToPocket(final FeedAdapter.DesignerNewsStoryHolder holder) { endAnimation(holder); // setup for anim ((ViewGroup) holder.pocket.getParent().getParent()).setClipChildren(false); final int initialLeft = holder.pocket.getLeft(); final int initialTop = holder.pocket.getTop(); final int translatedLeft = (holder.itemView.getWidth() - holder.pocket.getWidth()) / 2; final int translatedTop = initialTop - ((holder.itemView.getHeight() - holder.pocket.getHeight()) / 2); final GravityArcMotion arc = new GravityArcMotion(); // animate the title & pocket icon up, scale the pocket icon up Animator titleMoveFadeOut = ObjectAnimator.ofPropertyValuesHolder(holder.title, PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -(holder.itemView.getHeight() / 5)), PropertyValuesHolder.ofFloat(View.ALPHA, 0.54f)); Animator pocketMoveUp = ObjectAnimator.ofFloat(holder.pocket, View.TRANSLATION_X, View.TRANSLATION_Y, arc.getPath(initialLeft, initialTop, translatedLeft, translatedTop)); Animator pocketScaleUp = ObjectAnimator.ofPropertyValuesHolder(holder.pocket, PropertyValuesHolder.ofFloat(View.SCALE_X, 3f), PropertyValuesHolder.ofFloat(View.SCALE_Y, 3f)); ObjectAnimator pocketFadeUp = ObjectAnimator.ofInt(holder.pocket, ViewUtils.IMAGE_ALPHA, 255); AnimatorSet up = new AnimatorSet(); up.playTogether(titleMoveFadeOut, pocketMoveUp, pocketScaleUp, pocketFadeUp); up.setDuration(300L); up.setInterpolator(AnimUtils.getFastOutSlowInInterpolator(holder.itemView.getContext())); // animate everything back into place Animator titleMoveFadeIn = ObjectAnimator.ofPropertyValuesHolder(holder.title, PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f), PropertyValuesHolder.ofFloat(View.ALPHA, 1f)); Animator pocketMoveDown = ObjectAnimator.ofFloat(holder.pocket, View.TRANSLATION_X, View.TRANSLATION_Y, arc.getPath(translatedLeft, translatedTop, 0, 0)); Animator pvhPocketScaleDown = ObjectAnimator.ofPropertyValuesHolder(holder.pocket, PropertyValuesHolder.ofFloat(View.SCALE_X, 1f), PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f)); ObjectAnimator pocketFadeDown = ObjectAnimator.ofInt(holder.pocket, ViewUtils.IMAGE_ALPHA, 178); AnimatorSet down = new AnimatorSet(); down.playTogether(titleMoveFadeIn, pocketMoveDown, pvhPocketScaleDown, pocketFadeDown); down.setStartDelay(500L); down.setDuration(300L); down.setInterpolator(AnimUtils.getFastOutSlowInInterpolator(holder.itemView.getContext())); AnimatorSet addToPocketAnim = new AnimatorSet(); addToPocketAnim.playSequentially(up, down); addToPocketAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { dispatchChangeStarting(holder, false); } @Override public void onAnimationEnd(Animator animation) { ((ViewGroup) holder.pocket.getParent().getParent()).setClipChildren(true); runningAddToPocket = null; dispatchChangeFinished(holder, false); } @Override public void onAnimationCancel(Animator animation) { holder.title.setAlpha(1f); holder.title.setTranslationY(0f); holder.pocket.setTranslationX(0f); holder.pocket.setTranslationY(0f); holder.pocket.setScaleX(1f); holder.pocket.setScaleY(1f); holder.pocket.setImageAlpha(178); runningAddToPocket = null; dispatchChangeFinished(holder, false); } }); runningAddToPocket = Pair.create(holder, addToPocketAnim); addToPocketAnim.start(); } private void animateStoryCommentReturn(final FeedAdapter.DesignerNewsStoryHolder holder) { endAnimation(holder); AnimatorSet commentsReturnAnim = new AnimatorSet(); commentsReturnAnim.playTogether( ObjectAnimator.ofFloat(holder.pocket, View.ALPHA, 0f, 1f), ObjectAnimator.ofFloat(holder.comments, View.ALPHA, 0f, 1f)); commentsReturnAnim.setDuration(120L); commentsReturnAnim.setInterpolator( AnimUtils.getLinearOutSlowInInterpolator(holder.itemView.getContext())); commentsReturnAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { dispatchChangeStarting(holder, false); } @Override public void onAnimationEnd(Animator animation) { runningStoryCommentsReturn = null; dispatchChangeFinished(holder, false); } @Override public void onAnimationCancel(Animator animation) { holder.pocket.setAlpha(1f); holder.comments.setAlpha(1f); runningStoryCommentsReturn = null; dispatchChangeFinished(holder, false); } }); runningStoryCommentsReturn = Pair.create(holder, commentsReturnAnim); commentsReturnAnim.start(); } private class HomeGridItemHolderInfo extends ItemHolderInfo { boolean animateAddToPocket; boolean returnFromComments; } }