源码探索系列10---替代Listview的RecycleView

自从有了Recycleview,很多原本是我们的Listview业务都被替代了,关于两者的简单比较,可以看这篇文章。我们今天就去看看他背后故事,下次再写Listview,这名征战多年的老将。

一些不要搞懂的问题

  1. 为何谷歌推荐用这个,背后的效率是高在哪里?
  2. LayoutManager是怎么去弄不同布局的

起航

API:23 ,这RecyclerView有一万多行,看起来真的亚历山大啊。

我们常用的方式就是下面这样:

mRecycleView.setAdapter(mAdapter);

扔给他一个适配器,所以这个就当作我们的起航的第一个突破口吧,看下他背后都做了些什么事。

public void setAdapter(Adapter adapter) {
    // bail out if layout is frozen
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    requestLayout();
}

他先去调用setLayoutFrozen()去停止移动,再更新适配器,最后调用requestLayout()去更新界面。这里补充说下,这个RecyclerView是直接继承ViewGroup的。

public void setLayoutFrozen(boolean frozen) {
    if (frozen != mLayoutFrozen) { 
      ...
      final long now = SystemClock.uptimeMillis();
      MotionEvent cancelEvent = MotionEvent.obtain(now, now,
              MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
      onTouchEvent(cancelEvent);
      mLayoutFrozen = frozen;
      mIgnoreMotionEventTillDown = true;
      stopScroll();  
    }
}

我们看到他背后做的是发送一个cancelEvent同时调用了stopScroll()去停止滚动,背后是怎么停止滚动的呢?

public void stopScroll() {
    setScrollState(SCROLL_STATE_IDLE);
    stopScrollersInternal();
}

private void setScrollState(int state) {
    if (state == mScrollState) {
        return;
    } 
    ...
    mScrollState = state; 
    dispatchOnScrollStateChanged(state);
}

void dispatchOnScrollStateChanged(int state) {
    // Let the LayoutManager go first; this allows it to bring any properties into
    // a consistent state before the RecyclerView subclass responds.
    if (mLayout != null) {
        mLayout.onScrollStateChanged(state);
    }

    // Let the RecyclerView subclass handle this event next; any LayoutManager property
    // changes will be reflected by this time.
    onScrollStateChanged(state);

    // Listeners go last. All other internal state is consistent by this point.
    if (mScrollListener != null) {
        mScrollListener.onScrollStateChanged(this, state);
    }
    if (mScrollListeners != null) {
        for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
            mScrollListeners.get(i).onScrollStateChanged(this, state);
        }
    }
}

/**
 * Similar to {@link #stopScroll()} but does not set the state.
 */
private void stopScrollersInternal() {
    mViewFlinger.stop();
    if (mLayout != null) {
        mLayout.stopSmoothScroller();
    }
}

void stopSmoothScroller() {
        if (mSmoothScroller != null) {
            mSmoothScroller.stop();
        }
    }

上面代码我们看到些有意思的东西,他先去调用我们的mLayout去设置状态是IDLE闲置状态,再不通知监听的接口更新状态。最后才是实际的调用mLayout的stopSmoothScroller()去停止,这个SmoothScroller是一个静态的抽象内部类,具体干活的是LinearSmoothScroller
这个类最终是这mLayout是LayoutManager类,它是RecycleView的一个静态的抽象内部类,主要负责的是Measuring和Positioning我们的Item views 。
干活的有三个StaggeredGridLayoutManagerLinearLayoutManagerGridLayoutManager

StaggeredGridLayoutManager mGridLayoutManager =
                    new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
//两列竖直方向的瀑布流
mRecyclerView.setLayoutManager(mStaggeredGridLayoutManager);

相信使用过RecyclerView的应该对这么名字不陌生,经典的案例就是拿来修改方向灯。这个类有个2K行的就不深挖了,点到即可,继续回主线。

    /**
     * Stops running the SmoothScroller in each animation callback. Note that this does not
     * cancel any existing {@link Action} updated by
     * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
     * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
     */
final protected void stop() {
        if (!mRunning) {
            return;
        }
        onStop();
        mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
        mTargetView = null;
        mTargetPosition = RecyclerView.NO_POSITION;
        mPendingInitialRun = false;
        mRunning = false;
        // trigger a cleanup
        mLayoutManager.onSmoothScrollerStopped(this);
        // clear references to avoid any potential leak by a custom smooth scroller
        mLayoutManager = null;
        mRecyclerView = null;
    } 

我们到一个有意思的事情了,他在运行了得情况下并没有实际的去停止运行,就像我们的AsyncTask一样,是个假停止。如果没运行,才调用SmoothScroller.onStop()去实际的停止。

继续回主线,我们看完 setLayoutFrozen(false)的过程
现在继续下一步

setAdapterInternal(adapter, false, true);

private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
        boolean removeAndRecycleViews) {
     ...
     
    mAdapterHelper.reset();
    final Adapter oldAdapter = mAdapter;
    mAdapter = adapter;
    if (adapter != null) {
        adapter.registerAdapterDataObserver(mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
        mLayout.onAdapterChanged(oldAdapter, mAdapter);
    }
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
    markKnownViewsInvalid();
}

这个更改适配器 的界面,主要就更换了原来的适配器,然后注册新的数据观察者等操作
重要一句是调用Recycler的onAdapterChanged()方法。这个Recycler主要的工作是负责我们在RecyclerView上的各自小itemView的重用功能,所以我们更新了适配器需要告诉下人家。

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
            boolean compatibleWithPrevious) {
        clear();
        getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
    }

这样他就先去调用clear函数去清空原有的。再去调用RecycledViewPool的更新。
需要补充下,这个RecycledViewPool是RecyclerViews的静态内部类,他可以让你做到在不同的RecyclerViews内共享Views,这确实对我们的第一个问题有一定的解答作用,因为这是一个静态内部类啊,而且我们的View都是继承自ViewHolder的,就像我们java的object给人的感觉一样。这样用一个内部的ViewPool的做法,就像线程池,我们可以达到了更高的复用,提高滚动的效率。

private SparseArray<ArrayList<ViewHolder>> mScrap;

这个是RecycledViewPool内部使用稀疏数组来存储我们的ViewHolder。嗯,稀疏,直觉好像觉得不对啊,后面看完再看下是怎么回事.

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
            boolean compatibleWithPrevious) {
    if (oldAdapter != null) {
        detach();
    }
    if (!compatibleWithPrevious && mAttachCount == 0) {
         clear();
     }
    if (newAdapter != null) {
       attach(newAdapter);
    }
 } 

void detach() {
     mAttachCount--;
}
    
void attach(Adapter adapter) {
      mAttachCount++;//啊...这句让我有点意外,传的参数留着以后用?那就以后再加嘛.. 
}

public void clear() {
        mScrap.clear();
    }

这里记录有多少个适配器,同时保存我们的ViewHolder,当我们的适配器都移除了,那就清空缓存的ViewHolder。
我们看下他存的方式

public void putRecycledView(ViewHolder scrap) {
     final int viewType = scrap.getItemViewType();
     final ArrayList scrapHeap = getScrapHeapForType(viewType);
     if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
         return;
     }
     if (DEBUG && scrapHeap.contains(scrap)) {
         throw new IllegalArgumentException("this scrap item already exists");
     }
     scrap.resetInternal();
     scrapHeap.add(scrap);
 }

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
      if (scrap == null) {
          scrap = new ArrayList<ViewHolder>();
          mScrap.put(viewType, scrap);
          if (mMaxScrap.indexOfKey(viewType) < 0) {
              mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
          }
      }
      return scrap;
 }

他的存储是用viewType来做key从而存储对应的ViewHolder列表。
目前在我的开发项目中,这个ViewType存在感有点弱啊。
查看整个过程,发现这个itemViewType最后就是调用的是getItemViewType(int position),默认为0;

final int type = mAdapter.getItemViewType(offsetPosition);

这个补充一点,在前面的一篇比较RecyclerView和Listview的文章有提到,如果要给我们的RecyclerView添加头和尾,不想Listview那样可以 简单的加,实际会负责一点,其中就需要用到这个函数。具体的看 Listview和RecycleView的简单比较 这篇文章里面的缺点第一条。

看完大致的设置适配器部分内容,我们继续回主线。
到了最后的一个函数

requestLayout();

因为我们的RecyclerView是直接继承ViewGroup 的,那这句就会导致重画等步骤,我们继续看下去吧。
说道这里感觉也可以再开个贴,介绍下View的绘制流程和事件的传递流程,下次有空再写吧,虽然现在介绍这个已是烂大街的了,但自己来写应该有什么感觉呢?写了才知道 _
继续:

我们看下实际的绘制界面的部分吧


今天时间有限,下次继续写。。。

后记

那个layoutManager可以做很多文章啊,上次就看到一个有意思的项目叫伦敦眼的

LondonEyeLayoutManager

他的效果就像摩天轮一样绕着转动!


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

推荐阅读更多精彩内容

  • 自从有了Recycleview,很多原本是我们的Listview业务都被替代了,关于两者的简单比较,可以看这篇文章...
    SanjayF阅读 2,699评论 0 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,386评论 25 707
  • 文/丽子 史铁生说:“发烧了,才知道不发烧的日子是多么清爽;咳嗽了,才体会不咳嗽的嗓子多么安详。刚坐上轮椅时,我老...
    丽子a阅读 321评论 8 6
  • 丝帘频抚眉前,不平焉。 抿袖怨春寒两次三番。 虽北地,经微雨,似淮南。 清雾催人愁每每无端。 ————————— ...
    陈婉_阅读 466评论 0 3
  • 题记:Each man is the architect of his own fate.——每个人都是自己命运的...
    LynnXYT阅读 847评论 1 4