Java tutorial
/* * Copyright (c) 2016. Matsuda, Akihit (akihito104) * * 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 com.freshdigitable.udonroad; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorCompat; import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.SimpleItemAnimator; import android.view.View; import java.util.ArrayList; import java.util.List; /** * customized animation for RecyclerView as a Twitter Timeline * * Created by akihit on 2015/11/21. */ public class TimelineAnimator extends SimpleItemAnimator { @SuppressWarnings("unused") private static final String TAG = TimelineAnimator.class.getSimpleName(); public TimelineAnimator() { super(); setSupportsChangeAnimations(false); } private final List<ViewHolder> pendingRemoves = new ArrayList<>(); private final List<ViewHolder> removeAnimations = new ArrayList<>(); @Override public boolean animateRemove(final ViewHolder holder) { // Log.d(TAG, "animateRemove: "); clearAllAnimationSettings(holder.itemView); pendingRemoves.add(holder); return true; } private void animateRemoveImpl(final ViewHolder holder) { // Log.d(TAG, "animateRemoveImpl: "); removeAnimations.add(holder); ViewCompat.animate(holder.itemView).translationYBy(-holder.itemView.getHeight()).alpha(0) .setDuration(getRemoveDuration()).setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchRemoveStarting(holder); } @Override public void onAnimationEnd(View view) { clearAllAnimationSettings(view); dispatchRemoveFinished(holder); removeAnimations.remove(holder); dispatchFinishedWhenDone(); } @Override public void onAnimationCancel(View view) { clearAllAnimationSettings(view); } }).start(); } private final List<Move> pendingMoves = new ArrayList<>(); private final List<ViewHolder> moveAnimations = new ArrayList<>(); private static class Move { private final ViewHolder holder; private final int fromX; private final int fromY; private final int toX; private final int toY; private Move(ViewHolder holder, int fromX, int fromY, int toX, int toY) { this.holder = holder; this.fromX = fromX; this.fromY = fromY; this.toX = toX; this.toY = toY; } private int deltaX() { return toX - fromX; } private int deltaY() { return toY - fromY; } } @Override public boolean animateMove(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { // Log.d(TAG, "animateMove: " + debugString(holder)); fromX += ViewCompat.getTranslationX(holder.itemView); fromY += ViewCompat.getTranslationY(holder.itemView); clearAllAnimationSettings(holder.itemView); final int dX = toX - fromX; final int dY = toY - fromY; if (dX == 0 && dY == 0) { dispatchMoveFinished(holder); return false; } if (dX != 0) { ViewCompat.setTranslationX(holder.itemView, -dX); } if (dY != 0) { ViewCompat.setTranslationY(holder.itemView, -dY); } pendingMoves.add(new Move(holder, fromX, fromY, toX, toY)); return true; } private void animateMoveImpl(final Move move) { // Log.d(TAG, "animateMoveImpl: " + debugString(move.holder)); moveAnimations.add(move.holder); final ViewPropertyAnimatorCompat animate = ViewCompat.animate(move.holder.itemView); if (move.deltaX() != 0) { animate.translationX(0); } if (move.deltaY() != 0) { animate.translationY(0); } animate.setDuration(getMoveDuration()).setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchMoveStarting(move.holder); } @Override public void onAnimationEnd(View view) { animate.setListener(null); dispatchMoveFinished(move.holder); moveAnimations.remove(move.holder); dispatchFinishedWhenDone(); } @Override public void onAnimationCancel(View view) { if (move.deltaX() != 0) { ViewCompat.setTranslationX(view, 0); } if (move.deltaY() != 0) { ViewCompat.setTranslationY(view, 0); } } }).start(); } private final List<ViewHolder> pendingAdd = new ArrayList<>(); private final List<ViewHolder> addAnimations = new ArrayList<>(); @Override public boolean animateAdd(ViewHolder holder) { // Log.d(TAG, "animateAdd: " + debugString(holder)); clearAllAnimationSettings(holder.itemView); ViewCompat.setTranslationY(holder.itemView, 0); pendingAdd.add(holder); return true; } private void animateAddImpl(final ViewHolder holder) { // Log.d(TAG, "animateAddImpl: " + debugString(holder)); addAnimations.add(holder); ViewCompat.animate(holder.itemView).setDuration(0).setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchAddStarting(holder); } @Override public void onAnimationEnd(View view) { clearAllAnimationSettings(view); dispatchAddFinished(holder); addAnimations.remove(holder); } @Override public void onAnimationCancel(View view) { clearAllAnimationSettings(view); } }).start(); } private List<Change> pendingChange = new ArrayList<>(); private List<ViewHolder> changeAnimations = new ArrayList<>(); private static class Change { private ViewHolder oldHolder; private ViewHolder newHolder; private int fromLeft; private int fromTop; private int toLeft; private int toTop; private Change(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) { this.oldHolder = oldHolder; this.newHolder = newHolder; this.fromLeft = fromLeft; this.fromTop = fromTop; this.toLeft = toLeft; this.toTop = toTop; } View getOldView() { return getView(oldHolder); } View getNewView() { return getView(newHolder); } View getView(ViewHolder holder) { return holder != null ? holder.itemView : null; } private float deltaX() { return toTop - fromTop; } private float deltaY() { return toLeft - fromLeft; } private boolean removeViewHolderIfMatch(ViewHolder holder) { if (holder == oldHolder) { oldHolder = null; return true; } else if (holder == newHolder) { newHolder = null; return true; } return false; } private boolean isDone() { return oldHolder == null && newHolder == null; } } @Override public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) { // Log.d(TAG, "animateChange: "); if (oldHolder == newHolder) { return animateMove(oldHolder, fromLeft, fromTop, toLeft, toTop); } final float prevTransX = ViewCompat.getTranslationX(oldHolder.itemView); final float prevTransY = ViewCompat.getTranslationY(oldHolder.itemView); final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); endAnimation(oldHolder); ViewCompat.setTranslationX(oldHolder.itemView, prevTransX); ViewCompat.setTranslationY(oldHolder.itemView, prevTransY); ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); if (newHolder != null && newHolder.itemView != null) { endAnimation(newHolder); final float dX = toLeft - fromLeft - prevTransX; final float dY = toTop - fromTop - prevTransY; ViewCompat.setTranslationX(newHolder.itemView, -dX); ViewCompat.setTranslationY(newHolder.itemView, -dY); ViewCompat.setAlpha(newHolder.itemView, 0); } pendingChange.add(new Change(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop)); return true; } private void animateChangeImpl(Change change) { final View oldView = change.getOldView(); if (oldView != null) { final ViewHolder oldHolder = change.oldHolder; changeAnimations.add(oldHolder); final ViewPropertyAnimatorCompat oldAnim = ViewCompat.animate(oldView); oldAnim.setDuration(getChangeDuration()).translationX(change.deltaX()).translationY(change.deltaY()) .alpha(0).setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchChangeStarting(oldHolder, true); } @Override public void onAnimationEnd(View view) { oldAnim.setListener(null); ViewCompat.setTranslationX(view, 0); ViewCompat.setTranslationY(view, 0); ViewCompat.setAlpha(view, 1); dispatchChangeFinished(oldHolder, true); changeAnimations.remove(oldHolder); dispatchFinishedWhenDone(); } }).start(); } final View newView = change.getNewView(); if (newView != null) { final ViewHolder newHolder = change.newHolder; changeAnimations.add(newHolder); final ViewPropertyAnimatorCompat newAnim = ViewCompat.animate(newView); newAnim.setDuration(0).translationX(0).translationY(0).alpha(1) .setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationStart(View view) { dispatchChangeStarting(newHolder, false); } @Override public void onAnimationEnd(View view) { newAnim.setListener(null); ViewCompat.setTranslationX(view, 0); ViewCompat.setTranslationY(view, 0); ViewCompat.setAlpha(view, 1); dispatchChangeFinished(newHolder, false); changeAnimations.remove(newHolder); dispatchFinishedWhenDone(); } }).start(); } } private void endChangeAnimation(List<Change> changes, ViewHolder holder) { for (int i = changes.size() - 1; i >= 0; i--) { final Change change = changes.get(i); boolean isOld = change.oldHolder == holder; if (change.removeViewHolderIfMatch(holder)) { endChangeAnimation(holder, isOld); } if (change.isDone()) { changes.remove(change); } } } private void endChangeAnimation(Change change) { endChangeAnimation(change.oldHolder, true); endChangeAnimation(change.newHolder, false); change.oldHolder = null; change.newHolder = null; } private void endChangeAnimation(ViewHolder holder, boolean isOld) { ViewCompat.setTranslationX(holder.itemView, 0); ViewCompat.setTranslationY(holder.itemView, 0); ViewCompat.setAlpha(holder.itemView, 1); dispatchChangeFinished(holder, isOld); } private final List<List<Move>> moveAnimList = new ArrayList<>(); private final List<List<ViewHolder>> addAnimList = new ArrayList<>(); private final List<List<Change>> changeAnimList = new ArrayList<>(); @Override public void runPendingAnimations() { // Log.d(TAG, "runPendingAnimations: "); final boolean isPendingRemove = !pendingRemoves.isEmpty(); final boolean isPendingMove = !pendingMoves.isEmpty(); // remove for (ViewHolder pendingRemove : pendingRemoves) { animateRemoveImpl(pendingRemove); } pendingRemoves.clear(); // move if (isPendingMove) { final List<Move> moves = new ArrayList<>(pendingMoves.size()); moves.addAll(pendingMoves); moveAnimList.add(moves); pendingMoves.clear(); final Runnable mover = new Runnable() { @Override public void run() { for (Move move : moves) { animateMoveImpl(move); } moves.clear(); moveAnimList.remove(moves); } }; if (isPendingRemove) { ViewCompat.postOnAnimationDelayed(moves.get(0).holder.itemView, mover, getRemoveDuration()); } else { mover.run(); } } final boolean isPendingChange = !pendingChange.isEmpty(); if (isPendingChange) { final List<Change> changes = new ArrayList<>(pendingChange.size()); changes.addAll(pendingChange); changeAnimList.add(changes); pendingChange.clear(); final Runnable changer = new Runnable() { @Override public void run() { for (Change c : changes) { animateChangeImpl(c); } changes.clear(); changeAnimList.remove(changes); } }; if (isPendingRemove) { ViewCompat.postOnAnimationDelayed(changes.get(0).oldHolder.itemView, changer, getRemoveDuration()); } else { changer.run(); } } // add if (!pendingAdd.isEmpty()) { final List<ViewHolder> adds = new ArrayList<>(pendingAdd.size()); adds.addAll(pendingAdd); addAnimList.add(adds); pendingAdd.clear(); final Runnable adder = new Runnable() { @Override public void run() { for (ViewHolder add : adds) { animateAddImpl(add); } adds.clear(); addAnimList.remove(adds); } }; if (isPendingRemove || isPendingMove || isPendingChange) { long removeDuration = isPendingRemove ? getRemoveDuration() : 0; long moveDuration = isPendingMove ? getMoveDuration() : 0; long changeDuration = isPendingChange ? getChangeDuration() : 0; final long totalDelay = removeDuration + moveDuration + changeDuration; ViewCompat.postOnAnimationDelayed(adds.get(0).itemView, adder, totalDelay); } else { adder.run(); } } } @Override public void endAnimation(ViewHolder item) { // Log.d(TAG, "endAnimation: "); ViewCompat.animate(item.itemView).cancel(); cancelMoveAnimationFromList(pendingMoves, item); endChangeAnimation(pendingChange, item); if (pendingRemoves.remove(item)) { clearAllAnimationSettings(item.itemView); dispatchRemoveFinished(item); } cancelAddAnimationFromList(pendingAdd, item); for (int i = changeAnimList.size() - 1; i >= 0; i++) { final List<Change> changes = changeAnimList.get(i); endChangeAnimation(changes, item); if (changes.isEmpty()) { changeAnimList.remove(i); } } for (int i = moveAnimList.size() - 1; i >= 0; i--) { final List<Move> moves = moveAnimList.get(i); cancelMoveAnimationFromList(moves, item); if (moves.isEmpty()) { moveAnimList.remove(i); } } for (int i = addAnimList.size() - 1; i >= 0; i--) { final List<ViewHolder> adds = addAnimList.get(i); cancelAddAnimationFromList(adds, item); if (adds.isEmpty()) { addAnimList.remove(i); } } if (changeAnimations.remove(item)) { throw new IllegalStateException("after animation is canceled, item should not be in changeAnimations."); } if (removeAnimations.remove(item)) { throw new IllegalStateException("after animation is canceled, item should not be in removeAnimations."); } if (addAnimations.remove(item)) { throw new IllegalStateException("after animation is canceled, item should not be in addAnimations."); } if (moveAnimations.remove(item)) { throw new IllegalStateException("after animation is canceled, item should not be in moveAnimations."); } dispatchFinishedWhenDone(); } private void cancelMoveAnimationFromList(List<Move> moves, ViewHolder item) { for (int i = moves.size() - 1; i >= 0; i--) { final Move move = moves.get(i); if (move.holder == item) { cancelMoveAnimation(item); moves.remove(i); } } } private void cancelMoveAnimation(ViewHolder item) { ViewCompat.setTranslationY(item.itemView, 0); ViewCompat.setTranslationX(item.itemView, 0); dispatchMoveFinished(item); } private void cancelAddAnimationFromList(List<ViewHolder> adds, ViewHolder item) { if (adds.remove(item)) { clearAllAnimationSettings(item.itemView); dispatchAddFinished(item); } } @Override public void endAnimations() { // Log.d(TAG, "endAnimations: "); for (int i = pendingMoves.size() - 1; i >= 0; i--) { Move move = pendingMoves.get(i); cancelMoveAnimation(move.holder); pendingMoves.remove(i); } for (int i = pendingRemoves.size() - 1; i >= 0; i--) { final ViewHolder remove = pendingRemoves.get(i); dispatchRemoveFinished(remove); pendingRemoves.remove(i); } for (int i = pendingAdd.size() - 1; i >= 0; i--) { final ViewHolder add = pendingAdd.get(i); dispatchAddFinished(add); pendingRemoves.remove(i); } for (int i = pendingChange.size() - 1; i >= 0; i--) { final Change change = pendingChange.get(i); endChangeAnimation(change); } pendingChange.clear(); if (!isRunning()) { return; } for (int i = moveAnimList.size() - 1; i >= 0; i--) { final List<Move> moves = moveAnimList.get(i); for (int j = moves.size() - 1; j >= 0; j--) { final Move move = moves.get(j); cancelMoveAnimation(move.holder); moves.remove(j); } moveAnimList.remove(i); } for (int i = addAnimList.size() - 1; i >= 0; i--) { final List<ViewHolder> adds = addAnimList.get(i); for (int j = adds.size() - 1; j >= 0; j--) { dispatchAddFinished(adds.get(j)); adds.remove(j); } addAnimList.remove(i); } for (int i = changeAnimList.size() - 1; i >= 0; i--) { final List<Change> changes = changeAnimList.get(i); for (int j = changes.size() - 1; j >= 0; j--) { final Change change = changes.get(j); endChangeAnimation(change); changes.remove(j); } changeAnimList.remove(i); } cancelAll(removeAnimations); cancelAll(moveAnimations); cancelAll(addAnimations); cancelAll(changeAnimations); dispatchAnimationsFinished(); } private void cancelAll(List<ViewHolder> viewHolders) { for (int i = viewHolders.size() - 1; i >= 0; i--) { final ViewHolder vh = viewHolders.get(i); ViewCompat.animate(vh.itemView).cancel(); } viewHolders.clear(); } @Override public boolean isRunning() { // Log.d(TAG, "isRunning: "); return !changeAnimations.isEmpty() || !changeAnimList.isEmpty() || !addAnimations.isEmpty() || !addAnimList.isEmpty() || !moveAnimations.isEmpty() || !moveAnimList.isEmpty() || !removeAnimations.isEmpty(); } private void dispatchFinishedWhenDone() { if (!isRunning()) { dispatchAnimationsFinished(); } } private static void clearAllAnimationSettings(View v) { ViewCompat.setAlpha(v, 1); ViewCompat.setScaleX(v, 1); ViewCompat.setScaleY(v, 1); ViewCompat.setTranslationX(v, 0); ViewCompat.setTranslationY(v, 0); ViewCompat.setRotationX(v, 0); ViewCompat.setRotationY(v, 0); ViewCompat.setPivotX(v, v.getMeasuredWidth() / 2); ViewCompat.setPivotY(v, v.getMeasuredHeight() / 2); ViewCompat.animate(v).setInterpolator(null).setStartDelay(0); } }