RecyclerView 基础用法
class MainActivity : AppCompatActivity() {
private var mData = arrayListOf<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
for (i in 'A'..'Z') { mData.add(i.toInt()) }
recyclerView?.apply {
layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
itemAnimator = DefaultItemAnimator()
adapter = MainAdapter(mData)
adapter?.setHasStableIds(true)
}
}
class MainAdapter(private val data: List<Int>) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (viewType == 1) {
MainViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
)
} else {
MainViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
)
}
}
override fun getItemCount() = data.size
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (holder is MainViewHolder) {
holder.tv.setText(data[position])
}
}
override fun getItemViewType(position: Int): Int {
return 1
}
}
class MainViewHolder(view: View) : ViewHolder(view) {
var tv: TextView = view.findViewById(R.id.tv)
}
ItemDecoration 应用场景
我们可以通过该方法添加分割线
/**
* Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
* affect both measurement and drawing of individual item views.
*
* <p>Item decorations are ordered. Decorations placed earlier in the list will
* be run/queried/drawn first for their effects on item views. Padding added to views
* will be nested; a padding added by an earlier decoration will mean further
* item decorations in the list will be asked to draw/pad within the previous decoration's
* given area.</p>
*
* @param decor Decoration to add
*/
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
RecyclerView.ItemDecoration 是一个抽象类,下面介绍一下如何实现一个可点击、可浮动的 ItemDecoration
1、通过 getItemOffsets 方法设置不同 Item 对应 Decoration 的大小,在这个例子中,如果 Item 是分类第一个,则设置上边距 outRect.top = groupDividerHeight
override fun getItemOffsets(outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State?) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (isGroupFirst(position)) {
outRect.top = groupDividerHeight
}
}
2、由于只需要在 RecyclerView 顶部绘制浮层,所以下面代码中关于坐标方面的逻辑有所简化,其中 headersTopOnDrawOver 用于记录可点击的 Decoration 位置,if 判断条件用于绘制部分可见的浮层,else 用于绘制全部可见的浮层(配合 drawText,用于实现顶出的效果)
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State?) {
super.onDrawOver(canvas, parent, state)
headersTopOnDrawOver.clear()
val firstVisibleView = parent.getChildAt(0)
val firstVisiblePosition = parent.getChildAdapterPosition(firstVisibleView)
val groupName = getGroupName(firstVisiblePosition)
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
if (firstVisibleView.bottom <= groupDividerHeight && isGroupFirst(firstVisiblePosition + 1)) {
// 不完全覆盖的情况
canvas.drawRect(left.toFloat(), 0f, right.toFloat(),
firstVisibleView.bottom.toFloat(), groupBgPaint)
val baseLine = getBaseLineCoordinate(0f, firstVisibleView.bottom.toFloat(),
groupTextPaint.descent(), groupTextPaint.ascent())
canvas.drawText(groupName, (left + groupTextPaddingLeft),
baseLine + itemTextVerticalOffset, groupTextPaint)
} else {
// 完全覆盖的情况
canvas.drawRect(left.toFloat(), 0f, right.toFloat(), groupDividerHeight.toFloat(), groupBgPaint)
val baseLine = getBaseLineCoordinate(0f, groupDividerHeight.toFloat(),
groupTextPaint.descent(), groupTextPaint.ascent())
canvas.drawText(groupName, (left + groupTextPaddingLeft),
baseLine + itemTextVerticalOffset, groupTextPaint)
//绘制图标,并设置点击区域
headersTopOnDrawOver.put(firstVisiblePosition, 0)
val bitmap = BitmapFactory.decodeResource(context.resources, options.iconId)
val rectF = RectF(left.toFloat() + iconPaddingLeft,
groupDividerHeight.toFloat() - (iconPaddingBottom + iconHeight),
left.toFloat() + iconPaddingLeft + iconWeight,
groupDividerHeight.toFloat() - iconPaddingBottom)
canvas.drawBitmap(bitmap, null, rectF, groupTextPaint)
}
}
private fun getBaseLineCoordinate(top: Float, bottom: Float, descent: Float, ascent: Float) =
when (options.dividerVerticalGravity) {
//底部对齐,使descent和bottom对齐即可
Gravity.BOTTOM -> bottom - descent
Gravity.CENTER_VERTICAL -> (top + bottom) / 2f - (descent + ascent) / 2f
else -> (top + bottom) / 2f - (descent + ascent) / 2f
}
3、headersTopOnDraw 用于记录 Decoration 位置,ItemTouchListener 用于实现全局点击拦截,通过 GestureDetector 判断是否是点击事件
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State?) {
super.onDraw(canvas, parent, state)
val childCount = parent.childCount
headersTopOnDraw.clear()
if(gestureDetector == null) {
gestureDetector = StickyHeaderClickGestureDetector(
context, headersTopOnDraw, headersTopOnDrawOver, this)
parent.addOnItemTouchListener(this)
}
gestureDetector?.setOnHeaderClickListener(headerClickListener)
for (i in 0 until childCount) {
val childView = parent.getChildAt(i)
val childAdapterPosition = parent.getChildAdapterPosition(childView)
if (isGroupFirst(childAdapterPosition)) { //是分组第一个,则绘制分组分割线
var left = parent.paddingLeft.toFloat()
val bottom = childView.top.toFloat()
val top: Float = (bottom - groupDividerHeight)
val right = (parent.width - parent.paddingRight).toFloat()
// 绘制group背景
canvas.drawRect(left, top, right, bottom, groupBgPaint)
// 绘制group文本内容,居中显示
val baseLine = getBaseLineCoordinate(top, bottom,
groupTextPaint.descent(), groupTextPaint.ascent())
canvas.drawText(getGroupName(childAdapterPosition), left + groupTextPaddingLeft,
baseLine + itemTextVerticalOffset, groupTextPaint)
// 绘制规则介绍
headersTopOnDraw.put(childAdapterPosition, top.toInt())
val bitmap = BitmapFactory.decodeResource(context.resources, options.iconId)
val rectF = RectF(left + iconPaddingLeft,
bottom - (iconPaddingBottom + iconHeight),
left + iconPaddingLeft + iconWeight,
bottom - iconPaddingBottom)
canvas.drawBitmap(bitmap, null, rectF, groupTextPaint)
}
}
}
目前还没有自定义过 LayoutManager,RecyclerView 默认提供了三个布局,分别为:LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager
动画原理(两次Layout):http://www.birbit.com/recyclerview-animations-part-1-how-animations-work/
ItemAnimator:ItemAnimator是一个抽象类,系统为我们提供了一种默认的实现类DefaultItemAnimator,关键方法有animateRemove、animateAdd、animateRemove、animateChange、runPendingAnimations、mPendingRemovals、mPendingAdditions、mPendingMoves、mPendingChanges、endAnimation、endAnimations,注意,notifyDataSetChanged没有动画效果
自定义动画主要是修改 DefaultItemAnimator,修改animateRemoveImpl、animateAddImpl、animateRemoveImpl即可,Impl方法中监听onAnimationEnd控制各个End状态
AllFeed 框架
传统流程存在的问题就是 Adapter 相关的代码过于公式化且十分臃肿,而且大部分和业务无关,业务逻辑的开发不应该关心 Adapter 的数据与视图的绑定逻辑。AllFeed 框架主要关注数据与视图绑定关系的自动化处理
class DemoViewItem(dataEntity: DataEntity) : BaseViewItem(dataEntity) {
override fun contentSameWith(obj: Any?): Boolean {
return false
}
companion object {
@Keep
@JvmField
@Suppress("unused")
val PRESENTER_CREATOR: AllPresenterCreator<DemoViewItem> =
object : AllPresenterCreator<DemoViewItem> {
override fun create(view: View): BaseViewHolder<DemoViewItem> {
return DemoViewHolder(view)
}
override fun layoutId(): Int {
return R.layout.demo
}
}
}
}
框架底层封装了 BaseAdapter 类,并通过 Manager(单例类)统一管理类型信息,业务只需要实现 ViewItem 并通过注解提供 ViewHolder 和 View 布局信息,BaseAdapter 中会通过反射创建相应的 ViewHolder 和布局信息
ViewHolder 常见状态
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
*/
static final int FLAG_BOUND = 1 << 0;
/**
* The data this ViewHolder's view reflects is stale and needs to be rebound
* by the adapter. mPosition and mItemId are consistent.
*/
static final int FLAG_UPDATE = 1 << 1;
/**
* This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
*/
static final int FLAG_INVALID = 1 << 2;
/**
* This ViewHolder points at data that represents an item previously removed from the
* data set. Its view may still be used for things like outgoing animations.
*/
static final int FLAG_REMOVED = 1 << 3;
RecyclerView 四级缓存
一级缓存
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
- 一级缓存不限制大小
- 一级缓存不参与滑动时复用
- mAttachedScrap 存放未改变的 ViewHolder,mChangedScrap 存放改变的 ViewHolder
/**
* Mark an attached view as scrap.
*
* <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
* for rebinding and reuse. Requests for a view for a given position may return a
* reused or rebound scrap view instance.</p>
*
* @param view View to scrap
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
AttachedScrap:不参与滑动时的回收复用,只保存重新布局时从 RecyclerView 分离的 item 的无效、未移除、未更新的 holder。因为 RecyclerView 在 onLayout 时会先把 Children 全部移除掉,再重新添加进入,mAttachedScrap 临时保存这些 Holder 复用
ChangedScrap:mChangedScrap 和 mAttachedScrap 类似,不参与滑动时的回收复用,只是用作临时保存的变量,它只会负责保存重新布局时发生变化的 item 的无效、未移除的 holder,那么会重走 adapter 绑定数据的方法
- 如果设置了 stableIdEnable,则在 notifyDataSetChanged 时 ViewHolder 会存放在 ChangedScrap 中,复用逻辑会读取一级缓存,因为一级缓存没有大小限制(二级缓存大小为 2, 三级缓存大小为 10),所以可以节省 onCreateViewholder 开销,但还是会调用 onBindViewHolder
- 调用 notifyItemUpdate、notifyItemInsert 时,屏幕上已存在的 ViewHolder 会存放在 AttachedScrap 中,复用逻辑会读取一级缓存中的 AttachedScrap,且不会调用 onBindViewHolder
二级缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
CachedViews 用于保存最新被移除的 ViewHolder。RecyclerView 在滑动时如果需要新的 ViewHolder,精准匹配(根据 position / id 判断)是不是原来被移除的那个 item;如果是,则直接返回 ViewHolder 使用,不需要重新绑定数据;如果不是则不返回,再去 mRecyclerPool 中找 Holder 实例返回,并重新绑定数据。这一级的缓存是有容量限制的,最大数量为 2
三级缓存
ViewCacheExtension:RecyclerView 给开发者预留的缓存池,开发者可以自己拓展回收池,一般不会用到
四级缓存
缓存池
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
清空状态
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
RecyclerPool:是一个终极回收站,真正存放着被标识废弃(其他池都不愿意回收)的 ViewHolder 的缓存池,如果上述 mAttachedScrap、mChangedScrap、mCachedViews、mViewCacheExtension 都找不到 ViewHolder 的情况下,就会从 mRecyclerPool 返回一个废弃的ViewHolder 实例,但是这里的 ViewHolder 是已经被抹除数据的,没有任何绑定的痕迹,需要重新绑定数据。它是根据 viewType 来存储的,是以 SparseArray 嵌套一个 ArraryList 的形式保存 ViewHolder 的
Payload
关于 RecycleView 的数据更新,主要有以下几个方法
- notifyDataSetChanged(),刷新全部可见的 Item
- notifyItemChanged(int),刷新指定 Item
- notifyItemRangeChanged(int,int),从指定位置开始刷新指定个 Item
- notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int),插入、移动一个并自动刷新
- notifyItemChanged(int position)
- notifyItemChanged(int position, @Nullable Object payload)
其中 payload 参数可以认为是你要刷新的一个标示,比如我有时候只想刷新 itemView 中的 TextView,有时候只想刷新 ImageView,我就可以通过 payload 参数来标示这个特殊的需求了
@Override
public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
// payloads为空,说明是更新整个ViewHolder
onBindViewHolder(holder, position);
} else {
// payloads不为空,这只更新需要更新的View即可。
String payload = payloads.get(0).toString();
if ("changeColor".equals(payload)) {
holder.textView.setTextColor("");
}
}
}
RecyclerView 性能优化
- 避免全局刷新
- 增大缓存,比如 mCachedView 大小默认为2,可以设置大点,用空间来换取时间
- 如果高度固定,可以设置 setHasFixedSize(true) 来避免 requestLayout 浪费资源
- 如果多个 RecycledView 的 Adapter 是一样的,可以通过设置 RecyclerView.setRecycledViewPool(pool) 来共用一个 RecycledViewPool
- 设置 adapter.setHasStableIds(true),ViewHolder 会放到一级缓存中,避免重新匹配 ViewHolder 造成屏幕闪烁