文章开始前,我觉得有必要说一下为什么要使用动画。 2008年那会儿,Android用户就如一个22岁的玉女般 只要用一束鲜花就能让她感动一个星期。虽然那时候的Android系统就如22岁的小伙子房子 车子都没有 只有一股子真情。但是少女不会有任何的抱怨和奢求。但是现在已经是2016年,Android用户已经从玉女变身为欲女。光凭一腔热情已经难以满足他们生理和心里上的需求了。一次我们要想尽一切办法来讨用户的欢心。 恰恰动画就对用户来说就非常的受用,因此我们作为开发者必须学会甚至精通动画的实现和原理
~~~~~~~~~~~~~~~~~~~~~~~~~~~~请别再说废话!~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
回归主题,我们首先来看一下RecyclerView动画的核心执行类ItemAnimator,当继承一个ItemAnimator时,有如下几个方法需要被实现:
@Override public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) { return false; } @Override public boolean animateAppearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) { return false; } @Override public boolean animatePersistence(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) { return false; } @Override public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) { return false; } @Override public void runPendingAnimations() { } @Override public void endAnimation(RecyclerView.ViewHolder item) { } @Override public void endAnimations() { } @Override public boolean isRunning() { return false; }
通过实现以上几个接口方法就能实现不同的动画效果,以上几个方法有4个方法比较重要
animateDisappearance
当RecyclerView中的item在屏幕上由可见变为不可见时调用此方法
animateAppearance
当RecyclerView中的item显示到屏幕上时调用此方法
animateChange
当RecyclerView中的item状态发生改变时调用此方法(notifyItemChanged(position))
runPendingAnimations
统筹RecyclerView中所有的动画,统一启动执行
然而以上几个接口方法在RecyclerView执行动画时都是何时被调用的呢??
带着疑问,来看下RecyclerView动画触发流程,
以添加一个item为例,当我们在RecyclerView中新加一个item时,调用一下代码:
public void addItem(RecyclerModel model) { models.add(model); notifyItemInserted(models.size()); }
/** * Notify any registered observers that the item reflected at <code>position</code> * has been newly inserted. The item previously at <code>position</code> is now at * position <code>position + 1</code>. * * <p>This is a structural change event. Representations of other existing items in the * data set are still considered up to date and will not be rebound, though their * positions may be altered.</p> * * @param position Position of the newly inserted item in the data set * * @see #notifyItemRangeInserted(int, int) */ public final void notifyItemInserted(int position) { mObservable.notifyItemRangeInserted(position, 1); }
public void notifyItemRangeInserted(int positionStart, int itemCount) { // since onItemRangeInserted() is implemented by the app, it could do anything, // including removing itself from {@link mObservers} - and that could cause problems if // an iterator is used on the ArrayList {@link mObservers}. // to avoid such problems, just march thru the list in the reverse order. for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeInserted(positionStart, itemCount); } }for循环遍历了一个mObservers,并调用每一个Observer的onItemRangeInserted方法。 那这个mObservers是Observalbe中的一个全局集合变量,当调用RecyclerView.setAdapter(Adapter)时,会调用adapter.RegisterAadpterDataPbserver(mObserver)方法给Adapter对象添加一个观察者对象Observer。此Observer是RecyclerViewDataObserver类型对象,继续跟踪代码,进入RecyclerViewDataObserver的onItemRangeInserted方法,代码如下:
@Override public void onItemRangeInserted(int positionStart, int itemCount) { assertNotInLayoutOrScroll(null); if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { triggerUpdateProcessor(); } }
首先,调用一个mAdapterHelper的onItemRangeChaged方法将所要进行的操作(比如ADD)保存在AdapterHelper内部的一个集合中,如果此方法返回true,则继续调用triggerUpdateProcessor方法, 代码go on!
void triggerUpdateProcessor() { if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) { ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); } else { mAdapterUpdateDuringMeasure = true; requestLayout(); } }
/** * Note: this Runnable is only ever posted if: * 1) We've been through first layout * 2) We know we have a fixed size (mHasFixedSize) * 3) We're attached */ private final Runnable mUpdateChildViewsRunnable = new Runnable() { public void run() { if (!mFirstLayoutComplete || isLayoutRequested()) { // a layout request will happen, we should not do layout here. return; } if (mLayoutFrozen) { mLayoutRequestEaten = true; return; //we'll process updates when ice age ends. } consumePendingUpdateOperations(); } };
/** * Helper method reflect data changes to the state. * <p> * Adapter changes during a scroll may trigger a crash because scroll assumes no data change * but data actually changed. * <p> * This method consumes all deferred changes to avoid that case. */ private void consumePendingUpdateOperations() { if (!mFirstLayoutComplete) { // a layout request will happen, we should not do layout here. return; } if (mDataSetHasChangedAfterLayout) { TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); dispatchLayout(); TraceCompat.endSection(); return; } if (!mAdapterHelper.hasPendingUpdates()) { return; } // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any // of the visible items is affected and if not, just ignore the change. if (mAdapterHelper.hasAnyUpdateTypes(UpdateOp.UPDATE) && !mAdapterHelper .hasAnyUpdateTypes(UpdateOp.ADD | UpdateOp.REMOVE | UpdateOp.MOVE)) { TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); eatRequestLayout(); mAdapterHelper.preProcess(); if (!mLayoutRequestEaten) { if (hasUpdatedView()) { dispatchLayout(); } else { // no need to layout, clean state mAdapterHelper.consumePostponedUpdates(); } } resumeRequestLayout(true); TraceCompat.endSection(); } else if (mAdapterHelper.hasPendingUpdates()) { TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); dispatchLayout(); TraceCompat.endSection(); } }
在consumePendingUpdateOperations中,代码比较多,但是比较重要的是代码第16行 dispatchLayout(), 由名字也能知道,此方法的目的是重新布局RecyclerView的子控件,分三步进行布局 依次是:
dispatchLayoutStep1()
记录重新调用onLayout之前,用户操作的View的相关信息(getLeft(), getRight(), getTop(), getBottom()等)
dispatchLayoutStep2()
进行真正意义上的layout工作,只是调用onLayoutChildren方法递归摆放子控件的位置,此方法可能会被调用多次
dispatchLayoutStep3()
当dispatchLayoutStep2()进行完layout操作之后,RecyclerView在此方法中记录下重新布局之后,用户所操作View的相关信息(同样是getLeft(), getRight(), getTop(), getBottom())
代码具体如下:
void dispatchLayout() { if (mAdapter == null) { Log.e(TAG, "No adapter attached; skipping layout"); // leave the state in START return; } if (mLayout == null) { Log.e(TAG, "No layout manager attached; skipping layout"); // leave the state in START return; } mState.mIsMeasuring = false; if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) { // First 2 steps are done in onMeasure but looks like we have to run again due to // changed size. mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { // always make sure we sync them (to ensure mode is exact) mLayout.setExactMeasureSpecsFrom(this); } <span style="color:#ff0000;">dispatchLayoutStep3();</span> }此方法官方API对它做了如下解释:
专门用来处理由于layout布局改变而触发的animation动画, 动画总共有5种类型:
* PERSISTENT: items在layout布局前和布局后都是可见状态 * REMOVED: items在重新布局前可见,重新布局后不可见 * ADDED: items在重新布局前不存在,当重新布局后由APP添加到视图当中 * DISAPPEARING: items在适配器中的数据源中存在但是在屏幕上是由可见状态变为不可见状态, 一般是一个item被滑动出屏幕时的动画 * APPEARING: items跟DISAPPEARING正好相反的状态首先来看下dispatchLayoutStep1() :
private void dispatchLayoutStep1() {
<span style="white-space:pre"> </span>... if (mState.mRunSimpleAnimations) { // Step 0: Find out where all non-removed items are, pre-layout int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) { continue; }
final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo);
}
<span style="white-space:pre"> </span>...
}如红色字体所示mItemAnimator!! 仿佛终于看到希望了 哈哈。 mItemAnimator.recordPreLayoutInfomation方法记录了在重新布局之前所操作的View的信息,并将信息都封装在一个ItemHolderInfo的对象当中,代码比较简单 如下所示:
public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state, @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags, @NonNull List<Object> payloads) { return obtainHolderInfo().setFrom(viewHolder); }继续查看obtainHolderInfo()方法:
public ItemHolderInfo obtainHolderInfo() { return new ItemHolderInfo(); }就是简单的通过new ItemHolderInfo的方式返回了一个ItemHolderInfo对象,获取了这个ItemHolderInfo对象之后,会通过setFrom方法传入一个ViewHolder对象,并将ViewHolder对象中的itemView的getLeft getTop getRgith getBottom依次赋值给ItemHolderInfo的left top right bottom变量。 最后这个ItemHolderInfo被添加到一个ViewInfoStore的对象中进行保存。 到这里dispatchLayoutStep1该做的事基本已经完成。dispatchLayoutStep2()就是单纯的调用onLayoutChildren方法布局子控件,这里不做过多叙述,直接看dispatchLayoutStep3()方法, 代码如下:
/** * The final step of the layout where we save the information about views for animations, * trigger animations and do any necessary cleanup. */ private void dispatchLayoutStep3() { mState.assertLayoutStep(State.STEP_ANIMATIONS); eatRequestLayout(); onEnterLayoutOrScroll(); mState.mLayoutStep = State.STEP_START; if (mState.mRunSimpleAnimations) { // Step 3: Find out where things are now, and process change animations. // traverse list in reverse because we may call animateChange in the loop which may // remove the target view holder. for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore()) { continue; } long key = getChangedHolderKey(holder); final ItemHolderInfo animationInfo = mItemAnimator .recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { // run a change animation // If an Item is CHANGED but the updated version is disappearing, it creates // a conflicting case. // Since a view that is marked as disappearing is likely to be going out of // bounds, we run a change animation. Both views will be cleaned automatically // once their animations finish. // On the other hand, if it is the same view holder instance, we run a // disappearing animation instead because we are not going to rebind the updated // VH unless it is enforced by the layout manager. final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); if (oldDisappearing && oldChangeViewHolder == holder) { // run disappear animation instead of change mViewInfoStore.addToPostLayout(holder, animationInfo); } else { final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( oldChangeViewHolder); // we add and remove so that any post info is merged. mViewInfoStore.addToPostLayout(holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); if (preInfo == null) { handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); } else { animateChange(oldChangeViewHolder, holder, preInfo, postInfo, oldDisappearing, newDisappearing); } } } else { mViewInfoStore.addToPostLayout(holder, animationInfo); } } // Step 4: Process view info lists and trigger animations mViewInfoStore.process(mViewInfoProcessCallback); }
首先还是获取ItemHolderInfo,但是此时的ItemHolderInfo中保存的是在layout重新布局之后View的相关信息,然后将此ItemHolderInfo也保存在ViewInfoStore对象中。
到这步为止,添加一个item的前后的View信息都已经被保存在ViewInfoStore对象当中,有了这两个view信息之后,我们就有了自定义动画的起始值^_^
最后调用ViewInfoStore的process方法真正的执行处理操作,并传入mViewInfoProcessCallback回调接口, 代码如下:
void process(ProcessCallback callback) { for (int index = mLayoutHolderMap.size() - 1; index >= 0; index --) { final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); final InfoRecord record = mLayoutHolderMap.removeAt(index); if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { // Appeared then disappeared. Not useful for animations. callback.unused(viewHolder); } else if ((record.flags & FLAG_DISAPPEARED) != 0) { // Set as "disappeared" by the LayoutManager (addDisappearingView) if (record.preInfo == null) { // similar to appear disappear but happened between different layout passes. // this can happen when the layout manager is using auto-measure callback.unused(viewHolder); } else { callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); } } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { // Appeared in the layout but not in the adapter (e.g. entered the viewport) <span style="color:#ff0000;">callback.processAppeared</span>(viewHolder, record.preInfo, record.postInfo); } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { // Persistent in both passes. Animate persistence <span style="color:#ff0000;">callback.processPersistent</span>(viewHolder, record.preInfo, record.postInfo); } else if ((record.flags & FLAG_PRE) != 0) { // Was in pre-layout, never been added to post layout <span style="color:#ff0000;">callback.processDisappeared</span>(viewHolder, record.preInfo, null); } else if ((record.flags & FLAG_POST) != 0) { // Was not in pre-layout, been added to post layout <span style="color:#ff0000;">callback.processAppeared</span>(viewHolder, record.preInfo, record.postInfo); } else if ((record.flags & FLAG_APPEAR) != 0) { // Scrap view. RecyclerView will handle removing/recycling this. } else if (DEBUG) { throw new IllegalStateException("record without any reasonable flag combination:/"); } InfoRecord.recycle(record); } }通过判断InfoRecord的flags值,依次调用callback的processXXX方法,callback就是之前传进来的mViewInfoProcessCallback对象。
private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = new ViewInfoStore.ProcessCallback() { @Override public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, @Nullable ItemHolderInfo postInfo) { mRecycler.unscrapView(viewHolder); animateDisappearance(viewHolder, info, postInfo); } @Override public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) { animateAppearance(viewHolder, preInfo, info); } @Override public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { viewHolder.setIsRecyclable(false); if (mDataSetHasChangedAfterLayout) { // since it was rebound, use change instead as we'll be mapping them from // stable ids. If stable ids were false, we would not be running any // animations if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) { postAnimationRunner(); } } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { postAnimationRunner(); } } @Override public void unused(ViewHolder viewHolder) { mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); } };Finally ! 革命终于成功。
animateChange、animateDisappearance、animateAppearance、postAnimationRunner中依次调用了ItemAnimator的animateChange、animateDisappearance、animateAppearance、runPendingAnimations方法
这几个方法就是ItemAnimator暴露的接口方法。在这三个方法中可以通过不同的逻辑实现不同的动画效果。
animateChange方法中传入了layout之前的ViewHolder和layout之后的ViewHolder对象,通过这两个ViewHolder对象获取其中的itemView进行动画效果
animateAppearance和animateDisappearance中都是传入的layout之后的ViewHolder对象,Android OS官方API给我们的说法是可以在自定义的ItemAnimator中保存两个ViewHolder的集合,当调用animateAppearance和animateDisappearance时,就向集合中添加ViewHolder对象。
最后在调用runPendingAnimations时统一遍历这两个集合中所有的ViewHolder对象,并依次执行动画
下一节,我们就看一下系统给我们提供的两个继承ItemAnimator的封装类:SimpleItemAnimator和DefaultItemAnimator, 如果要实现自定义动画,一般不是直接继承ItemAnimator类,而是通过继承SimpleItemAnimator和DefaultItemAnimator,简单又实用