如何控制 LinearLayout 優先顯示右邊的佈局,空間不足時擠壓左邊控制元件?

振之發表於2019-03-03

本文是一種奇怪又常見的佈局需求實現方案的記錄。
具體需求長這樣子:

如何控制 LinearLayout 優先顯示右邊的佈局,空間不足時擠壓左邊控制元件?

  • 顯示使用者名稱和使用者 ID
  • 整體寬度不能固定,要跟隨內容變化
  • 空間不夠的話優先顯示ID,擷取使用者名稱過長部分顯示為“…”

抽象起來就是多個元素橫向排列,在空間不足的小屏手機上,保證顯示右邊的元素,擠壓左邊的。

怎麼實現呢?

好簡單啊,一開始我是這樣想,腦子裡已經瞬間想好兩個方法 —— RTL 屬性, Weight 屬性。然而真正實現的時候發現它們都有些小缺陷,最後兩個都沒采用,而是重寫了 measure。過程頗費周折。

把這3種方法和對應的缺點記錄在了本文,作為備忘。如果能順便幫到你,那再好不過了。

RTL屬性

簡單來說,從 Android 4.2開始,Android SDK 支援一種從右到左(RTL,Right-to-Left)UI 佈局的方式,儘管這種佈局方式經常被使用在諸如阿拉伯語、希伯來語等環境中,中國使用者很少使用。不過在某些特殊用途中還是很方便的…才怪!

很多機子不能很好地支援 RTL,無法得到一致的外觀。啟用 RTL 也有點麻煩的,而且在寫很多佈局屬性如padding時都要注意做一些轉換計算。關於RTL的詳細介紹和使用點這裡。

所以,RTL,撲街了。

如何控制 LinearLayout 優先顯示右邊的佈局,空間不足時擠壓左邊控制元件?

Weight

LinearLayout 中的控制元件可以新增 layout_weight 屬性。LinearLayout 在分配空間時會先分配沒有設定 Weight 的元素,然後對當前剩餘空間按Weight比例分配給設定了 Weight 的元素。

這個屬性可以很好的應對那些內容會動態變化的佈局結構。屬於 Android 佈局的基礎,再具體的不在這裡敘述。

至此一切都很順利,問題出在把她放在 RecyclerView 中的時候,假如不固定 LinearLayout 的寬度(wrap_content),因為一些 View 重用的機制,notify adapter 時寬度偶爾會亂套。一些長的內容會誤設到之前的短內容寬度的 LinearLayout 中。

如果是非列表佈局,或者是可以確定 LinearLayout 寬度的情況下,Weight 屬性其實非常好用。

但是很遺憾,這次 Weight,也撲街。

如何控制 LinearLayout 優先顯示右邊的佈局,空間不足時擠壓左邊控制元件?

重寫 measure

沒辦法,只能自己做點處理了。
仔細想想,LinearLayout 本來就是從左到右佈局,空間不夠時擠壓右邊,我們只要反過來就可以了。

那就先繼承 LinearLayout:

public class RearFirstLinearlayout extends LinearLayout {}複製程式碼

呼叫父類的 onMeasure 方法以正常使用各種好用的 layout 屬性,如 layout_gravity:

java
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }複製程式碼

然後在 Measure 的時候優先分配右邊元素的空間:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        非橫排和不開啟RearFirst,則用系統預設measure
        if (!isHorizon() || !isRearFirst) {
            return;
        }
        int mWidth = MeasureSpec.getSize(widthMeasureSpec);//可用寬度
        int mHeight = getMeasuredHeight();
        int mCount = getChildCount();//子view數量
        //計算預計總寬
        int preComputeWidth = 0;
        //臨時記錄位置
        mLeft = 0;
        mRight = 0;
        mTop = 0;
        mBottom = 0;

        rest = mWidth;

        for (int i = mCount - 1; i >= 0; i--) {//從後往前算
            Log.i("measure-i:", "" + i);
            final View child = getChildAt(i);
            int spec = MeasureSpec.makeMeasureSpec(rest, MeasureSpec.AT_MOST);
            child.measure(spec, MeasureSpec.UNSPECIFIED);
            int childw = child.getMeasuredWidth();
            int childh = child.getMeasuredHeight();
            preComputeWidth += childw;

//          計算rest
            mRight = getPositionRight(i, mCount, mWidth);
            mLeft = mRight - childw;
            mBottom = mTop + childh;
            rest = mLeft;
        }
        //儲存最終的測量結果
        if (preComputeWidth<=mWidth){
            setMeasuredDimension(preComputeWidth, mHeight);
        }else {
            setMeasuredDimension(mWidth, mHeight);
        }
    }複製程式碼

歐了。


2017.04.05 更新,補充FlexboxLayout方案。
Flexbox 是屬於web前端領域CSS的一種佈局方案,FlexboxLayout是Google出品的一款類似 Flexbox 的佈局,主要解決各種UI比例劃分的問題。可以看到裡面簡單的一個layout_flexShrink屬性就可以解決問題。

layout_flexShrink 屬性定義了專案的縮小比例,預設為1,即如果空間不足,該專案將縮小。
如果所有專案的 layout_flexShrink 屬性都為1,當空間不足時,都將等比例縮小。如果一個專案的flex-shrink屬性為0,其他專案都為1,則空間不足時,前者不縮小。

FlexboxLayout 很強大,這不過她的一個小功能。只是不想因為一個小問題就引個庫進來,所以沒采用。而且 FlexboxLayout 版本還沒到1.0.0。


2017.04.05 2次更新,評論裡提到的Android原始碼裡面實現類似功能的EllipsizeLayout,也放上來吧。
思路類似,不過 EllipsizeLayout 實現得比較細緻,還考慮了gone了的view等處理。

相關文章