【从零开始撸一个App】RecyclerView的使用
目标
前段时间打造了一款简单易用功能全面的图片上传组件,现在就来将上传的图片以图片集的形式展现到App上。出于用户体验考虑,加载新图片采用[无限]滚动模式,Android平台上我们优选RecyclerView组件。
显示图片,用的自然是ImageView
,然而它并不支持直接加载网络图片,需要先通过其它网络组件(如HttpURLConnection
、okhttp3
等)将图片获取到本地,得到BitMap
数据,然后通过setImageBitmap()
加载。
ImageView也有setImageURI(Uri uri)
方法,这里uri的命名容易让人产生错觉,其实只能是本地文件路径。
所幸,一些开源组件封装了繁琐的网络操作和缓存策略,提供了易用的API。这里我选择了Glide
。
实现
加载更多
项布局
有两个,一个用于列表中各个图片显示,一个显示加载更多/已全部加载
放置在列表最末提示用户。
<!--图片-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/thumbnail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
</LinearLayout>
<!--loadmore-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:id="@+id/tv_load_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载更多" />
</LinearLayout>
RecyclerView.Adapter
RecyclerView的设计模式网上资料很多,此处不再赘述。先实现RecyclerView.Adapter
。
class ThumbnailListAdapter(
private val thumbnails: List<Thumbnail>,
private val totalCount: Long,
private val context: Context
) :
RecyclerView.Adapter<ThumbnailListAdapter.ThumbnailViewHolder>() {
// 调用若干次
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
// viewType就是通过getItemViewType得到的
val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
return ThumbnailViewHolder(itemView)
}
// 搞分页/瀑布加载的同学不要把这个和数据库的总数量搞混,这里的itemCount表示现在内存中数据量
// 我们可以[从后端]获取新数据添加到数据集,以实现loadmore功能
override fun getItemCount(): Int {
return if (thumbnails.isNotEmpty())
thumbnails.size + 1 // +1 是因为除了thumbnails数据集之外,还有个写死的loadmore项
else
0
}
// R.layout.xxx 是Int类型,可以直接返回
override fun getItemViewType(position: Int): Int {
return if (position < thumbnails.size)
R.layout.list_thumbnail_image // 正常图片显示
else
R.layout.list_loadmore_footer // 末尾loadmore
}
// 有屏幕外item进入屏幕时就会调用
override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
if (position < thumbnails.size) {
Glide.with(context)
.load(thumbnails[position].uri)
.into(holder.itemView.thumbnail_view)
} else {
if (thumbnails.size >= totalCount)
holder.itemView.tv_load_more.text = "全部加载完毕"
}
}
// 必须这么继承一下
class ThumbnailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
滚动监听
为RecyclerView添加滚动监听,在合适的时候加载新数据到数据集中。
recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
// 已经在加载则跳过
if (!_thumbnailsLoading) {
// 找到最后可见项的索引
val lastPos = layoutManager.findLastVisibleItemPosition()
val sum = adapter.itemCount
// 当快接近末尾项时(这里差额10,表示再显示10个item就没数据了)获取新数据
if (newState == RecyclerView.SCROLL_STATE_IDLE && sum - lastPos <= 10) {
vm.thumbnails.addAll(vm.getMoreAlbumCovers()) // 加载新数据到数据集中
_thumbnailsLoading = true
}
}
}
})
不要将上面预加载数据和Glide的预加载图片混淆起来,拿到数据,和通过数据中的uri获取图片并下载,这是两个步骤。Glide专门针对RecyclerView提供了预加载方案,是为了减少滑动时图片还未从网络请求导致的等待加载情况,目前只支持LinearLayoutManager
或其子类布局
布局
StaggeredGridLayoutManager
按列瀑布流显示图片。简单地将RecyclerView的layoutManager设为StaggeredGridLayoutManager实例即可,注意StaggeredGridLayoutManager目前还是beta版。
val sgLayoutManager =
StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recyclerview.layoutManager = sgLayoutManager
使用StaggeredGridLayoutManager
会发现上下滑动过程中,经常发生图片块重排。根据网上说法,这是因为复用的ViewHolder
和该ViewHolder要加载的图片,它们的尺寸不一致导致。比如某个ViewHolder之前加载的图片高度为60,之后被回收,但是尺寸信息仍然保留着,后来被一张高度80的图片复用,由于StaggeredGridLayoutManager是根据ViewHolder的尺寸排序布局,尺寸的变化导致发生多次排序。解决方法是在ViewHolder绑定数据时(在RecyclerView.Adapter.onBindViewHolder()
中),就事先设置好本次布局的最终尺寸,如下:
override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
val layoutParams =
holder.itemView.thumbnail_view.layoutParams as LinearLayout.LayoutParams
//手动设置ViewHolder高度
layoutParams.height = thumbnails[position].height
Glide.with(context).load(thumbnails[position].uri)
.into(holder.itemView.thumbnail_view)
}
当由下滑回到最顶部时,经常会出现顶部(第一行)的图片相互重排。仔细观察,这是因为第一行初次布局时是按顺序排列而非按空缺插入,往回滑时则是按空缺(哪里最空最先排哪里),这导致顺序可能与初次排序不一致。不过还好,最终仍会按照图片尺寸各自归位。而且这种情况只会出现在第一次由下滑回到顶部时。
GreedoLayoutManager
StaggeredGridLayoutManager一共有3k多行代码,又是beta版。代码洁癖的我把目光投向了GreedoLayoutManager,它是500px
开源的一个LayoutManager,能在保持图片宽高比例的前提下将多张图片拼接到一行显示,原理很简单,看下面动图:
替换LayoutManager也相当简单,重新设置下RecyclerView的layoutManager即可。
val layoutManager =
GreedoLayoutManager(adapter).also { it.setMaxRowHeight(resources.displayMetrics.heightPixels / 3) }
recyclerview.layoutManager = layoutManager
GreedoLayoutManager在布局之前需要知道item的宽高比例,只要让Adapter实现SizeCalculatorDelegate
接口即可
override fun aspectRatioForIndex(index: Int): Double {
val thumbnail = thumbnails[index]
return thumbnail.width / thumbnail.height.toDouble()
}
运行界面显示:
可以看到每张图片都比预期大很多,只能看到一小部分。经研究发现,上面定义的图片展示项的布局(LinearLayout内嵌ImageView),最终呈现后,LinearLayout的尺寸是每个网格的尺寸,而内嵌的ImageView则超出了LinearLayout,似乎其最终尺寸是MeasuredSize
——我们在onCreateViewHolder
时使用了LayoutInflater.from(context).inflate(viewType, parent, false)
,这里的parent
是RecyclerView,而在布局xml中宽高都设置为match_parent
,因此其中ImageView的MeasuredSize同RecyclerView的宽高——然而ImageView最终尺寸应该同样适配网格尺寸才对。
以width为例:
期望:ImageView.width == LinearLayout.width == 网格.width
实际:ImageView.width == ImageView.measuredWith == RecyclerView.width
我们看到每个框格其实是ImageView被截取的左上角那部分。
经过一番搜索,网上各种对getWidth
和getMeasuredWidth
区别的阐述,并没有解决我的困惑,直到这篇从源码的角度分析,getWidth() 与 getMeasuredWidth() 的不同之处让我知道,其实Android系统并没有对width下定义,自定义布局时可随意设置子项大小,是否超出屏幕也没有限制。在我们这个场景下,估计GreedoLayoutMananger在处理了最外层控件(这里是LinearLayout)的width后,并没有递归处理内部控件的width,从而导致了这个bug。
既然如此,那么就不要外围的LinearLayout,直接使用ImageView,反倒省了一点开销。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
return if (viewType == 0) {
val imageView = ImageView(parent.context).apply {
scaleType = ImageView.ScaleType.CENTER_CROP
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
ThumbnailViewHolder(imageView)
} else {
val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
ThumbnailViewHolder(itemView)
}
}
当然也有ViewHolder重用导致的显示问题,图片只显示一部分,且是按ViewHolder重用前的宽高比例显示,如下:
懒得深究,使用Glide官方文档建议的waitForLayout()并没有用,override(width, height)
提前告知图片尺寸解决。
Glide.with(context)
.load(thumbnails[position].uri)
.override(thumbnails[position].width, thumbnails[position].height)
.into(holder.itemView as ImageView)
// .waitForLayout() //并没有用
下拉刷新
使用SwipeRefreshLayout,easy,按过不表。最后成品如下
其它
一般常用detachAndScrapView
,RecyclerView会自动帮我们处理后续重用View[Holder]的逻辑。然而在某些场景下(如只是重排当前显示的Views而不是移除),我们可以使用更轻量级的detachView
(detach之后view就不在界面上显示了),不过要记得在下次布局之前手动调用attachView
(位置的话,detach之前在哪,attach后就在哪)或removeDetachedView
/recycleView
。
注意detach之后,RecyclerView.getChildCount()就相应减少。
真正把 view layout到界面上的是RecyclerView的layoutDecorated
方法。
【从零开始撸一个App】RecyclerView的使用的更多相关文章
- 【从零开始撸一个App】Kotlin
工欲善其事必先利其器.像我们从零开始撸一个App的话,选择最合适的语言是首要任务.如果你跟我一样对Java蹒跚的步态和僵硬的语法颇感无奈,那么Kotlin在很大程度上不会令你失望.虽然为了符合JVM规 ...
- 【从零开始撸一个App】PKCE
一个成功的App背后肯定有一堆后端服务提供支撑,认证授权服务(Authentication and Authorization Service,以下称AAS)就是其中之一,它是约束App.保障资源安全 ...
- 【从零开始撸一个App】Dagger2
Dagger2是一个IOC框架,一般用于Android平台,第一次接触的朋友,一定会被搞得晕头转向.它延续了Java平台Spring框架代码碎片化,注解满天飞的传统.尝试将各处代码片段串联起来,理清思 ...
- 【从零开始撸一个App】Fragment和导航中的使用
Fragment简介 Fragment自从Android 3.0引入开始,它所承担的角色就是显而易见的.它之于Activity就如html片段之于页面,好处无需赘述. Fragment的生命周期和Ac ...
- Android(4)—Mono For Android 第一个App应用程序
0.前言 年前就计划着写这篇博客,总结一下自己做的第一个App,却一直被新项目所累,今天抽空把它写完,记录并回顾一下相关知识点,也为刚学习Mono的同学提供佐证->C#也是开发Android的! ...
- 深入浅出React Native 3: 从零开始写一个Hello World
这是深入浅出React Native的第三篇文章. 1. 环境配置 2. 我的第一个应用 将index.ios.js中的代码全部删掉,为什么要删掉呢?因为我们准备从零开始写一个应用~学习技术最好的方式 ...
- Django1.8教程——从零开始搭建一个完整django博客(一)
第一个Django项目将是一个完整的博客网站.它和我们博客园使用的博客别无二致,一样有分类.标签.归档.查询等功能.如果你对Django感兴趣的话,这是一个绝好的机会.该教程将和你一起,从零开始,搭建 ...
- 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)
从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...
- 从零开始构建一个的asp.net Core 项目
最近突发奇想,想从零开始构建一个Core的MVC项目,于是开始了构建过程. 首先我们添加一个空的CORE下的MVC项目,创建完成之后我们运行一下(Ctrl +F5).我们会在页面上看到"He ...
随机推荐
- UML第二次结对作业
|作业要求|https://edu.cnblogs.com/campus/fzzcxy/2018SE1/homework/11250| | ---------- | ----------------- ...
- hive 将hive表数据查询出来转为json对象和json数组输出
一.将hive表数据查询出来转为json对象输出 1.将查询出来的数据转为一行一行,并指定分割符的数据 2.使用UDF函数,将每一行数据作为string传入UDF函数中转换为json再返回 1.准备数 ...
- Oracle 锁表以及解锁
-- kill_exec 列为解锁的语句,copy出来执行即可.select 'alter system kill session ''' || s.sid || ',' || s.serial# | ...
- Unity优化图解
花了2天把之前学到的一些关于优化的知识全都写了下来,放到一张表里面 https://www.processon.com/mindmap/5cf64f53e4b0bc8329e8112e
- 【SpringMVC】SpringMVC 实现文件上传
SpringMVC 实现文件上传 文章源码 文件上传回顾 查看 JavaWeb 阶段的文件上传下载 实现步骤: 客户端: 发送 post 请求,告诉服务器要上传什么文件 服务器: 要有一个 form ...
- Flutter 布局类组件:弹性布局(Flex)
前言 弹性布局允许子组件按照一定比例来分配父容器空间,Flutter中的弹性布局主要通过Flex和Expanded来配合实现. Flex Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方 ...
- 牛客剑指Offer-数字在升序数组中出现的次数
题目 统计一个数字在升序数组中出现的次数. 示例1 输入 [1,2,3,3,3,3,4,5],3 返回值 4 题解 第一种最简单的方法是O(n)复杂度.遍历数组统计结果. public int Get ...
- Openstack Ocata 负载均衡安装(二)
Openstack OCATA 负载节点(二) 安装haproxy: apt install haproxy 配置haproxy: vim /etc/haproxy/haproxy.cfg globa ...
- C语言补码(C语言学习笔记)
记录 在学习C语言数据范围时了解到了补码的概念,记录一下什么是补码,补码怎么运算的 运算 原文链接:https://www.cnblogs.com/lsgsanxiao/p/5113305.html ...
- iptables自动屏蔽访问网站最频繁的IP
iptables自动屏蔽访问网站频繁的IP 屏蔽每分钟访问超过200的IP 方法1:根据访问日志(Nginx为例 #!/bin/bash DATE=$(date +%d/%b/%Y:%H:%M) AB ...