文章目录
一 前言
1.1 参考资料与预期目标
创建一个 RecyclerView LayoutManager – Part 1
创建 RecyclerView LayoutManager – Part 2
创建 RecyclerView LayoutManager – Part 3
10 steps to create a custom LayoutManager
玩转仿探探卡片式滑动效果
自定义LayoutManager简明教程
自定义控件三部曲视图篇(六)
RecyclerView.LayoutManager的常用相关方法
我用过这篇文章的开源库,但是我感觉不是特别好,
我想像岛读APP一样的效果
1.2 ItemTouchHelper与CallBack
详细看了一下玩转仿探探卡片式滑动效果,发现用到了了之前学习的CallBack。来回顾一下:ItemTouchHelper是干什么用的?它是帮助RecyclerView完成滑动和拖拽而制作的工具类
它是什么帮助你的?
//这个adapter就是RecyclerAdapter
ItemTouchHelper.Callback callback = new Card_Recycler_Callback(adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);
通过设置Callback
而且既然是用RecyclerView实现的,这篇文章就当做RecycleView的第二篇文把。
二 样式实现
参考:创建一个 RecyclerView LayoutManager – Part 1
2.1 自定义LayoutManager
2.1.1 generateDefaultLayoutParams()方法
public class CardLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams
(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
这是一旦继承RecyclerView.LayoutManager之后就需要实现的一个方法。这个方法是什么用的呢?
参考资料1:
通过读上面的注释和代码,可以知道原来平常我们调用addView的方法时,不指定LayoutParams,是通过generateDefaultLayoutParams()获取默认的LayoutParams属性的。
创建一个 RecyclerView LayoutManager – Part 1:
generateDefaultLayoutParams() 事实上你只要重写这个方法你的 LayoutManager 就能编译通过了。 实现也很简单,返回一个你想要默认应用给所有从 Recycler 中获得的子视图做参数的 RecyclerView.LayoutParams 实例。 这些参数会在对应的 getViewForPosition() 返回前赋值给相应的子视图。
2.1.2 onLayoutChildren()方法
这个方法的作用:
onLayoutChildren() 是 LayoutManager 的主入口。 它会在 view 需要初始化布局时调用, 当适配器的数据改变时(或者整个适配器被换掉时)会再次调用。 注意!这个方法不是在每次你对布局作出改变时调用的。 它是 初始化布局 或者 在数据改变时重置子视图布局的好位置。
也就是说,这个方法会在初始化布局,和adapter数据更新的时候调用。那应该些什么?怎么写呢?
参考代码 - 玩转仿探探卡片式滑动效果 布局考虑,这里就不贴完整代码了
首先学习会使用到的API,来自:创建一个 RecyclerView LayoutManager – Part 1
2.1.2.1 Detach vs. Remove
- Detach 是一个轻量的记录 view 操作。 被 detach 的视图在你的代码返回前能够重新连接。
- Remove 意味着这个 view 已经不需要了。任何被永久移除的 view 都应该 放到 Recycler 中,方便以后重用
2.1.2.2 Scrap vs. Recycle
scrap heap 和 recycle pool (垃圾堆和回收池)
- Scrap heap 是一个轻量的集合,视图可以不经过适配器直接返回给 LayoutManager 。通常被 detach但会在同一布局重新使用的视图会临时储存在这里。
当要给 LayoutManager 提供一个新 view 时,Recycler 首先会 检查 scrap heap 有没有对应的 position/id;如果有对应的内容, 就直接返回数据不需要通过适配器重新绑定。- Recycle pool 存放的 是那些假定并没有得到正确数据(相应位置的数据)的视图, 因此它们都要经过适配器重新绑定后才能返回给 LayoutManager。
当要给 LayoutManager 提供一个新 view 时,Recycler 首先会 检查 scrap heap 有没有对应的 position/id;
如果没有的话, Recycler 就会从 recycle pool 里弄一个合适的视图出来, 然后用 adapter 给它绑定必要的数据 (就是调用RecyclerView.Adapter.bindViewHolder()) 再返回。 如果 recycle pool 中也不存在有效 view ,就会在绑定数据前 创建新的 view (就是 RecyclerView.Adapter.createViewHolder()), 最后返回数据。
综上两个比较:
通常来说, 如果你想要临时整理并且希望稍后在同一布局中重新使用某个 view 的话, 可以对它调用 detachAndScrapView() 。如果基于当前布局 你不再需要某个 view 的话,对其调用 removeAndRecycleView()。
只要你原意,LayoutManager 的 API 允许你独立完成所有这些任务, 所以可能的组合有点多。通常来说, 如果你想要临时整理并且希望稍后在同一布局中重新使用某个 view 的话, 可以对它调用 detachAndScrapView() 。如果基于当前布局 你不再需要某个 view 的话,对其调用 removeAndRecycleView()。
2.1.2.3 实践1-让一个最简单的子布局显示出来
a 代码实践
照着玩转仿探探卡片式滑动效果去实现,完全失败了。根本显示不出图来。不知道为什么。
自定义控件三部曲视图篇(六)这篇文章讲得很清楚了。我怎么还是界面上一片空白呢?
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定义竖直方向的偏移量
int offsetY = 0;
Log.d(TAG, "onLayoutChildren: ItemCount is: " + getItemCount());
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
Log.d(TAG, "onLayoutChildren: the offsetY is " + offsetY + " width is " + width + " height is " + height);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
}
- 首先,我们通过
measureChildWithMargins(view, 0, 0);函数测量这个view,并且通过getDecoratedMeasuredWidth(view)得到测量出来的宽度,需要注意的是通过getDecoratedMeasuredWidth(view)得到的是item+decoration的总宽度。如果你只想得到view的测量宽度,通过view.getMeasuredWidth()就可以得到了 - 然后通过
layoutDecorated();函数将每个item摆放在对应的位置,每个Item的左右位置都是相同的,从左侧x=0开始摆放,只是y的点需要计算。所以这里有一个变量offsetY,用以累加当前Item之前所有item的高度。从而计算出当前item的位置。这个部分难度不大,就不再细讲了。
b 实践反馈
找了很有一会儿,我发现在fragment布局的时候,我给FrameLayout的布局中,height的布局为wrap_content。改为math_parent之后就一切正常了
为什么呢?这恐怕和子View的测量过程有关。新写一篇文章把,关于View的测量过程
写了在这里:https://blog.csdn.net/Wby_Nju/article/details/90406169
效果如下:
2.1.3 卡片布局实现
之前给恶心到我了,我要先把效果显示一下看看gakki…
代码基本上照搬玩转仿探探卡片式滑动效果。注意把super.onLayoutChildren(recycler, state);给删掉。再设置一个view.setAlpha(1 - position * CardConfig.DEFAULT_ALPHA);来改变透明度
现在依次逐句看下代码
// 先移除所有view
removeAllViews();
// 在布局之前,将所有的子 View 先 Detach 掉,放入到 Scrap 缓存中
detachAndScrapAttachedViews(recycler);
这两句我抱有疑惑,因为在创建一个 RecyclerView LayoutManager – Part 1中没有用第一句。只用了:detachAndScrapAttachedViews(recycler);效果就如同这个方法名,把所有attached的view都放进recycler中。
同时,在RecyclerView.LayoutManager的常用相关方法中介绍removeAllViews方法的作用是:Remove all views from the currently attached RecyclerView.
注意这个remove,在2.1.2.1 Detach vs. Remove中
Remove 意味着这个 view 已经不需要了。任何被永久移除的 view 都应该 放到 Recycler 中,方便以后重用
你不再需要某个 view 的话,对其调用 removeAndRecycleView()。
而很明显detachAndScrapAttachedViews(recycler);的作用就是把所有Attached的view都放到Recycler(回收池)中。所以我认为或许只需要detachAndScrapAttachedViews(recycler);即可。
int itemCount = getItemCount();
获取Adapter中子项的个数。
// 在这里,我们默认配置 CardConfig.DEFAULT_SHOW_ITEM = 3。即在屏幕上显示的卡片数为3
// 当数据源个数大于最大显示数时
if (itemCount > CardConfig.DEFAULT_SHOW_ITEM) {
// 把数据源倒着循环,这样,第0个数据就在屏幕最上面了
for (int position = CardConfig.DEFAULT_SHOW_ITEM; position >= 0; position--) {
...
}}else{// 当数据源个数小于最大显示数时
...}
这个逻辑基本上没什么好说的。现在进入for循环
final View view = recycler.getViewForPosition(position);
// 将 Item View 加入到 RecyclerView 中
addView(view);
方法的作用看方法名就能知道,但是我想问的是,recycler-回收池中,获取View?它究竟是什么东西?再看一下recycler的解释:
当要给 LayoutManager 提供一个新 view 时,Recycler 首先会 检查 scrap heap 有没有对应的
position/id; 如果没有的话, Recycler 就会从 recycle pool 里弄一个合适的视图出来, 然后用
adapter 给它绑定必要的数据 (就是调用RecyclerView.Adapter.bindViewHolder()) 再返回
再从创建一个 RecyclerView LayoutManager – Part 1中:
重新连接已有的视图很简单; 新的视图是从 Recycler 之中获取的。
在这里是不会有更新操作的,如果有排序更新等操作,就不能一股脑的全从recycler里面获取新的view了。具体细节参考上面引用的文章。
// 测量 Item View
measureChildWithMargins(view, 0, 0);
// getDecoratedMeasuredWidth(view) 可以得到 Item View 的宽度
// 所以 widthSpace 就是除了 Item View 剩余的值
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
// 同理
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
// 将 Item View 放入 RecyclerView 中布局
// 在这里默认布局是放在 RecyclerView 中心
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
这是在干什么呢?看不懂参数多半是对方法,参数是什么意思弄不懂。倒着看
layoutDecoratedWithMargins (View child,
int left,
int top,
int right,
int bottom):
Lay out the given child view within the RecyclerView using coordinates(坐标) that include any current ItemDecorations and margins.
将指定的子view放到RecyclerView中,放置的坐标包含了所有当前使用的item装饰和margin。
看到Decorated,就想起之前的自定义分割线。所以如果是在之后定义分割线,这个地方也会加上我分割线请求的offset吗?(应该是会的吧。)getDecoratedMeasuredHeight(view)是获取带Decorated的宽高。
所以这个地方的代码究竟在干啥呢?
widthSpace 就是除了 Item View 剩余的值 这句话恐怕是指margin和padding值。关于margin和padding。
看来layoutDecoratedWithMargins是直接值绘制的坐标。这也是为什么注释说这段代码将view放在中央
if (position == CardConfig.DEFAULT_SHOW_ITEM) {
...
后面的这一部分就是view绘制出来之后,因为是层叠式的,所以暂时是全部叠在一起的,对不同位置坐不同的缩放、移动和透明度变换
三 响应实现
这里是拖拽实践,自然而然就想到了项目实践-RecyclerView(一)里响应拖拽和响应事件的学习内容。自定义一个Callback,定义一个Callback需要的接口,让adapter来实现这个接口。最后用Callback创建ItemTouchHelper。完事了就进行绑定attachToRecyclerView
ItemTouchHelper.Callback callback = new Card_Recycler_Callback(adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);
这里如何呢?
后记
- 明明可以先设置LayoutManager,再设置adapter,最后设置ItemDecorations。但是似乎manager是最后统筹的?也就是设置完毕了,靠manager显示的时候,会统一调用设置给recyclerView的所有内容。这中间的调用是怎么样的呢?
- 来弄清楚margin、padding、和Decorated在view大小之间的关系吧!