Android自定義LayoutManager實現表格,卡片顯示效果

? ? ? ?LayoutManager是RecyclerView的布局管理器,RecyclerView里面每個item的布局都依賴于LayoutManager的實現。Andorid系統給提供了三個LayoutManager布局管理器:LinearLayoutManager(線性布局管理器)、GridLayoutManager(網格布局管理器)、StaggeredGridLayoutManager(錯列網格布局管理器)。大部分情況下這三個布局管理器都能滿足我們的需求。但是,總有特殊情況的時候。有的時候UI設計師給的效果用系統自帶的這三個布局管理器就是達不到。這個時候咱們就得根據實際需求自定義LayoutManager了,這也是我們接下來所要討論的重點。

一、效果展示

? ? ? ?為了讓大家在客觀上有更好的感受,這里實現了兩種效果:卡片式效果、表格式效果。效果如下:

table.gif

表格實現的功能:

  • 行數和列數不固定,可以隨便多少個。
  • 行數或者列數超過屏幕大小是可以上下左右全方位滑動。
  • 支持設置固定第一行的功能,因為第一行經常是標題欄,在上下滑動的時候,大部分情況也許固定的效果更好點。
  • 支持設置固定每一行的前面多少列,在左右滑動的時候固定。
  • 支持設置除固定多少列之外,當總的列數小于這個設定值的時候。讓剩下的列平分剩下的寬度。比如我們表格只有兩列,我們可以設置讓這兩列平分屏幕的寬度。(@2018.1.19)
card.gif

二、LayoutManager自定義

? ? ? ?對于自定義LayoutManager我們主要處理好三件事情,就所有的問題就都迎刃而解了:

  • 布局每個ItemView
  • 處理滑動事件
  • 緩存重用ItemView

2.1、LayoutManager自定義時常用函數介紹

  1. 布局每個ItemView

? ? ? ?布局每個item需要重寫的函數:

    /**
     * 這里定義RecyclerView里面每個item默認的LayoutParams
     */
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    /**
     * 重寫這個函數來布局RecyclerView當前需要顯示的item,確定每個item的位置
     */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
        //TODO:來布局RecyclerView當前需要顯示的item
    }

? ? ? ?generateDefaultLayoutParams和onLayoutChildren函數也是每個LayoutManager自定義的時候都必須重寫的函數。

? ? ? ?布局每個item常用API:

    /**
     * 根據Adapter的位置position找RecyclerView中對應的item
     * 我們不管它是從scrap里取,還是從RecyclerViewPool里取,亦或是onCreateViewHolder里拿
     * 系統已經幫我們處理好了緩存了
     */
    View view = recycler.getViewForPosition(position);

    /**
     * 將item添加到RecyclerView當中去
     */
    addView(item);
    addView(item, index);

    /**
     * 測量item,這個方法會考慮到View的ItemDecoration以及Margin
     */
    measureChildWithMargins(item, 0, 0);

    /**
     * 將item layout布局出來,顯示在屏幕上,內部會自動追加上該View的ItemDecoration和Margin
     * 這里就需要自己去控制顯示的位置了
     */
    layoutDecoratedWithMargins(item, 0, 0, 0, 0);

? ? ? ?布局每個ItemView關鍵部分確定當前界面上需要顯示的position對應的item,接著測量item,最后把item layout到指定的位置上。

  1. 處理滑動事件

? ? ? ?處理滑動需要重寫的函數:

    /**
     * 是否可以水平滑動
     */
    @Override
    public boolean canScrollHorizontally() {
        return true;
    }

    /**
     * canScrollHorizontally返回true的基礎上,RecyclerView有手指水平滑動的時候回調該函數
     * 注意處理完之后要返回消費的距離
     */
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //TODO:滑動邏輯
        return dx;
    }

    /**
     * 是否可以垂直滑動
     */
    @Override
    public boolean canScrollVertically() {
        return true;
    }

    /**
     * canScrollVertically返回true的基礎上,RecyclerView有手指垂直滑動的時候回調該函數
     * 注意處理完之后要返回消費的距離
     */
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //TODO:滑動邏輯
        return dy;
    }

? ? ? ?canScrollHorizontally和canScrollVertically是滑動的開關操作,只有開關打開了scrollHorizontallyBy()、scrollVerticallyBy()才會被回調,這樣咱們就可以在里面處理滑動的邏輯了。在滑動的時候不管是重新去布局item,還是讓item offset都可以。

? ? ? ?處理滑動常用API:

    /**
     * 垂直移動RecyclerView內所有的item
     */
    offsetChildrenVertical(-dy);
    /**
     * 垂直移動RecyclerView內所有的item
     */
    offsetChildrenHorizontal(-dx);

? ? ? ?處理滑動事的關鍵之處是,在滑動過程中RecyclerView里面每個item的位置變化。

  1. 緩存重用ItemView

? ? ? ?常用API:

    /**
     * detach輕量回收View
     */
    detachAndScrapAttachedViews(recycler);
    detachAndScrapView(view, recycler);

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

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

? ? ? ?緩存重用ItemView的時候,只要你調用了合適的回收函數(detachAndScrapAttachedViews、detachAndScrapView)對應的View會自動回收到緩存機制里面去,當下次recycler.getViewForPosition(position)的時候會先檢測緩存里面是否存在,如果存在則直接從緩存里面獲取,如果不存在則執行Adapter的onBindViewHolder獲取。

2.2、LayoutManager自定義實例講解

? ? ? ?前面講了一大堆,都是為自定義LayoutManager實例做鋪墊。咱得真刀真槍的用上來。接下來介紹兩個自定義LayoutManager的思路:CardLayoutManager、TableLayoutManager。

  1. CardLayoutManager
card.gif

? ? ? ?CardLayoutManager實現卡片式布局,RecyclerView里面所有的item從上往下依次疊在一起,同時為了看起來有層次感,越底下的item會我們稍微添加錯位的效果。并且上層第一張卡片可以隨手指的滑動刪除。關于卡片隨手指滑動刪除效果通過ItemTouchHelper來實現,對于ItemTouchHelper的使用我們就不深究,有興趣的可以參考ItemTouchHelper源碼分析。我們重點在卡CardLayoutManager的實現。

? ? ? ?我們需要通過CardLayoutManager來實現RecyclerView里面所有item疊在一起的效果。而且也不用去處理滑動的邏輯。所有關鍵部分在于重寫onLayoutChildren()函數。

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }
        // 先移除所有view
        detachAndScrapAttachedViews(recycler);
        // 防止view過多產生oom情況,這里我們做了view最大個數的限制,因為沒辦法像別的LayoutManager那樣通過item是否落在屏幕內判斷是否回收
        int viewCount = getItemCount();
        if (getItemCount() > mShowViewMax) {
            viewCount = mShowViewMax;
        }
        // 這里要注意view要反著加,因為adapter position = 0對應的view我們要顯示在最上層
        for (int position = viewCount - 1; position >= 0; position--) {
            // 獲取到制定位置的view
            final View view = recycler.getViewForPosition(position);
            addView(view);
            // 測量view
            measureChildWithMargins(view, 0, 0);
            // view在RecyclerView里面還剩余的寬度
            int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
            // layout view,水平居中,靠上
            layoutDecoratedWithMargins(view, widthSpace / 2, 0, widthSpace / 2 + getDecoratedMeasuredWidth(view),
                                       getDecoratedMeasuredHeight(view));
            // 為了讓重疊在一起的view,有一個更好的顯示效果
            view.setScaleX(getScaleX(position));
            view.setScaleY(getScaleY(position));
            view.setTranslationX(getTranslationX(position));
            view.setTranslationY(getTranslationY(position));
        }

    }

? ? ? ?看到里面的邏輯也很簡單,先移除回收之前所有的item view。然后確定RecyclerView當前要顯示多少個item(防止OOM,不可能顯示所有item),接著就是獲取view,添加view,測量view,布局view。 為了讓疊在一起的item有層次感對item做一些縮放和偏移。大功告成。

  1. TableLayoutManager
table.gif

? ? ? ?TableLayoutManager實現表格的功能,當表格比較大的時候既可以上下滑動又可以左右滑動。在這個基礎之上我們還加入了可以固定表頭(因為第一行有的時候是標題,需要固定),以及可以固定指定前面多少列的需求。其實之前我們也用ListView實現類似的需求,有興趣的可以點擊:Android打造全方位滾動的ListView

? ? ? ?TableLayoutManager的實現我們分以下幾個部分:

  • item布局:

? ? ? ?表格里面肯定是有很多item項的,我們不可能所有的item都在onLayoutChildren里面布局出來。所以為了防止OOM發生,我們一定要明確當前屏幕要顯示哪些item。所以我們有一個Rect變量來記錄item可以顯示的區域。在item layout布局的時候只布局在區域內的item。

  • 處理滑動:

? ? ? ?在滑動的時候,一定要及時去更新顯示區域的變化,以及記錄滑動offset的位置。當每次滑動的時候我們都會重新去layout RecyclerView 里面的item,加上相應的offset。更加具體的邏輯可以參考代碼里面fillChildren()函數實現部分。

  • 回收

? ? ? ?其實在item布局的時候就已經把這個考慮進去了,item不在顯示區域之內的不會add到RecyclerView里面,已經避免了OOM的情況。

  • 處理固定列,固定行部分:

? ? ? ?處理固定列,固定行我這里用的非常粗暴的辦法,在每次layout RecyclerView 里面item的時候,我會把需要固定的item在重新layout到固定的位置上。具體可以參考代碼里面fillChildren()函數實現。

? ? ? ?TableLayoutManager的實現上面講的比較少,推薦大家對照TableLayoutManager源碼來看,TableLayoutManager源碼里面也有相應的注釋。

? ? ? ?最后給出CardLayoutManager、TableLayoutManager實現下載地址:CardLayoutManager、TableLayoutManager代碼鏈接。里面肯定也有很多可以優化的地方歡迎大家指出,同時如果大家在CardLayoutManager、TableLayoutManager基礎之上有什么新的需要也可以提出來,在能力范圍之內的會盡量幫大家實現的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,689評論 25 708
  • 這篇文章分三個部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比...
    LucasAdam閱讀 4,410評論 0 27
  • 但愿你始終不懂人心,游離于世俗。但愿你沒有秘密,自白于人間。如果這但愿,讓你歷經苦楚和傷心,你需了解在骯臟的池塘...
    螢火蟲0727閱讀 849評論 0 1
  • 欲擒故縱 我叫夜鶯,在魅夜當頭牌,我來這里的目的很單純---就是掙錢! 最近,店里生意不是很好,我身邊好多姑娘都離...
    d977f5c6b818閱讀 227評論 0 0
  • 世界上有一種感情介于明戀和暗戀之間,叫crush。 肖小小在做英語閱讀理解的時候碰到一個意味深長的詞匯:crush...
    不辭而別的秋天閱讀 1,157評論 3 7