Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。

【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。

編輯:關於Android編程

概述

這篇文章是深入掌握自定義LayoutManager系列的開篇,是一份總結報告。部分內容不屬於引言、過於深入,用作系列後續文章的參考,以及浏覽完後的復習之用。

本文內容涉及RecyclerView、LayoutManager、RecyclerViewPool、Recycler。

注:
1 以下問題,初學者如有不理解的,可以不用太在意,等學習完自定義LayoutManager相關知識,寫幾個Demo再回來看更好理解。
2 在RecyclerView中,ItemView和ViewHolder其實是一一綁定的,所以提到的View = ViewHolder。

一 常見誤區、問題、注意事項:

在自定義LayoutManager文章開始之前,我總結了一些我在學習以及閱讀別人的文章、編碼的過程中,遇到的一些疑惑問題,並附上我個人的理解與答案。歡迎拍磚討論。

因網上有大量半吊子寫的LayoutManager相關的中文文章。(包括我也是半吊子),所以很多文章看完了,心中都有N個疑問,如,作者好牛逼啊,但是為什麼我獨立寫還是寫不出來。 自定義一個LayoutManager就自動復用了嗎?…等等,下面逐個來講講。

Q1 看完了,但是我獨立寫還是不知道怎麼寫。

A1: 自定義LayoutManager是一項頗有難度的工程,你很難僅僅閱讀一兩篇文章,花兩三個小時就能學習完。
裡面涉及到子View的布局,坐標的計算,偏移量的計算,在滑動時、在合適的時機回收屏幕上不再顯示的View,如何判斷這些View是在屏幕上不可見,以及View究竟是暫時detach掉,還是recycle回收掉…等大量問題
。老實說,也許我水平有限,這是我在學習Android過程中,耗時最久的幾個知識點之一。(十幾個小時才寫出第一個及格的作品)
但是它值得你學習。所以獨立寫不出來別灰心,先仿照一個Demo寫一寫,如果用心理解,第二遍第二遍應該就可以獨立完成了。

Q2 學習自定義LayoutManager需要的鋪墊知識

一 :熟練掌握自定義ViewGroup。

(在自定義LayoutManager過程的第一步,onLayoutChildren()方法裡,就類似於自定義ViewGroup的onLayout()方法。)
但與自定義LayoutManager相比,自定義ViewGroup是一種靜態的layout 子View的過程,因為ViewGroup內部不支持滑動,所以只需要無腦layout出所有的View,便不用再操心剩下的事。
而自定義LayoutManager與之不同,在第一步layout時,千萬不要layout出所有的子View,這裡也是網上一些文章裡的錯誤做法,他們帶著老思想,在第一步就layout出了所有的childView,這會導致一個很嚴重的問題:你的自定義LayoutManager = 自定義ViewGroup。即,他們沒有View復用機制
why?這裡簡單證明結論,在Q5的回答裡會說明為什麼。
在Adapter的onCreateViewHolder()方法裡增加打印語句,如果你的數據源有100000條數據,那麼在RecyclerView第一次顯示在屏幕上時,onCreateViewHolder()會執行100000次,你就可以盡情的欣賞ANR了。
反觀使用官方提供的三種LayoutManager,開始時屏幕上有n少個ItemView,一般就執行n次onCreateViewHolder(),(也有可能多執行1次),在後續滑動時,大部分情況都只是執行onBindViewHolder()方法,不會再執行onCreateViewHolder()。

二 : 熟練使用RecyclerView。這個不用多說,畢竟RecyclerView是LayoutManager的宿主。

其實會以上兩點就可以開始我們的學習之旅了,不過如果能對RecyclerView的Adapter、RecyclerViewPool、ItemDecoration也有一定的了解那是最好。

Q3 自定義LayoutManager的實戰場景多嗎?

A3:實戰場景還是相當有限的。系統自帶的三個LayoutManager已經很夠用,滿足絕大部分需求。

我個人從學習自定義LayoutManager至今的收獲 ,大部分是對RecyclerView機制的理解進一步加深,也會伴隨一定量的源碼閱讀經驗提升。隨沒有我想象中的提升巨大生產力的趕腳,因為很多時候,產品設計要求的布局,現有方案已經可以很好解決。

但是它值得學習

Q4 自定義一個LayoutManager就自動復用ItemView了嗎?

A4:不是,實際上這是自定義LayoutManager的重頭戲之一,要做到在合適的時機回收 不可見的舊子View ,復用子View layout 新的子View,以及Q2提及的在LayoutManager的初始化時合理布局可見數量的子View等,才算是復用了ItemView。
注意,這裡的回收是recycle,而不是detach。
如果你只detach了ItemView,並沒有recycle它們,它們會一直被保存在Recycler的mAttachedScrap裡,它是一個ArrayList,保存了被detach但還沒有recycle的ViewHolder。

    public final class Recycler {
        final ArrayList mAttachedScrap = new ArrayList<>();

(實際上Recycler內部的緩存機制遠不止一個mAttachedScrap 。)

Q5 用RecyclerView就等於ItemView復用?

A5:顯然也不是。除了Q4的因素外,這裡還有一個很大的誤區:很多人認為使用了RecyclerView,ItemView就都回收復用了。
這裡出個題:基本上APP都有個TopBanner在,它放在RecyclerView裡作為HeaderView(通過特殊的ItemViewType實現),剩下都是普通的ItemView,那麼列表滾動,當Banner早已不可見時,它的View(ViewHolder)會被回收被其他ItemView復用嗎?
如下圖:
這裡寫圖片描述
答案:Banner的ViewHolder 會被回收,但該ViewHolder的內存空間 不會被釋放不會被其他的ItemView復用
回收都好理解,在屏幕上不可見時,LayoutManager會把它回收至RecyclerViewPool裡。
然而卻不會給nZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcm1hbEl0ZW24tNPDo6zS8s6qy/zDx7XESXRlbVZpZXdUeXBlsrvNrDwvc3Ryb25nPqGjPGJyIC8+DQrL+dLUy/y1xMTatOa/1bzksru74bG7ys23xaOsvavSu9axsbtSZWN5Y2xlclZpZXdQb29ss9bT0NfFo6y1yLT918XQ6Mfzz+DNrEl0ZW1WaWV3VHlwZbXEVmlld0hvbGRlcrXEx+vH87W9wLShozxiciAvPg0KvLSjrLWx0rPD5rn2tq+72Lalsr+jrM/Uyr5CYW5uZXLKsaOs1eK49lZpZXe74bG7uLTTw6GjPGJyIC8+DQrPyMu1zqrKssO0o6zU2cu1yOe6zsil0enWpKGjPC9jb2RlPjwvY29kZT48L3A+DQo8aDMgaWQ9"為什麼">為什麼?

這涉及到Recycler、RecyclerViewPool的知識,(小安利,我在http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章的第四節裡對RecyclerViewPool的源碼進行過全解,不過大家也可以自己去查看,源碼很短。)
在LayoutManager裡,獲取childView是通過如下方法得到:

View child = recycler.getViewForPosition(i);

該方法內部,先通過position去獲取是否有detach掉的scrapView(ViewHolder),

holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

如果沒有則根據position去獲取itemViewType,

final int type = mAdapter.getItemViewType(offsetPosition);

根據itemViewType獲取在RecyclerViewPool裡是否有該ViewHolder,

holder = getRecycledViewPool().getRecycledView(type);

這裡由於我們的Banner的viewType和normalItem的viewType不一樣即使Banner被回收進了RecyclerViewPool,但是由於itemViewtype和普通的ItemView不同,它也無法被取出、從而復用
再往下由於holder還是空的,最終便會調用Adapter的onCreateViewHolder()方法create一個新的ViewHolder。

`holder = mAdapter.createViewHolder(RecyclerView.this, type);`

驗證:

感興趣的人去重寫任意Adapter的getItemViewType()方法:

            @Override
            public int getItemViewType(int position) {
                return position;
            }

這樣每一個ItemViewType都不一樣,RecyclerView不會有任何的復用,因為每一個ItemView在RecyclerViewPool裡都找不到可以復用的holder,ItemView有n個,onCreateViewHolder方法會執行n次。

看到這裡就能回答Q2一的問題:
因為在初始化時,Recycler(scrapCache)和RecyclerViewPool裡的緩存都是空的,所以此時得到的ViewHolder都是通過onCreateViewHolder(),new 出的ViewHolder。如果此時get了整個itemCount數量的View,那麼也會new出itemCount數量的ViewHolder,此時這些ViewHolder都存在內存裡,和普通ViewGroup毫無分別,也更容易OOM。

Q6 RecyclerView的緩存機制簡述

A6: 上面BB了這麼多,涉及到Recycler、RecyclerViewPool以及scrap,detach,remove,recycle等概念。
這裡寫圖片描述
這張圖摘自(http://kymjs.com/code/2016/07/10/01),源頭應該是Google官方的視頻裡。
我理解圖上的cache是被detach掉的ViewHolder存放的區域,即scrapCache區域。
這個區域由

        final ArrayList mAttachedScrap = new ArrayList<>();
        ArrayList mChangedScrap = null;

        final ArrayList mCachedViews = new ArrayList();

這三個ArrayList組成。
而被remove掉的ViewHolder會按照ViewType分組被存放在RecyclerViewPool裡,默認最大緩存每組(ViewType)5個。

        private SparseArray> mScrap =
                new SparseArray>();

Q7 detach 和recycle的時機。

一個View只是暫時被清除掉,稍後立刻就要用到,使用detach。它會被緩存進scrapCache的區域。
一個View 不再顯示在屏幕上,需要被清除掉,並且下次再顯示它的時機目前未知 ,使用remove。它會被以viewType分組,緩存進RecyclerViewPool裡。
注意:一個View只被detach,沒有被recycle的話,不會放進RecyclerViewPool裡,會一直存在recycler的scrap 中。網上有人的Demo就是如此,因此View也沒有被復用,有多少ItemCount,就會new出多少個ViewHolder。

Q8 初始化時,onLayoutChildren()為什麼會執行兩次?

答 :參看RecyclerView源碼,onLayoutChildren 會執行兩次,一次RecyclerView的onMeasure() 一次onLayout()。

李菊福:RecyclerView的onMeasure(),會調用dispatchLayoutStep2()方法,該方法內部會調用 mLayout.onLayoutChildren(mRecycler, mState); ,這是第一次。如下:

@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
            ......
            dispatchLayoutStep2();
            ......
    }
    /**
     * The second layout step where we do the actual layout of the views for the final state.
     * This step might be run multiple times if necessary (e.g. measure).
     */
    private void dispatchLayoutStep2() {
        .....
        mLayout.onLayoutChildren(mRecycler, mState);
        .....
    }

onLayout()方法會調用dispatchLayout();,該方法內部又調用了dispatchLayoutStep2();,這是第二次。

Q9 基於上個問題,我們要注意什麼?

答:即使是在寫onLayoutChildren()方法時,也要考慮將屏幕上的View(如果有),detach掉,否則屏幕初始化時,同一個position的ViewHolder,也會onCreateViewHolder兩次。因此childCount也會翻倍。

 

最後也是最重要的

LayoutManager API 支持強大且復雜的布局回收,正因為它API強大,所以我們需要實現大量的代碼才能完成功能。不要過度封裝、過度優化你的代碼,只要能完成你的需求即可。(當然最基本的要求:ViewHolder復用 要滿足
原話如下:
這裡寫圖片描述
文章鏈接:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
該文章是我見過學習自定義LayoutManager最好的資料。

二 常用API:

布局API:

//找recycler要一個childItemView,我們不管它是從scrap裡取,還是從RecyclerViewPool裡取,亦或是onCreateViewHolder裡拿。
View view = recycler.getViewForPosition(xxx);  //獲取postion為xxx的View
addView(view);//將View添加至RecyclerView中,
addView(child, 0);//將View添加至RecyclerView中,childIndex為0,但是View的位置還是由layout的位置決定,該方法在逆序layout子View時有大用
measureChildWithMargins(scrap, 0, 0);//測量View,這個方法會考慮到View的ItemDecoration以及Margin
//將ViewLayout出來,顯示在屏幕上,內部會自動追加上該View的ItemDecoration和Margin。此時我們的View已經可見了
layoutDecoratedWithMargins(view, leftOffset, topOffset,
                        leftOffset + getDecoratedMeasuredWidth(view),
                        topOffset + getDecoratedMeasuredHeight(view));

回收API:

detachAndScrapAttachedViews(recycler);//detach輕量回收所有View
detachAndScrapView(view, recycler);//detach輕量回收指定View

// recycle真的回收一個View ,該View再次回來需要執行onBindViewHolder方法
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);

detachView(view);//超級輕量回收一個View,馬上就要添加回來
attachView(view);//將上個方法detach的View attach回來
recycler.recycleView(viewCache.valueAt(i));//detachView 後 沒有attachView的話 就要真的回收掉他們

移動子ViewAPI:

offsetChildrenVertical(-dy); // 豎直平移容器內的item 
offsetChildrenHorizontal(-dx);//水平平移容器內的item

工具API:

public int getPosition(View view)//獲取某個view 的 layoutPosition,很有用的方法,卻鮮(沒)有文章提及,是我翻看源碼找到的。

//以下方法會我們考慮ItemDecoration的存在,但部分函數沒有考慮margin的存在
getDecoratedLeft(view)=view.getLeft()
getDecoratedTop(view)=view.getTop()
getDecoratedRight(view)=view.getRight()
getDecoratedBottom(view)=view.getBottom()
getDecoratedMeasuredHeight(view)=view.getMeasuredWidth()
getDecoratedMeasuredHeight(view)=view.getMeasuredHeight()
//由於上述方法沒有考慮margin的存在,所以我參考LinearLayoutManager的源碼:
    /**
     * 獲取某個childView在水平方向所占的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /**
     * 獲取某個childView在豎直方向所占的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved