Android 巧用 flexboxLayout 佈局

FlexBoxlayout是Google推出的開源的可伸縮性佈局,在項目中也會經場使用,大大提高了用戶的體驗。

compile 'com.google.android:flexbox:1.0.0'

有前端基礎的同學估計都知道 CSS 中這個佈局,用來爲盒狀模型提供最大的靈活性。因爲 android 中這個庫屬性和 CSS 中 都一樣,並且阮一峯老師寫的前端知識真的很通俗易懂,所以這裏的介紹大多來自 Flex 佈局教程

採用 Flex 佈局的元素,稱爲 Flex 容器(flex container),簡稱”容器”。它的所有子元素自動成爲容器成員,稱爲 Flex 項目(flex item),簡稱”項目”。

容器默認存在兩根軸:水平的主軸(main axis)和垂直的交叉軸(cross axis)。這裏與 react native 相反,與前端 CSS 保持一致。

主軸的開始位置(與邊框的交叉點)叫做main start,結束位置叫做main end;交叉軸的開始位置叫做cross start,結束位置叫做cross end

項目默認沿主軸排列。單個項目佔據的主軸空間叫做 main size,佔據的交叉軸空間叫做 cross size。

2.容器的屬性 (FlexboxLayout 屬性介紹)

這裏說的容器也就是上面的採用了 Flex 佈局的元素,在 android 中也就是引用了 FlexboxLayout 的 控件。即 FlexboxLayout 控件支持的屬性。主要屬性有:


各個屬性的詳細含義這裏就不再贅述,阮一峯老師 這篇文章 寫的超級棒,圖文並茂,很容易理解,推薦大家看一下。

3.項目的屬性 (子 View 屬性介紹)

設置被 FlexboxLayout 包裹的子 View 的屬性,因爲 android 中的 flexboxLayout 佈局 和 CSS 中 flex 佈局關於子 View 屬性有些差異,所以這裏詳細說明下,取值如下:

  • layout_order (integer)
  • layout_flexGrow (float)
  • layout_flexGrow (float)
  • layout_alignSelf
  • layout_flexBasisPercent (fraction)
  • layout_minWidth / layout_minHeight (dimension)
  • layout_maxWidth / layout_maxHeight (dimension)
  • layout_wrapBefore (boolean)

下面來看一下github文檔中對這些屬性的描述

3.1 layout_order

這個屬性可以改變佈局子視圖的順序。默認情況下,子元素的顯示和佈局順序與佈局XML中的順序相同。如果沒有指定,則將1設置爲默認值( CSS 中默認值爲 0) ,數值越小,排列越靠前


看下文檔中的這張圖,可以看到將 “2” 號 View 的 layout_order 屬性設置爲 -1時,由於其他的 View 默認都是 1,所以 “2” 號 view會排在最前面,同理,將 “2” 號 View 的 layout_order 的屬性值設爲 2 時,比其他默認值 1 都大,所以會排在最後。

3.2 layout_flexGrow

這個屬性類似於 LinearLayout 中的 layout_weight 屬性,如果沒有指定,則將 0 設置爲默認值。如果果同一 flex 行中的多個子 View 有正的 layout_flexGrow 值,那麼剩餘的空閒空間將根據它們聲明的 layout_flexGrow 值的比例分佈。

3.3 layout_flexShrink

該屬性定義了子 View 的縮小比例,默認爲 1,即如果空間不足,該子 View 將縮小。
如果所有子 View 的 layout_flexShrink 屬性都爲 1,當空間不足時,都將等比例縮小。如果一個項目的 layout_flexShrink 屬性爲0,其他子View都爲 1,則空間不足時,layout_flexShrink 屬性爲 0 的不縮小。


看一下文檔中的這張圖,開始設置所有子 view 的 layout_flexShrink 屬性爲1,添加子 view 的時候所有子 view 等比縮小,但是如果設置 layout_flexShrink 屬性值爲 0,子 view 將會按照原有比例顯示,不縮小。

3.4 layout_alignSelf

layout_alignSelf 屬性允許單個子 View 有與其他 View 不一樣的對齊方式,可覆蓋 align-items 屬性。默認值爲 auto,表示繼承父元素的 align-items 屬性,如果沒有父元素,則等同於 stretch。
該屬性可能取 6 個值,除了 auto,其他都與 align-items 屬性完全一致

3.5 layout_flexBasisPercent

flex-layout_flexBasisPercent 屬性定義了在分配多餘空間之前,子 View 佔據的主軸空間(main size)。根據這個屬性來計算主軸是否有多餘空間。它的默認值爲 -1,即不設置,採用子 View 的本來大小。

如果設置了這個值,layout_width (或 layout_height )中指定的長度將被該屬性的計算值覆蓋。這個屬性只有在父 View 的長度是確定的時候纔有效(測量模式是 MeasureSpec.EXACTLY 模式下)。
並且該屬性值只接受百分比值。


可以分析下文檔中的這張圖:可以看到,如果把中間子 View 的這個屬性值設爲 50% 或 90%,那麼這個 View 將佔據主軸 50% 或 90% 的空間,然後剩餘 View 會看有沒有剩餘空間換行。如果設置爲 -1 默認值,那麼將佔據給定的大小。

3.6 layout_minWidth / layout_minHeight

這個屬性設置了子 View 的最小的寬和高。在 layout_flexShrink 模式下,再怎麼縮小也不會小於這個值

3.7 layout_maxWidth / layout_maxHeight

這個屬性設置了子 View 的最大的寬和高。在 layout_flexGrow 模式下,再怎麼放大也不會大於這個值

3.8 layout_wrapBefore

這個屬性使得子 View 可以強制換行,不管在 main size 剩餘空間有多少。這種對於類似 grid 網格佈局中特殊設置某一個 item 佈局特別有用。
這個屬性是 CSS 中沒有的屬性。該屬性在 flex_wrap 屬性值 爲 nowrap(不換行)的時候是無效的。
該屬性結束 boolean 變量,默認 false,即不強制換行


分析下文檔中的這張圖,“5” 號和 “6” 號 View 設置 layout_wrapBefore 屬性爲ture 的時候,不管前面剩餘多少空間,都會強制換行

到這裏,flexboxLayout 基本屬性就介紹完畢了。

然後再來介紹一下跟 recycleView 結合使用。

4. 高能:與 RecyclewView 結合使用

Flexbox 能夠作爲一個 LayoutManager(FlexboxlayoutManager) 用在 RecyclerView 裏面,這也就意味着你可以在一個有大量 Item 的可滾動容器裏面使用 Flexbox,提高性能。具體使用示例:

1
2
3
4
5
6
7
8
9
10
//設置主軸方向爲橫軸
FlexboxLayoutManager manager = new FlexboxLayoutManager(this, FlexDirection.ROW);
//設置item沿主軸方向的位置
manager.setJustifyContent(JustifyContent.CENTER);
//設置item 沿次軸方向的位置
manager.setAlignItems(AlignItems.CENTER);

recycleView.setLayoutManager(manager);
centerGridAdapter = new CenterGridAdapter(items, this);
recycleView.setAdapter(centerGridAdapter);

可以看到跟 RecycleView 的其他 manager 使用一樣,只需設置 manager 屬性即可,屬性值爲上面敘述的幾個容器的屬性。
如果想對某個 item 進行單獨的設置,可以在 adapter 中去設置,設置示例代碼爲:

1
2
3
4
5
6
ViewGroup.LayoutParams lp =  holder.itemLL.getLayoutParams();
       if (lp instanceof FlexboxLayoutManager.LayoutParams) {
           FlexboxLayoutManager.LayoutParams flexboxLp =
                   (FlexboxLayoutManager.LayoutParams)  holder.itemLL.getLayoutParams();
           flexboxLp.setFlexGrow(1.0f);
       }

我這裏是設置每個 item 有個權重(相當於 Linearlayout 的 weight 屬性),所以會按比例分配 item 的寬,而不是我佈局中設定的固定寬高。看下效果:

是不是有種鍵盤的感覺?並且我只是修改了極少的代碼就實現了這個功能。

小試牛刀1

最後,看了那麼多,回到最開始的問題上,現在知道類似微信的那個中間擴展的網格佈局怎麼寫的嗎?
首先我們簡單分析一下,

  • 主軸方向我們應該設置爲水平方向,即默認 flexDirection :“row”’
  • 可以換行,即 flexWrap:“wrap”
  • 子 View 在主軸方向的對其方式爲居中(這一步實現從中間往兩邊展開),即 justifyContent: “center”
  • 子 View 在交叉軸方向的對其方式爲居中,即 alignItems:”center”
  • 子 View 寬高固定

也就是上面講 Recycleview 結合的例子中去掉單獨設置 item 的部分,並且 item 的寬高要根據屏幕來適配的。
嗯,就是 so easy。

小試牛刀2


實際應用中還有種很常見的就是那種分類選擇的佈局,像圖中的網易和簡書中,這種佈局用我們今天的這個主角是不是輕而易舉的就實現了?都不用設置特別的屬性,內容超過一行自動換行。
代碼如下:

1
2
3
4
5
6
7
8
9
10
//設置主軸方向爲橫軸
       FlexboxLayoutManager manager = new FlexboxLayoutManager(this, FlexDirection.ROW);
       //設置item沿主軸方向的位置
       manager.setJustifyContent(JustifyContent.FLEX_START);
       //設置item 沿次軸方向的位置
       manager.setAlignItems(AlignItems.CENTER);

       recycleView.setLayoutManager(manager);
       labelAdapter = new LabelAdapter(labels,this);
       recycleView.setAdapter(labelAdapter);

小試牛刀3

以項目需求爲例,如圖所示:

                              在這裏插入圖片描述                          在這裏插入圖片描述

在第一張需求圖中,品牌篩選時 最後有一個”+“號,想要的效果時 根據品牌選擇後,這個”+“號會緊挨着顯示,如果顯示不完,就換行顯示。

剛開始考慮的是的在RecyclerView添加FooterView,FooterView爲一個“+”的按鈕,這個按鈕會換一行顯示,不能緊挨着顯示,如果超過一行再換行顯示。

此時會遇到兩個問題,

按照RecyclerView中的基本用法使用後 會報錯,日誌如下:

ClassCastException: android.support.v7.widget.RecyclerView$LayoutParams cannot be cast to com.google.android.flexbox.FlexItem。

我們添加的FooterView,無法轉換成FlexlItem。網上找到解決方案,詳情見FlexboxLayoutManager 踩坑。

一是需要重寫FlexboxLayoutManager,代碼示例如下:
 

import com.google.android.flexbox.FlexboxLayoutManager

class MyFlexboxLayoutManager : FlexboxLayoutManager {
    constructor(context: Context) : super(context)

    constructor(context: Context, flexDirection: Int) : super(context, flexDirection)

    constructor(context: Context, flexDirection: Int, flexWrap: Int) : super(context, flexDirection, flexWrap)


    /**
     * 將LayoutParams轉換成新的FlexboxLayoutManager.LayoutParams
     */
    override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
        return when (lp) {
            //TODO:可能需要適配,特殊處理"+"的寬度
            is RecyclerView.LayoutParams -> LayoutParams(lp)
            is ViewGroup.MarginLayoutParams -> LayoutParams(lp)
            else -> LayoutParams(lp)
        }
    }
}

第二個問題:顯示問題。此時會出現我們的FooterView(”+“號)換一行顯示,不能緊挨着顯示。

於是陷入思考,在上述代碼中,RecyclerView.LayoutParams的計算方法LayoutParams(lp)也需要重寫。

在查閱資料和源碼後發現比較麻煩,最後在網友的幫助下(FlexboxLayoutManager 踩坑),換一種思路,用RecyclerView多佈局來實現,豁然開朗。

思路如下:在數據實體類中添加一個標誌,如是否是添加標誌isAdd,根據該值的是不同顯示不同的item佈局。

class CameraVehicleBrandAdapter : MyBaseMultiItemAdapter<CameraVehicleBrandEntity>() {

    init {
        addItemType(CameraVehicleBrandEntity.ITEM_TYPE_NORMAL, R.layout.home_recycle_item_camera_vehicle_brand_selected)
        addItemType(CameraVehicleBrandEntity.ITEM_TYPE_ADD, R.layout.home_recycle_item_camera_vehicle_brand_add)
    }

    override fun convert(helper: BaseViewHolder, item: CameraVehicleBrandEntity) {
        super.convert(helper, item)
        if (item.isAdd) {
            helper.addOnClickListener(R.id.ivAddBrand)
            return
        }
        helper.setText(R.id.tvBrandName, item.brandName)
        helper.addOnClickListener(R.id.ivDelete)
    }
}

調用方法:

  //品牌篩選
rvBrand.layoutManager = FlexboxLayoutManager(context)
rvBrand.adapter = brandAdapter

 

總結

所以說了這麼多,那麼我們什麼時候會用到這種佈局呢?我目前想到的場景主要有 3 類:

  1. 類似LinearLayout 線性佈局,但是又可以自動換行的
  2. 類似grid 網格佈局的,但總有一兩個item的排列方式很特立獨行的
  3. 類似瀑布流的,但也是總有一兩個item跟別人不一樣的

當然這些場景加上 RecycleView 就會更加暢享絲滑了。

demo地址:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章