上次我们讲了创建一个 RecyclerView LayoutManager 的核心步骤。接下来, 我们会介绍如何给普通基于适配器的 View 加入一些附加特性。
友情提醒:示例中的代码在这里 Github
RecyclerView 有一个很好的特性 RecyclerView.ItemDecoration
,它可以给
子视图添加自定义样式,还可以在不修改子视图布局参数的情况下插入
布局属性(margins)。后者就是 LayoutManager 必须提供的约束子视图布局方式。
RecyclerPlayground 里有几个 decorator 用来介绍它们的实现方式。
LayoutManager 中提供了一些辅助方法操作 decorations ,不需要我们自己实现:
getDecoratedLeft()
代替child.getLeft()
获取子视图的 left 边缘。getDecoratedTop()
代替getTop()
获取子视图的 top 边缘。getDecoratedRight()
代替getRight()
获取子视图的 right 边缘。getDecoratedBottom()
代替getBottom()
获取子视图的 bottom 边缘。measureChild()
或 measureChildWithMargins()
代替child.measure()
测量来自 Recycler 的新视图。layoutDecorated()
代替 child.layout()
布局来自 Recycler 的新视图。getDecoratedMeasuredWidth()
或 getDecoratedMeasuredHeight()
代替 child.getMeasuredWidth()
或child.getMeasuredHeight()
获取
子视图的测量数据。只要你使用了正确的方法去获取视图的属性和测量数据,RecyclerView 会自己搞定细节部分的处理。
当使用 notifyDataSetChanged()
触发 RecyclerView.Adapter
的更新操作时,
LayoutManager 负责更新布局中的视图。这时,onLayoutChildren()
会被再次调用。
实现这个功能需要我们调整代码,判断出当前状态是生成一个新的视图 还是 adapter
更新期间的视图改变。下面是FixedGridLayoutManager
中的填充方法的完整实现:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//We have nothing to show for an empty data set but clear any existing views
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
//...on empty layout, update child size measurements
if (getChildCount() == 0) {
//Scrap measure one child
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
/*
* We make some assumptions in this code based on every child
* view being the same size (i.e. a uniform grid). This allows
* us to compute the following values up front because they
* won't change.
*/
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
detachAndScrapView(scrap, recycler);
}
updateWindowSizing();
int childLeft;
int childTop;
if (getChildCount() == 0) { //First or empty layout
/*
* Reset the visible and scroll positions
*/
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else if (getVisibleChildCount() > getItemCount()) {
//Data set is too small to scroll fully, just reset position
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
} else { //Adapter data set changes
/*
* Keep the existing initial position, and save off
* the current scrolled offset.
*/
final View topChild = getChildAt(0);
if (mForceClearOffsets) {
childLeft = childTop = 0;
mForceClearOffsets = false;
} else {
childLeft = getDecoratedLeft(topChild);
childTop = getDecoratedTop(topChild);
}
/*
* Adjust the visible position if out of bounds in the
* new layout. This occurs when the new item count in an adapter
* is much smaller than it was before, and you are scrolled to
* a location where no items would exist.
*/
int lastVisiblePosition = positionOfIndex(getVisibleChildCount() - 1);
if (lastVisiblePosition >= getItemCount()) {
lastVisiblePosition = (getItemCount() - 1);
int lastColumn = mVisibleColumnCount - 1;
int lastRow = mVisibleRowCount - 1;
//Adjust to align the last position in the bottom-right
mFirstVisiblePosition = Math.max(
lastVisiblePosition - lastColumn - (lastRow * getTotalColumnCount()), 0);
childLeft = getHorizontalSpace() - (mDecoratedChildWidth * mVisibleColumnCount);
childTop = getVerticalSpace() - (mDecoratedChildHeight * mVisibleRowCount);
//Correct cases where shifting to the bottom-right overscrolls the top-left
// This happens on data sets too small to scroll in a direction.
if (getFirstVisibleRow() == 0) {
childTop = Math.min(childTop, 0);
}
if (getFirstVisibleColumn() == 0) {
childLeft = Math.min(childLeft, 0);
}
}
}
//Clear all attached views into the recycle bin
detachAndScrapAttachedViews(recycler);
//Fill the grid for the initial layout of views
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);
}
我们根据有没有已经被 attach 的子视图来判断当前是一个新的布局还是一个更新操作。
如果是更新,我们根据第一个可见视图的 position(通过监测视图左上角是哪个子视图)
和当前 x/y 滑动的位移这些信息去执行新的 fillGrid()
,同时保证左上角的 item 位置不变。
下面是一些需要特殊处理得情况:
这个方法提供了另一个重置布局的场所,设置新的 adapter 会触发这个事件
(在这,setAdapter
会被再次调用)。
这个阶段你可以安全的返回一个与之前 adapter 完全不同的视图。所以,
示例中我们移除了所有当前视图(并没有回收它们)。
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
//Completely scrap the existing layout
removeAllViews();
}
移除视图会触发一个新的布局过程,当 onLayoutChildren()
被再次调用时,
我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。
另一个重要的特性就是给 LayoutManager 添加滚动到特定位置的功能。 可以带有有动画效果,也可以没有,下面是对应的两个回调方法。
当 layout 应该立即将所给位置设为第一个可见 item 时,调用 RecyclerView 的 scrollToPosition()
。
在一个 vertical list 里,item 应该放在顶部;horizontal list 中,通常放在左边。在我们的
网格布局中,被选中的 item 应该放在视图的左上角。
@Override
public void scrollToPosition(int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
return;
}
//Ignore current scroll offset, snap to top-left
mForceClearOffsets = true;
//Set requested position as first visible
mFirstVisiblePosition = position;
//Trigger a new view layout
requestLayout();
}
因为有一个良好的 onLayoutChildren()
实现,这里就可以简单的更新目标位置并触发一个
新的 fill 操作。
在带有动画的情况下,我们需要使用一些稍微不同的方法。
在这方法里我们需要创建一个 RecyclerView.SmoothScroller
实例,
然后在方法返回前请求startSmoothScroll()
启动动画。
RecyclerView.SmoothScroller
是提供 API 的抽象类,含有四个方法:
onStart()
:当滑动动画开始时被触发。onStop()
:当滑动动画停止时被触发。onSeekTargetStep()
:当 scroller 搜索目标 view 时被重复调用,这个方法负责读取提供的
dx/dy ,然后更新应该在这两个方向移动的距离。RecyclerView.SmoothScroller.Action
实例做参数。
通过向 action 的 update()
方法传递新的 dx, dy, duration 和 Interpolator
,
告诉 view 在下一个阶段应该执行怎样的动画。onTargetFound()
:只在目标视图被 attach 后调用一次。 这是将目标视图要通过动画移动到准确位置最后的场所。findViewByPosition()
方法
查找对象。如果你的 LayoutManager 可以有效匹配 view 和 position ,
可以覆写这个方法来优化性能。默认提供的实现是通过每次遍历所有子视图查找。你可以自己实现一个 scroller 达到你想要的效果。不过这里我们只使用系统提供的
LinearSmoothScroller
就好了。只需实现一个方法computeScrollVectorForPosition()
,
然后告诉 scroller 初始方向还有从当前位置滚动到目标位置的大概距离。
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
return;
}
/*
* LinearSmoothScroller's default behavior is to scroll the contents until
* the child is fully visible. It will snap to the top-left or bottom-right
* of the parent depending on whether the direction of travel was positive
* or negative.
*/
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
/*
* LinearSmoothScroller, at a minimum, just need to know the vector
* (x/y distance) to travel in order to get from the current positioning
* to the target.
*/
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int rowOffset = getGlobalRowOfPosition(targetPosition)
- getGlobalRowOfPosition(mFirstVisiblePosition);
final int columnOffset = getGlobalColumnOfPosition(targetPosition)
- getGlobalColumnOfPosition(mFirstVisiblePosition);
return new PointF(columnOffset * mDecoratedChildWidth, rowOffset * mDecoratedChildHeight);
}
};
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
在这个实现中,和现有 ListView 的行为相似,无论是 RecyclerView 的哪个方向滚动, 当视图完全可见时滚动就会停止。
我们现在已经有些起色了!事实上还有很多可以实现的功能。在下篇文章中,我们会介绍 如何给你的 LayoutManager 提供数据集改变时的动画效果。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。