diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java index 0c7d9ae..89018d0 100644 --- a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java +++ b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java @@ -243,14 +243,6 @@ public class BrowserPresenter { onTabChanged(tab); } - // TODO Restore this - // new Handler().postDelayed(new Runnable() { - // @Override - // public void run() { - // mDrawerListLeft.smoothScrollToPosition(mTabsManager.size() - 1); - // } - // }, 300); - mView.updateTabNumber(mTabsModel.size()); return true; diff --git a/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java index 2b9c292..d7f5e15 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java @@ -21,6 +21,7 @@ import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.LayoutManager; +import android.support.v7.widget.SimpleItemAnimator; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,6 +43,8 @@ import acr.browser.lightning.bus.BrowserEvents; import acr.browser.lightning.bus.NavigationEvents; import acr.browser.lightning.bus.TabEvents; import acr.browser.lightning.controller.UIController; +import acr.browser.lightning.fragment.anim.HorizontalItemAnimator; +import acr.browser.lightning.fragment.anim.VerticalItemAnimator; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.Utils; @@ -72,6 +75,7 @@ public class TabsFragment extends Fragment implements View.OnClickListener, View @Nullable private LightningViewAdapter mTabsAdapter; private UIController mUiController; + private RecyclerView mRecyclerView; @Inject TabsManager tabsManager; @Inject Bus mBus; @@ -122,11 +126,24 @@ public class TabsFragment extends Fragment implements View.OnClickListener, View } }); } - RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.tabs_list); - recyclerView.setLayoutManager(layoutManager); + mRecyclerView = (RecyclerView) view.findViewById(R.id.tabs_list); + SimpleItemAnimator animator; + if (mShowInNavigationDrawer) { + animator = new VerticalItemAnimator(); + } else { + animator = new HorizontalItemAnimator(); + } + animator.setSupportsChangeAnimations(false); + animator.setAddDuration(200); + animator.setChangeDuration(0); + animator.setRemoveDuration(200); + animator.setMoveDuration(200); + mRecyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mRecyclerView.setItemAnimator(animator); + mRecyclerView.setLayoutManager(layoutManager); mTabsAdapter = new LightningViewAdapter(mShowInNavigationDrawer); - recyclerView.setAdapter(mTabsAdapter); - recyclerView.setHasFixedSize(true); + mRecyclerView.setAdapter(mTabsAdapter); + mRecyclerView.setHasFixedSize(true); return view; } @@ -211,6 +228,12 @@ public class TabsFragment extends Fragment implements View.OnClickListener, View public void tabAdded() { if (mTabsAdapter != null) { mTabsAdapter.notifyItemInserted(tabsManager.last()); + mRecyclerView.postDelayed(new Runnable() { + @Override + public void run() { + mRecyclerView.smoothScrollToPosition(mTabsAdapter.getItemCount() - 1); + } + }, 500); } } @@ -320,7 +343,6 @@ public class TabsFragment extends Fragment implements View.OnClickListener, View } holder.favicon.setImageBitmap(getDesaturatedBitmap(favicon)); } - holder.itemView.setLayerType(View.LAYER_TYPE_HARDWARE, null); } @Override diff --git a/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java b/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java new file mode 100644 index 0000000..da92f60 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 acr.browser.lightning.fragment.anim; + +import android.support.v4.animation.AnimatorCompatHelper; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.RecyclerView; +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; + +/** + * This implementation of {@link RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a HorizontalItemAnimator by default. + * + * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) + */ +public class HorizontalItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + private ArrayList> mAdditionsList = new ArrayList<>(); + private ArrayList> mMovesList = new ArrayList<>(); + private ArrayList> mChangesList = new ArrayList<>(); + + private ArrayList mAddAnimations = new ArrayList<>(); + private ArrayList mMoveAnimations = new ArrayList<>(); + private ArrayList mRemoveAnimations = new ArrayList<>(); + private ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + private MoveInfo(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 static class ChangeInfo { + public ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + public void run() { + for (ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()) + .alpha(0).translationY(holder.itemView.getHeight()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateAdd(final ViewHolder holder) { + resetAnimation(holder); + ViewCompat.setAlpha(holder.itemView, 0); + ViewCompat.setTranslationY(holder.itemView, holder.itemView.getHeight()); + mPendingAdditions.add(holder); + return true; + } + + private void animateAddImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mAddAnimations.add(holder); + animation.alpha(1).translationY(0).setDuration(getAddDuration()). + setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + ViewCompat.setAlpha(view, 1); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += ViewCompat.getTranslationX(holder.itemView); + fromY += ViewCompat.getTranslationY(holder.itemView); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + ViewCompat.setTranslationX(view, -deltaX); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, -deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + ViewCompat.animate(view).translationX(0); + } + if (deltaY != 0) { + ViewCompat.animate(view).translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + if (deltaX != 0) { + ViewCompat.setTranslationX(view, 0); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, 0); + } + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromX, fromY, toX, toY); + } + final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); + final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); + final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); + ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); + ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + ViewCompat.setTranslationX(newHolder.itemView, -deltaX); + ViewCompat.setTranslationY(newHolder.itemView, -deltaY); + ViewCompat.setAlpha(newHolder.itemView, 0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + private void animateChangeImpl(final ChangeInfo changeInfo) { + final ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(View view) { + oldViewAnim.setListener(null); + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0); + ViewCompat.setTranslationY(view, 0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). + alpha(1).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + + @Override + public void onAnimationEnd(View view) { + newViewAnimation.setListener(null); + ViewCompat.setAlpha(newView, 1); + ViewCompat.setTranslationX(newView, 0); + ViewCompat.setTranslationY(newView, 0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + ViewCompat.setAlpha(item.itemView, 1); + ViewCompat.setTranslationX(item.itemView, 0); + ViewCompat.setTranslationY(item.itemView, 0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + ViewCompat.animate(view).cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + private void resetAnimation(ViewHolder holder) { + AnimatorCompatHelper.clearInterpolator(holder.itemView); + endAnimation(holder); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() || + !mPendingChanges.isEmpty() || + !mPendingMoves.isEmpty() || + !mPendingRemovals.isEmpty() || + !mMoveAnimations.isEmpty() || + !mRemoveAnimations.isEmpty() || + !mAddAnimations.isEmpty() || + !mChangeAnimations.isEmpty() || + !mMovesList.isEmpty() || + !mAdditionsList.isEmpty() || + !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + private void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingAdditions.get(i); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + ViewHolder item = moveInfo.holder; + View view = item.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + ViewHolder item = additions.get(j); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + ViewCompat.animate(viewHolders.get(i).itemView).cancel(); + } + } + + private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { + @Override + public void onAnimationStart(View view) {} + + @Override + public void onAnimationEnd(View view) {} + + @Override + public void onAnimationCancel(View view) {} + } +} diff --git a/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java b/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java new file mode 100644 index 0000000..4d435bc --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 acr.browser.lightning.fragment.anim; + +import android.support.v4.animation.AnimatorCompatHelper; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.RecyclerView; +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; + +/** + * This implementation of {@link RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a VerticalItemAnimator by default. + * + * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) + */ +public class VerticalItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + private ArrayList> mAdditionsList = new ArrayList<>(); + private ArrayList> mMovesList = new ArrayList<>(); + private ArrayList> mChangesList = new ArrayList<>(); + + private ArrayList mAddAnimations = new ArrayList<>(); + private ArrayList mMoveAnimations = new ArrayList<>(); + private ArrayList mRemoveAnimations = new ArrayList<>(); + private ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + private MoveInfo(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 static class ChangeInfo { + public ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + public void run() { + for (ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()) + .alpha(0).translationX(-holder.itemView.getWidth() / 2).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateAdd(final ViewHolder holder) { + resetAnimation(holder); + ViewCompat.setAlpha(holder.itemView, 0); + ViewCompat.setTranslationX(holder.itemView, -holder.itemView.getWidth() / 2); + mPendingAdditions.add(holder); + return true; + } + + private void animateAddImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mAddAnimations.add(holder); + animation.alpha(1).translationX(0).setDuration(getAddDuration()). + setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + ViewCompat.setAlpha(view, 1); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += ViewCompat.getTranslationX(holder.itemView); + fromY += ViewCompat.getTranslationY(holder.itemView); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + ViewCompat.setTranslationX(view, -deltaX); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, -deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + ViewCompat.animate(view).translationX(0); + } + if (deltaY != 0) { + ViewCompat.animate(view).translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(View view) { + if (deltaX != 0) { + ViewCompat.setTranslationX(view, 0); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, 0); + } + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromX, fromY, toX, toY); + } + final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); + final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); + final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); + ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); + ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + ViewCompat.setTranslationX(newHolder.itemView, -deltaX); + ViewCompat.setTranslationY(newHolder.itemView, -deltaY); + ViewCompat.setAlpha(newHolder.itemView, 0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + private void animateChangeImpl(final ChangeInfo changeInfo) { + final ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(View view) { + oldViewAnim.setListener(null); + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0); + ViewCompat.setTranslationY(view, 0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). + alpha(1).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + + @Override + public void onAnimationEnd(View view) { + newViewAnimation.setListener(null); + ViewCompat.setAlpha(newView, 1); + ViewCompat.setTranslationX(newView, 0); + ViewCompat.setTranslationY(newView, 0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + ViewCompat.setAlpha(item.itemView, 1); + ViewCompat.setTranslationX(item.itemView, 0); + ViewCompat.setTranslationY(item.itemView, 0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + ViewCompat.animate(view).cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + private void resetAnimation(ViewHolder holder) { + AnimatorCompatHelper.clearInterpolator(holder.itemView); + endAnimation(holder); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() || + !mPendingChanges.isEmpty() || + !mPendingMoves.isEmpty() || + !mPendingRemovals.isEmpty() || + !mMoveAnimations.isEmpty() || + !mRemoveAnimations.isEmpty() || + !mAddAnimations.isEmpty() || + !mChangeAnimations.isEmpty() || + !mMovesList.isEmpty() || + !mAdditionsList.isEmpty() || + !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + private void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + ViewHolder item = mPendingAdditions.get(i); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + ViewHolder item = moveInfo.holder; + View view = item.itemView; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + ViewHolder item = additions.get(j); + View view = item.itemView; + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + ViewCompat.animate(viewHolders.get(i).itemView).cancel(); + } + } + + private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { + @Override + public void onAnimationStart(View view) {} + + @Override + public void onAnimationEnd(View view) {} + + @Override + public void onAnimationCancel(View view) {} + } +}