注*以下都是来自Google官方的自定义LayoutManager文档,在学习的过程中整理出来的。
原文地址是:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
错误的地方还请指出谢谢啦。
在前一篇文章中,我们讨论了添加对数据集更改和目标滚动的适当支持。在本系列的这一部分中,我们将重点介绍如何在LayoutManager中正确地支持动画
上次我们讨论了notifyDataSetChanged(),但是您可能已经注意到,以这种方式更改数据不会使更改具有动画效果。RecyclerView包含了一个用于进行动画更改的新API,它要求您通知适配器中的哪些位置已经更改,以及操作是什么:
- notifyItemInserted() and notifyItemRangeInserted():在指定的位置插入新项
- notifyItemChanged() and notifyItemRangeChanged():使给定位置的项无效,数据集中没有任何结构更改。
- notifyItemRemoved() and notifyItemRangeRemoved():删除指定位置的item
- notifyItemMoved():移动到目标位置
默认情况下,当使用这些方法时,LayoutManager可以有简单动画,这些动画仅仅是基于每个当前视图位置在更改后是否仍然出现在布局中。新视图被淡入,删除的视图被淡出,其他视图被移动到它们的新位置。(下面应该有个动图的原本是能看的但是突然显示不出来了,就自己去官网看吧:Part3 Figure 1. Default Simple Item Animations )
Predictive Item Animations
下面的动画描述了删除一个项目时应该发生的事情:(Part3 Figure 2. Removal Animation Concept )
特别注意,左边的项必须向上滑动,而右边的项必须向上滑动以填充前一行的空白。您可以想象添加到此位置的项的情况正好相反。
正如我们在本系列的第一篇文章中讨论的,在初始布局或数据集大小(即项计数)更改时,RecyclerView通常只调用一次OnlayoutChildred(),Predictive Item Animations
功能允许我们提供一个更有意义的描述,说明如何根据数据的变化来转换视图。我们需要首先向Framework指出,我们的LayoutManager能够提供这些附加数据:
@Override
public boolean supportsPredictiveItemAnimations() {
return true;}
有了这一个更改,对于每批数据集更改,onLayoutChildren()将被调用两次——第一次作为“预布局”阶段,第二次用于实际布局。
What Should I Do During Pre-Layout?
在onLayoutChildren()的预布局阶段,应该运行布局逻辑来设置更改动画的初始条件。这意味着,需要列出在更改之前当前可见的所有视图,以及知道在动画运行之后,将会可见的任何其他视图(这些称为出现视图)。这些额外出现的视图应该放置在屏幕外用户希望它们来自的位置。。Framework将捕捉这些位置,并使用它们将新视图动画到适当的位置,而不是进行简单的淡入。
RecyclerView.State.isPreLayout ()这个方法可以检查我们是在哪个布局阶段
在FixedGridLayoutManager示例中,我们使用预布局来确定由于数据集更改而删除了多少可见的视图。在预先布局中,已移除的视图仍然会从回收器返回,因此可以将它们放置在原来的位置,而不必担心占空位置。为了向您指示将来的删除,LayoutParams.isViewRemoved()将为给定视图返回true。我们的示例计算了已删除视图的数量,这样我们就大致了解了出现的视图将填充多少空间。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {
...
SparseIntArray removedCache = null;
/* * 在预布局期间,需要注意哪些被删除的itrm */
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()) {
//把这些删除的视图跟踪为可见
removedCache.put(lp.getViewPosition(), REMOVE_VISIBLE);
}
}
...
}
...
//Fill the grid for the initial layout of views
fillGrid(DIRECTION_NONE, childLeft, childTop,
recycler, state.isPreLayout(), removedCache);
...}
在预布局期间,RecyclerView尝试将视图的Adapter的位置映射到它们的“旧”位置(意味着在数据集更改之前)。当您按位置请求视图时,希望该位置是该项视图的初始位置。注意不要试图自己在预布局和“实际”布局之间转换它们。
示例中的最后一个更改是对fillGrid()的修改,其中我们将尝试将“N”附加视图(每列)作为出现视图列出,其中N是被删除的可见视图的数量。这些视图总是在删除时从右侧填充,因此它们被计算为最后一个可见列:
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;
//在预布局过程中,每列的末尾,使用额外的显示的view
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;
}
/* * 获取我们希望作为动画一部分显示的其他位置视图 */
View appearing = recycler.getViewForPosition(extraPosition);
addView(appearing);
//查找布局增量
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);
}}
在layoutAppearingViews()
方法中每个额外的出现视图都位于它的“全局”位置(即它将在网格中占据的行/列位置)。这个位置不在屏幕上,但是为Framwork提供了它需要的数据,以生成动画的起始点,以便将这些视图插入其中。
Changes for the “Real” Layout
在Part1中,我们已经讨论了在布局过程中应该做些什么,但是我们必须稍微调整一下公式,添加动画支持。另外一个步骤是确定是否有任何正在消失的视图。在我们的例子中,这是通过运行一个普通的布局遍历来完成的,然后确定在Recycler scrap中是否还有视图。
我们可以在开始每个布局之前使用detachAndScrapAttachedViews()。
未被认为已删除的视图仍然在Scrap中,因此我们需要将这些视图放在屏幕外的位置,这样动画系统就可以将它们滑出视图(而不是将它们淡出)。
@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);
//把每一个消失的视图调整到适当的位置
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);}
与出现视图的代码类似,layoutDisappearingView() 将每个剩余视图放在其“全局”位置作为最终布局位置。这为Framework提供了在动画期间将这些视图滑出正确方向所需的信息。
下面的图像应该有助于可视化FixedGridLayoutManager示例:(Figure 3. Simple Remove Animation )
- 黑盒子代表了可回收视图的可见边界。
- 红色视图:从数据集中删除的项。
- 绿色视图(出现视图):不是最初出现的视图,而是在预布局期间在屏幕外布局的视图。
- 紫色视图(消失视图):在预布局期间最初放置在它们的原始位置,然后在“实际”布局阶段在屏幕外进行布局。
Reacting to Off-Screen Changes
您可能已经注意到,我们在最后一节中确定删除更改的能力取决于可视视图。如果更改发生在可见范围之外怎么办?根据您的布局结构,这样的更改可能仍然需要您调整布局以获得更好的动画体验。
可以重写 onItemsRemoved(), *onItemsMoved(), onItemsAdded(),onItemsChanged()*来响应这些事件,。这些方法将给出更改的位置和范围。即使它们发生在当前布局没有反映的视图范围中。
当移除范围出现在可见区域之外时,在预布局之前调用onitemremove()。这允许我们收集关于我们可能需要的变更的数据,以便最好地支持可能由这个事件引起的任何出现的视图变更。
在下面的示例中,用前面相同的方式收集这些删除操作,但是使用不同的类型标记它们。
@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()) {
...
//删除超出屏幕外的视图
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);
...}
当删除的项可见时,将调用此方法。然而,在这种情况下,它是在预布局之后调用的。这就是为什么我们的示例仍然从可见的已删除视图中收集数据。
所有这些就绪后,我们可以再次运行示例应用程序。我们可以看到左边正在消失的项滑出,重新加入到前一行的末尾。右边出现的新项目会沿着现有的网格适当地滑动到适当的位置。现在,在我们的新动画中唯一淡出的视图是实际被删除的视图!(Figure 4. Predictive Removal Animation )