写出你自己的 RecyclerView LayoutManager

原文链接
如果你是Android开发者,到现在为止,你应该至少听说过RecyclerView;一个新的组件,将被加入Support库,它致力于便捷自定义、高性能、可回收的View集合。关于RecyclerView怎么使用的文章在网上已经有一大片就不多说了,这里有几个Demo:
A First Glance at Android's RecyclerView
The new TwoWayView
RecyclerViewItemAnimators
在这一系列的文章中,我将关注于底层的实现,来帮助你建立你自己的较复杂LayoutManager实现,而不是一个简单的横向或者竖向的滑动列表。

在介绍之前,需要注意一下,LayoutManager API 支持强大且复杂的布局回收,因为它没有实现很多,大部分的代码需要你自己去实现。就像在其他的项目中所涉及的自定义View,不要过度的封装和过度的优化你的代码,只需实现在这个应用需要的特性即可。

RecyclerView Playground

在该系列的文章中,所有的代码片段都是来自于我之前在GitHub中提交的代码RecyclerView Playground sample,这个示例包含了几乎各种类型的RecyclerView的用法,从创建一个简单的列表到自定义LayoutManagers。
这篇文章中的代码是从 FixedGridLayoutManager的示例中摘出来的,一个二维的Grid Layout,并且支持水平和垂直方向的滑动。

The Recycler

首先,看一下API的结构。LayoutManager被赋予一个可访问的Recycler实例在需要的地方,当你需要回收旧的views时,或者需要从可能之前已经被回收的之前的子view中获取新的views。

Recycler同时屏蔽了直接操作当前Adapter中的views,当你的LayoutManager需要请求一个新的子view是,只需要简单的调用getViewForPosition(),然后Recycler将会把绑定正确数据的View返回给你。Recycler注重确定是否需要创建一个新的View还是将已存在的,报废的View复用。在LayoutManager中,你的职责就是确定那些不再显示的View及时传递给Recycler,这将使Recycler保证不会创造多于需求的View。

Detach vs.Remove

有两种方式可以在更新布局的适合传递退出的子View,分离和移除。分离是一种比较轻量级的重新排序的方式。在你的代码被返回之前,被分离的View将被重新附加,这将被用于改变附加子View指数而不需要通过Recycler重新绑定或者重新创建这些View。

移除意味着View将不在需要,任何被永久移除的将被放置在Recycler用于以后的复用,但是API没有强制这样做,这取决于你是否需要回收它们。

Scrap vs. Recycle

Recycler是两级缓存系统:废料堆和回收池。废料堆是一个轻量级的可回收的集合,这里的View可以被直接返回到LayoutManager而不需要经过Adapter。
View通常被放在这里当它们被暂时废弃的时候,但在同一次布局中马上复用。回收池由一些被认定为绑定不正确的数据(从其他位置类的数据)的View,所以在传递到LayoutManager之前,它们将被传递到Adapter去重新绑定。

当尝试着给LayoutManager提供新的View时,Recycler首先会检查废料堆去匹配位置/ID,如果存在,它将被直接返回,而不需要重新通过Adapter绑定数据。如果没有匹配的View,Recycler将会从回收池中取一个合适的View并通过Adapter绑定必要的数据(i.e. RecyclerView.Adapter.bindViewHolder() 会被执行)在返回之前。万一回收池中没有可用的View,一个新的View将会被创建(i.e. RecyclerView.Adapter.createViewHolder()将会被执行)在绑定之前,然后返回。

Rule of Thumb

如果你愿意,LayoutManager API可以让你独立做很多完美的事情,所以可能的组合是多种多样的,一般来说,对你想要临时改编并期望在同一操作中使用的可以直接调用detachAndScrapView(),如果你不需要马上复用,则可以调用removeAndRecyclerView()

Building The Core

LayoutManager实例主要负责在需要的时候,实时的添加、测量和铺设所有的子View,当用户在滑动View的时候,它将取决于LayoutManager去决定什么时候,子View需要被添加,什么时候旧的View可以被分离和报废。

你将需要重写和继承以下的方法,去创建一个最小的可用的LayoutManager.

generateDefaultLayoutParams()

事实上,这个方法是唯一一个必须被重写的方法。实现非常的简单,只要返回一个RecyclerView.LayoutParams的实例用于设置子View的高度,在返回getViewForPosition()之前,这些参数将被赋予每个子View。

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams(){
    return new RecyclerView.LayoutParams(
        RecyclerView.LayoutParams.WRAP_CONTENT,
        RecyclerView.LayoutParams.WRAP_CONTENT);
}

onLayoutChildren()

这是LayoutManager的主要入口,当view需要被初始化或者Adapter的数据有变化时会调用该方法。不管是初始化还是数据源变化,对于重置子view,这是个好时机。

在下段,我们将学习当Adapter更新时,这个方法如何实现基于当前显示的元素布局。现在,我们简单的认为它是实现初始化子布局。底下是一个简单的例子,在FixedGridLayoutManager示例:

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //报废的view,用于计算宽高
    View scrap = recycler.getViewForPosition(0);
    addView(scrap);
    measureChildWithMargins(scrap, 0, 0);

    /*
     * 在这里我们假设每个Item的高度是一致的
     * 这使我们可以计算Item的大小,因为Item的大小是不变的
     */
    mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
    mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
    detachAndScrapView(scrap, recycler);

    updateWindowSizing();
    int childLeft;
    int childTop;

    /*
     * 重置第一个item的位置
     */
    mFirstVisiblePosition = 0;
    childLeft = childTop = 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);
}

我们设定并保证所有view的退出都保持到废料堆里,我们抽象了一部分工作到fillGrid()以便于复用,不久之后我们将看到这个方法中执行了一大堆的更新view是否显示当滑动发生时。

就像是对ViewGroup的自定义实例化,你的任务就是触发测量布局每个从Recycler中获取到的子view。这些工作Api是无法直接实现的。

总体来说,初步完成这个方法需要执行的步骤如下:

  • 当最后一次滑动时间发生时,检查所有附加view当前位置偏移;
  • 判断滑动是否导致空白的空间产生,是否需要添加新的view,如果有,从Recycler中获取;
  • 判断是否有不再需要显示的view,如果有,移除并将他们返回Recycler;
  • 检查是否有需要呗重新绘制的view,可能是上层请求移动item的位置。

注意我们在讨论的FixedGridLayoutManager.fillGrid()。这个Manager调整从右到左的位置,当到达最大列时,包裹他们:

  • 取到我们拥有的所有view的清单,分离他们以便于我们可以重新添加。

    SparseArray<View> viewCache = new SparseArray<View>(getChildCount());
    //...
    if (getChildCount() != 0) {
    //...
    //Cache all views by their existing position, before updating counts
    for (int i=0; i < getChildCount(); i++) {
    int position = positionOfIndex(i);
    final View child = getChildAt(i);
    viewCache.put(position, child);
    }

      //Temporarily detach all views.
      // Views we still need will be added back at the  proper index.
      for (int i=0; i < viewCache.size(); i++) {
          detachView(viewCache.valueAt(i));
      }
    

    }

by goyourfly


未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容