RecyclerView

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 造成屏幕闪烁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值