From 53a4f63bc28b1f701d2bcf815ffa2fe2e931eba0 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 27 Jun 2017 23:25:51 -0700 Subject: Add animators --- .../ca/allanwang/kau/about/AboutActivityBase.kt | 24 +- .../kotlin/ca/allanwang/kau/about/LibraryItem.kt | 8 + .../main/kotlin/ca/allanwang/kau/about/MainItem.kt | 11 +- .../ca/allanwang/kau/adapters/ChainedAdapters.kt | 15 +- .../ca/allanwang/kau/adapters/SectionAdapter.kt | 13 + .../allanwang/kau/animators/BaseItemAnimator.java | 764 +++++++++++++++++++++ .../ca/allanwang/kau/animators/DefaultAnimator.kt | 62 ++ .../ca/allanwang/kau/animators/NoAnimator.kt | 41 ++ .../kau/animators/SlideUpAlphaAnimator.kt | 52 ++ .../ca/allanwang/kau/searchview/SearchView.kt | 4 +- .../src/main/res/layout/kau_about_item_library.xml | 2 +- 11 files changed, 970 insertions(+), 26 deletions(-) create mode 100644 library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt create mode 100644 library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java create mode 100644 library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt create mode 100644 library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt create mode 100644 library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt (limited to 'library/src') diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt index f469028..4690718 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt @@ -1,20 +1,18 @@ package ca.allanwang.kau.about import android.os.Bundle -import android.os.PersistableBundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.RecyclerView import android.support.v7.widget.Toolbar import ca.allanwang.kau.R import ca.allanwang.kau.adapters.ChainedAdapters -import ca.allanwang.kau.logging.KL +import ca.allanwang.kau.adapters.SectionAdapter +import ca.allanwang.kau.animators.SlideUpAlphaAnimator import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.string import ca.allanwang.kau.views.KauTextSlider import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library -import com.mikepenz.fastadapter.adapters.HeaderAdapter -import com.mikepenz.fastadapter.adapters.ItemAdapter import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread @@ -38,17 +36,14 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() { val toolbar: Toolbar by bindView(R.id.kau_toolbar) val toolbarText: KauTextSlider by bindView(R.id.kau_toolbar_text) val recycler: RecyclerView by bindView(R.id.kau_recycler) - val libSection: Pair> by lazy { string(R.string.kau_dependencies_used) to HeaderAdapter() } + val libSection: Pair> by lazy { string(R.string.kau_dependencies_used) to SectionAdapter() } val sectionsChain: ChainedAdapters = ChainedAdapters() fun addLibsAsync() { doAsync { val libs = Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields)) val items = getLibraries(libs) - uiThread { - KL.e("Coun ${items.count()}") - libSection.second.add(items.map { LibraryItem(it) }) - } + uiThread { libSection.second.add(items.map { LibraryItem(it) }) } } } @@ -69,12 +64,15 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() { if (dy > 0) toolbarText.setNextText(item) else toolbarText.setPrevText() } + recycler.itemAnimator = SlideUpAlphaAnimator() toolbarText.setCurrentText(sectionsChain[0].first) + onPostCreate() + addLibsAsync() } - override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - super.onPostCreate(savedInstanceState, persistentState) - addLibsAsync() + + open fun onPostCreate() { + } /** @@ -82,5 +80,5 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() { * The adapters should be listed in the order that they appear, * and if the [libSection] shouldn't be at the end, it should be added in this list */ - open fun onCreateSections(): List>> = listOf(libSection) + open fun onCreateSections(): List>> = listOf(libSection) } \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt index 560eb82..0dd4973 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt @@ -54,6 +54,14 @@ class LibraryItem(val lib: Library) : AbstractItem Unit) : AbstractItem?) { super.bindView(holder, payloads) - with (holder) { + with(holder) { title.text = configs.title creator.text = configs.author description.text = configs.description @@ -44,6 +44,15 @@ class MainItem(builder: Config.() -> Unit) : AbstractItem(vararg items: Pair>) { - private val chain: MutableList>> = mutableListOf(*items) +class ChainedAdapters(vararg items: Pair>) { + private val chain: MutableList>> = mutableListOf(*items) val baseAdapter: FastItemAdapter> = FastItemAdapter() private val indexStack = Stack() var recycler: RecyclerView? = null val firstVisibleItemPosition: Int get() = (recycler?.layoutManager as LinearLayoutManager?)?.findFirstVisibleItemPosition() ?: throw IllegalArgumentException("No recyclerview was bounded to the chain adapters") - fun add(vararg items: Pair>) = add(items.toList()) + fun add(vararg items: Pair>) = add(items.toList()) - fun add(items: Collection>>): ChainedAdapters { + fun add(items: Collection>>): ChainedAdapters { if (recycler != null) throw IllegalAccessException("Chain adapter is already bounded to a recycler; cannot add directly.") + items.map { it.second }.forEachIndexed { index, sectionAdapter -> sectionAdapter.sectionOrder = chain.size + 1 + index } chain.addAll(items) return this } @@ -51,7 +51,6 @@ class ChainedAdapters(vararg items: Pair>) { chain.map { it.second }.forEachReversedWithIndex { i, headerAdapter -> if (i == chain.size - 1) headerAdapter.wrap(baseAdapter) else headerAdapter.wrap(chain[i + 1].second) - if (i < chain.size - 1) KL.e("${chain[i].first} ${chain[i + 1].first}") } recycler = recyclerView indexStack.push(0) @@ -68,10 +67,8 @@ class ChainedAdapters(vararg items: Pair>) { val nextAdapterIndex = (currentAdapterIndex until chain.size).asSequence() .firstOrNull { val adapter = chain[it].second - KL.e("A $it ${adapter.adapterItemCount} ${adapter.getGlobalPosition(0)} $topPosition $currentAdapterIndex") - adapter.adapterItemCount > 0 && adapter.getGlobalPosition(0) >= topPosition + adapter.adapterItemCount > 0 && adapter.getGlobalPosition(adapter.adapterItemCount - 1) >= topPosition } ?: currentAdapterIndex - KL.e("Next $nextAdapterIndex") if (nextAdapterIndex == currentAdapterIndex) return indexStack.push(nextAdapterIndex) onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy) diff --git a/library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt b/library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt new file mode 100644 index 0000000..cf7205a --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt @@ -0,0 +1,13 @@ +package ca.allanwang.kau.adapters + +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter.adapters.HeaderAdapter + +/** + * Created by Allan Wang on 2017-06-27. + * + * Extension of [HeaderAdapter] where we can define the order + */ +class SectionAdapter>(var sectionOrder: Int = 100) : HeaderAdapter() { + override fun getOrder(): Int = sectionOrder +} \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java b/library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java new file mode 100644 index 0000000..f25678f --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java @@ -0,0 +1,764 @@ +package ca.allanwang.kau.animators; + +/* + * Created by Allan Wang on 2017-06-27. + * + * Based on Item Animator by {@author Mike Penz} + * Rewritten to match with the updated compat dependencies + */ +/* + * 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. + */ + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.SimpleItemAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * This implementation of {@link android.support.v7.widget.RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. + * + * @see android.support.v7.widget.RecyclerView#setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator) + */ +public abstract class BaseItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private static TimeInterpolator sDefaultInterpolator; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + ArrayList> mAdditionsList = new ArrayList<>(); + ArrayList> mMovesList = new ArrayList<>(); + ArrayList> mChangesList = new ArrayList<>(); + + ArrayList mAddAnimations = new ArrayList<>(); + ArrayList mMoveAnimations = new ArrayList<>(); + ArrayList mRemoveAnimations = new ArrayList<>(); + ArrayList mChangeAnimations = new ArrayList<>(); + + Interpolator interpolator; + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + 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; + } + } + + public 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; + } + + 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, 0); + } 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; + long moveDuration = movesPending ? getMoveDuration() : 0; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDelay(getRemoveDuration(), moveDuration, getChangeDuration())); + } 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() { + @Override + 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; + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, getAddDelay(removeDuration, moveDuration, changeDuration)); + } else { + adder.run(); + } + } + } + + /** + * used to calculated the delay until the remove animation should start + * + * @param remove the remove duration + * @param move the move duration + * @param change the change duration + * @return the calculated delay for the remove items animation + */ + public long getRemoveDelay(long remove, long move, long change) { + return remove + Math.max(move, change); + } + + /** + * used to calculated the delay until the add animation should start + * + * @param remove the remove duration + * @param move the move duration + * @param change the change duration + * @return the calculated delay for the add items animation + */ + public long getAddDelay(long remove, long move, long change) { + return remove + Math.max(move, change); + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final ViewPropertyAnimator animation = removeAnimation(holder); + mRemoveAnimations.add(holder); + animation.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + removeAnimationCleanup(holder); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + abstract public ViewPropertyAnimator removeAnimation(ViewHolder holder); + + abstract public void removeAnimationCleanup(ViewHolder holder); + + @Override + public boolean animateAdd(final ViewHolder holder) { + resetAnimation(holder); + addAnimationPrepare(holder); + mPendingAdditions.add(holder); + return true; + } + + void animateAddImpl(final ViewHolder holder) { + final ViewPropertyAnimator animation = addAnimation(holder); + mAddAnimations.add(holder); + animation.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + addAnimationCleanup(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + addAnimationCleanup(holder); + } + }).start(); + } + + /** + * the animation to prepare the view before the add animation is run + * + * @param holder + */ + public abstract void addAnimationPrepare(ViewHolder holder); + + /** + * the animation for adding a view + * + * @param holder + * @return + */ + public abstract ViewPropertyAnimator addAnimation(ViewHolder holder); + + /** + * the cleanup method if the animation needs to be stopped. and tro prepare for the next view + * + * @param holder + */ + abstract void addAnimationCleanup(ViewHolder holder); + + @Override + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += (int) holder.itemView.getTranslationX(); + fromY += (int) holder.itemView.getTranslationY(); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + view.setTranslationX(-deltaX); + } + if (deltaY != 0) { + view.setTranslationY(-deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + 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) { + view.animate().translationX(0); + } + if (deltaY != 0) { + view.animate().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 ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + 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); + } + changeAnimation(oldHolder, newHolder, + fromX, fromY, toX, toY); + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + 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 ViewPropertyAnimator oldViewAnim = changeOldAnimation(holder, changeInfo); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(Animator animator) { + oldViewAnim.setListener(null); + changeAnimationCleanup(holder); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimator newViewAnimation = changeNewAnimation(newHolder); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + + @Override + public void onAnimationEnd(Animator animator) { + newViewAnimation.setListener(null); + changeAnimationCleanup(newHolder); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + /** + * the whole change animation if we have to cross animate two views + * + * @param oldHolder + * @param newHolder + * @param fromX + * @param fromY + * @param toX + * @param toY + */ + public void changeAnimation(ViewHolder oldHolder, ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevValue = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + oldHolder.itemView.setTranslationX(prevTranslationX); + oldHolder.itemView.setTranslationY(prevTranslationY); + + oldHolder.itemView.setAlpha(prevValue); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + newHolder.itemView.setTranslationX(-deltaX); + newHolder.itemView.setTranslationY(-deltaY); + newHolder.itemView.setAlpha(0); + } + } + + /** + * the animation for removing the old view + * + * @param holder + * @return + */ + public abstract ViewPropertyAnimator changeOldAnimation(ViewHolder holder, ChangeInfo changeInfo); + + /** + * the animation for changing the new view + * + * @param holder + * @return + */ + public abstract ViewPropertyAnimator changeNewAnimation(ViewHolder holder); + + /** + * the cleanup method if the animation needs to be stopped. and tro prepare for the next view + * + * @param holder + */ + public abstract void changeAnimationCleanup(ViewHolder holder); + + 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; + } + changeAnimationCleanup(item); + item.itemView.setTranslationX(0); + item.itemView.setTranslationY(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. + view.animate().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) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + removeAnimationCleanup(item); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + addAnimationCleanup(item); + 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) { + view.setTranslationY(0); + view.setTranslationX(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)) { + addAnimationCleanup(item); + 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) { + + if (sDefaultInterpolator == null) { + sDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + holder.itemView.animate().setInterpolator(sDefaultInterpolator); + 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. + */ + 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; + view.setTranslationY(0); + view.setTranslationX(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); + addAnimationCleanup(item); + 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; + view.setTranslationY(0); + view.setTranslationX(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; + addAnimationCleanup(item); + 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--) { + viewHolders.get(i).itemView.animate().cancel(); + } + } + + /** + * {@inheritDoc} + *

+ * If the payload list is not empty, DefaultItemAnimator returns true. + * When this is the case: + *

    + *
  • If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both + * ViewHolder arguments will be the same instance. + *
  • + *
  • + * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, + * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and + * run a move animation instead. + *
  • + *
+ */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } +} diff --git a/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt new file mode 100644 index 0000000..abb22c8 --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt @@ -0,0 +1,62 @@ +package ca.allanwang.kau.animators + +import android.support.v7.widget.RecyclerView +import android.view.ViewPropertyAnimator + +/** + * Created by Allan Wang on 2017-06-27. + */ +open class DefaultAnimator : BaseItemAnimator() { + override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate().apply { + alpha(0f) + duration = this@DefaultAnimator.removeDuration + interpolator = this@DefaultAnimator.interpolator + } + } + + override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 1f + } + + override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 0f + } + + override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate().apply { + alpha(1f) + duration = this@DefaultAnimator.addDuration + interpolator = this@DefaultAnimator.interpolator + } + } + + override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 1f + } + + override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator { + return holder.itemView.animate().apply { + alpha(0f) + translationX(changeInfo.toX.toFloat() - changeInfo.fromX) + translationY(changeInfo.toY.toFloat() - changeInfo.fromY) + duration = this@DefaultAnimator.changeDuration + interpolator = this@DefaultAnimator.interpolator + } + } + + override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate().apply { + alpha(1f) + translationX(0f) + translationY(0f) + duration = this@DefaultAnimator.changeDuration + interpolator = this@DefaultAnimator.interpolator + } + } + + override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) { + holder.itemView.alpha = 1f + } + +} \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt new file mode 100644 index 0000000..244287b --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt @@ -0,0 +1,41 @@ +package ca.allanwang.kau.animators + +import android.support.v7.widget.RecyclerView +import android.view.ViewPropertyAnimator + +/** + * Created by Allan Wang on 2017-06-27. + * + * Truly have no animation + */ +class NoAnimator : DefaultAnimator() { + override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {} + + override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 } + + override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {} + + override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 } + + override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 } + + override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {} + + override fun changeAnimation(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int) {} + + override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0 + + override fun getAddDuration(): Long = 0 + + override fun getMoveDuration(): Long = 0 + + override fun getRemoveDuration(): Long = 0 + + override fun getChangeDuration(): Long = 0 + + override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0 + + override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 } + + override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {} +} \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt new file mode 100644 index 0000000..3b1b223 --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt @@ -0,0 +1,52 @@ +package ca.allanwang.kau.animators + +import android.support.v7.widget.RecyclerView +import android.view.ViewPropertyAnimator + +/** + * Created by Allan Wang on 2017-06-27. + */ +class SlideUpAlphaAnimator : DefaultAnimator() { + override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) { + with(holder.itemView) { + translationY = height.toFloat() + alpha = 0f + } + } + + override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate().apply { + translationY(0f) + alpha(0f) + duration = this@SlideUpAlphaAnimator.addDuration + interpolator = this@SlideUpAlphaAnimator.interpolator + } + } + + public override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) { + with(holder.itemView) { + translationY = 0f + alpha = 1f + } + } + + override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0 + + override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0 + + override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator { + return holder.itemView.animate().apply { + duration = this@SlideUpAlphaAnimator.removeDuration + alpha(0f) + translationY(holder.itemView.height.toFloat()) + interpolator = this@SlideUpAlphaAnimator.interpolator + } + } + + override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) { + with(holder.itemView) { + translationY = 0f + alpha = 1f + } + } +} \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt index 29c2223..b2490ac 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt @@ -11,13 +11,13 @@ import android.support.transition.AutoTransition import android.support.v7.widget.AppCompatEditText import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView -import android.support.v7.widget.SimpleItemAnimator import android.util.AttributeSet import android.view.* import android.widget.FrameLayout import android.widget.ImageView import android.widget.ProgressBar import ca.allanwang.kau.R +import ca.allanwang.kau.animators.NoAnimator import ca.allanwang.kau.kotlin.nonReadable import ca.allanwang.kau.searchview.SearchView.Configs import ca.allanwang.kau.utils.* @@ -247,7 +247,7 @@ class SearchView @JvmOverloads constructor( } }) adapter = this@SearchView.adapter - (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false //clear the fade between item changes + itemAnimator = NoAnimator() } with(adapter) { withSelectable(true) diff --git a/library/src/main/res/layout/kau_about_item_library.xml b/library/src/main/res/layout/kau_about_item_library.xml index 5730d67..0ae4d3c 100644 --- a/library/src/main/res/layout/kau_about_item_library.xml +++ b/library/src/main/res/layout/kau_about_item_library.xml @@ -1,7 +1,7 @@