Note * The following are all from Google's official custom Layout Manager documents, which were sorted out during the learning process.
The original address is: http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
Thank you for the mistakes.
In the previous article, we discussed adding appropriate support for data set changes and target scrolling. In this part of this series, we will focus on how to correctly support animation in Layout Manager
Last time we discussed notify DataSetChanged (), but you may have noticed that changing data in this way does not animate the changes. RecyclerView contains a new API for animation changes that requires you to notify the adapter of which locations have changed and what operations are:
- notifyItemInserted() and notifyItemRangeInserted(): Insert a new item at the specified location
- NotfyItemChanged () and notifyItemRangeChanged (): Invalidates entries in a given location and makes no structural changes in the data set.
- Notify Item Remove () and notify ItemRangeRemove (): Delete the item at the specified location
- Notify ItemMoved (): Move to the target location
By default, when using these methods, LayoutManager can have simple animations based solely on whether each current view location still appears in the layout after being changed. New views are faded in, deleted views are faded out, and other views are moved to their new locations. (There should be a motion map that can be seen but suddenly does not show up. Let's go to the official website and see it for ourselves: Part 3 Figure 1. Default Simple Item Animations)
Predictive Item Animations
The following animation describes what should happen when deleting an item: (Part 3 Figure 2. Removal Animation Concept)
In particular, the items on the left must slide upwards, while the items on the right must slide upwards to fill in the blanks in the previous row. You can imagine that the opposite is true for items added to this location.
As we discussed in the first article in this series, RecyclerView typically calls OnlayoutChildred(), Predictive Item Animations only once when the initial layout or data set size (i.e., item count) changes.
Functions allow us to provide a more meaningful description of how views can be transformed according to changes in data. We need to first point out to the Framework that our LayoutManager can provide these additional data:
@Override public boolean supportsPredictiveItemAnimations() { return true;}
With this change, onLayoutChildren() will be called twice for each batch of data set changes -- the first as a "pre-layout" phase and the second for actual layout.
What Should I Do During Pre-Layout?
In the pre-layout phase of onLayoutChildren(), the layout logic should be run to set the initial conditions for changing the animation. This means that you need to list all the views that are currently visible before the change, and any other views that you know will be visible after the animation is run (these are called emergent views). These additional views should be placed outside the screen where users want them to come from. Framework captures these locations and uses them to animate the new view to the appropriate location, rather than simply fade in.
RecyclerView.State.isPreLayout () This method can check which layout stage we are in.
In the Fixed Grid Layout Manager example, we used pre-layout to determine how many visible views were deleted due to data set changes. In the pre-layout, the removed views will still be returned from the recycler, so they can be placed in their original location without worrying about the occupied position. To indicate future deletions, LayoutParams. isViewRemove () returns true for a given view. Our example calculates the number of deleted views, so that we can get a rough idea of how much space the resulting views will fill.
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... SparseIntArray removedCache = null; /* * During pre-layout, you need to pay attention to which itrm s are deleted */ if (state.isPreLayout()) { removedCache = new SparseIntArray(getChildCount()); for (int i=0; i < getChildCount(); i++) { final View view = getChildAt(i); LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.isItemRemoved()) { //Track these deleted views as visible removedCache.put(lp.getViewPosition(), REMOVE_VISIBLE); } } ... } ... //Fill the grid for the initial layout of views fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state.isPreLayout(), removedCache); ...}
During the pre-layout period, RecyclerView attempts to map the position of the view's Adapter to their "old" location (meaning before the data set changes). When you request a view by location, you want that location to be the initial location of the view. Be careful not to try to switch them between pre-layout and "real" layout on your own.
The last change in the example is a modification to fillGrid(), where we will try to list the "N" additional views (each column) as the occurrence views, where N is the number of deleted visible views. These views are always filled from the right when deleted, so they are computed as the last visible column:
private void fillGrid(int direction,
int emptyLeft,
int emptyTop,
RecyclerView.Recycler recycler,
boolean preLayout,
SparseIntArray removedPositions) {
...
for (int i = 0; i < getVisibleChildCount(); i++) { int nextPosition = positionOfIndex(i); ... if (i % mVisibleColumnCount == (mVisibleColumnCount - 1)) { leftOffset = startLeftOffset; topOffset += mDecoratedChildHeight; //In the pre-layout process, at the end of each column, use the additional view displayed if (preLayout) { layoutAppearingViews(recycler, view, nextPosition, removedPositions.size(), ...); } } else { leftOffset += mDecoratedChildWidth; } } ...}private void layoutAppearingViews(RecyclerView.Recycler recycler, View referenceView, int referencePosition, int extraCount, int offset) { //Nothing to do... if (extraCount < 1) return; for (int extra = 1; extra <= extraCount; extra++) { //Grab the next position after the reference final int extraPosition = referencePosition + extra; if (extraPosition < 0 || extraPosition >= getItemCount()) { //Can't do anything with this continue; } /* * Get the other position views we want to display as part of the animation */ View appearing = recycler.getViewForPosition(extraPosition); addView(appearing); //Find Layout Increments final int newRow = getGlobalRowOfPosition(extraPosition + offset); final int rowDelta = newRow - getGlobalRowOfPosition(referencePosition + offset); final int newCol = getGlobalColumnOfPosition(extraPosition + offset); final int colDelta = newCol - getGlobalColumnOfPosition(referencePosition + offset); layoutTempChildView(appearing, rowDelta, colDelta, referenceView); }}
In layoutAppearingViews()
Each additional view that appears in the method is in its "global" position (that is, the row/column position it will occupy in the grid). This location is not on the screen, but it provides Framwork with the data it needs to generate the starting point of the animation to insert these views into it.
Changes for the "Real" Layout
In Part 1, we have discussed what we should do in the layout process, but we have to adjust the formula a little and add animation support. Another step is to determine if there are any vanishing views. In our example, this is done by running a normal layout traversal and then determining whether there are views in Recycler scrap.
We can use detachAndScrapAttachedViews() before starting each layout.
Views that are not considered deleted are still in Scrap, so we need to put them out of the screen so that the animation system can slide them out of the view (instead of fading them out).
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... if (!state.isPreLayout() && !recycler.getScrapList().isEmpty()) { final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList(); final HashSet<View> disappearingViews = new HashSet<View>(scrapList.size()); for (RecyclerView.ViewHolder holder : scrapList) { final View child = holder.itemView; final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isItemRemoved()) { disappearingViews.add(child); } } for (View child : disappearingViews) { layoutDisappearingView(child); } }}private void layoutDisappearingView(View disappearingChild) { addDisappearingView(disappearingChild); //Adjust each vanished view to its proper position final LayoutParams lp = (LayoutParams) disappearingChild.getLayoutParams(); final int newRow = getGlobalRowOfPosition(lp.getViewPosition()); final int rowDelta = newRow - lp.row; final int newCol = getGlobalColumnOfPosition(lp.getViewPosition()); final int colDelta = newCol - lp.column; layoutTempChildView(disappearingChild, rowDelta, colDelta, disappearingChild);}
Similar to the code where views appear, layoutDisappearingView() places each remaining view in its "global" position as the final layout location. This provides Framework with the information needed to slide these views out of the right direction during animation.
The following images should help visualize the Fixed Grid Layout Manager example: (Figure 3. Simple Remove Animation)
- Black boxes represent the visible boundaries of recyclable views.
- Red view: Items deleted from the data set.
- Green View (Appearance View): Not the original view, but the view that is laid out outside the screen during pre-layout.
- Purple View (Vanishing View): Place them initially in their original position during pre-layout, then lay them out outside the screen during the "actual" layout phase.
Reacting to Off-Screen Changes
As you may have noticed, in the last section we determined that the ability to delete changes depends on the visual view. What if changes occur outside the visible range? Depending on your layout structure, such changes may still require you to adjust the layout for a better animation experience.
You can rewrite onItemsRemoved (),* onItemsMoved (), onItemsAdded (), onItemsChanged ()* to respond to these events. These methods give the location and scope of the changes. Even if they occur within the scope of the view that the current layout does not reflect.
When the removal range appears outside the visible area, onitemremove() is called before the pre-layout. This allows us to collect data about changes we may need to best support any view changes that may arise from this event.
In the following example, these deletion operations are collected in the same way as before, but they are marked with different types.
@Override public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { mFirstChangedPosition = positionStart; mChangedPositionCount = itemCount;}@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... SparseIntArray removedCache = null; if (state.isPreLayout()) { ... //Delete views beyond the screen if (removedCache.size() == 0 && mChangedPositionCount > 0) { for (int i = mFirstChangedPosition;i < (mFirstChangedPosition + mChangedPositionCount); i++) { removedCache.put(i, REMOVE_INVISIBLE); } } } ... fillGrid(DIRECTION_NONE, childLeft, childTop, recycler, state.isPreLayout(), removedCache); ...}
This method is called when the deleted item is visible. However, in this case, it is invoked after the pre-layout. That's why our example still collects data from visible deleted views.
With all this in place, we can run the sample application again. We can see that the disappearing item on the left slides out and rejoins at the end of the previous line. New projects that appear on the right will slide appropriately along the existing grid to the right. Now, the only faded view in our new animation is the actually deleted view! (Figure 4. Predictive Removal Animation)