高级UI<第五十三篇>:最大最小布局

在工作中,您是否遇到遇到一个界面的需求:左边最大宽度为 2 / 3,右边最小宽度为 1 / 3。

有人说,使用线性布局的权重属性配置,xml布局如下:

图片.png

效果如下:

图片.png

显然,权重属性是一个好方案,但是并不能完全满足需求。

由于权重使某view的宽度值变的固定,这样容易导致字符串不够显示的情况,上图中,手机号码的左侧明明还有很多空间,为了将左侧空间利用起来,我们需要自定义线性布局。

为了完美满足需求,本人写了一个自定义布局,我将它命名为最大最小布局

先来看一下这样一个布局

<com.example.factorhorizontallayoutdemo.FactorHorizontalLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent"
    android:layout_margin="20dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#D5EDBA"
        app:maxFactor="0.7"
        android:padding="10dp"
        android:layout_margin="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="手机号码"
            android:maxLines="1"
            android:ellipsize="end"
            android:textSize="24sp"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#E4D4A2"
        app:minFactor="0.3"
        android:padding="10dp"
        android:layout_margin="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="13770705254"
            android:maxLines="1"
            android:ellipsize="end"
            android:textSize="24sp"/>
    </LinearLayout>
    
</com.example.factorhorizontallayoutdemo.FactorHorizontalLayout>

效果如下:

图片.png

左右两边view的宽度由参数maxFactorminFactor动态分配。

当右边view的内容过长时,效果如下:

图片.png

FactorHorizontalLayout可以完美实现这样的需求,当前布局下的子view都可以随意设置最大因子最小因子,子view的数量可以达到无限。

下面直接贴下代码:

自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FactorHorizontalLayout">
        <!--最大因子-->
        <attr name="maxFactor" format="float"/>
        <!--最小因子-->
        <attr name="minFactor" format="float"/>
    </declare-styleable>

</resources>

自定义布局:

package com.example.factorhorizontallayoutdemo;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * 功能描述:
 * 最大最小布局常用语Item类布局,常见左大右小布局方式,可以使用weight(权重)按比例划分,但是它是固定的,
 * 如果右边view内容过长时,应该判断左边view是否还有足够的控件,如果有,那么左边的view将分配部分空间给
 * 右边view使用。
 *
 * 参数说明:
 * maxFactor 最大因子,view的宽度不能超过最大值
 * minFactor 最小因子,view的宽度不能小于最小值
 *
 * 说明:该布局可以添加任意数量的子view
 *
 * 需要处理的条如下:
 * (1)需要排除marigin和padding的影响;
 *  (2) 需要重写onMeasure方法,对布局下的view进行重新测量;
 *  (3) 必须先测量当前布局和其子view才可以进行下一步的计算;
 *  (4) 为了防止onMeasure执行多次,在计算测量数据之前,需要先清除MeasureSubViewData集合的缓存;
 *  (5) 只需考虑match_parent和固定值两种情况,wrap_content无需考虑;
 *  (6) 为了能够让子view能够使用自定义属性,需要重写generateLayoutParams方法,将LayoutParams重新装饰;
 *  (7) 自定义两个属性maxFactor和minFactor,分别表示最大因子和最小因子,因子的取值在[0~1]之间;
 *  (8) 即使子view没有设置因子,那么也要给它们默认的因子值;
 *  (9) 因子的设置是人为的,为了方式因子取值非法,需要对因子做非法判断;
 *  (10)必须考虑最小因子之和大于1的情况;
 */

public class FactorHorizontalLayout extends LinearLayout {

    private List<MeasureSubViewData> measureSubViewDataList;

    public FactorHorizontalLayout(Context context) {
        this(context, null);
    }

    public FactorHorizontalLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FactorHorizontalLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 将线性布局的方向设置为水平
        setOrientation(HORIZONTAL);
        measureSubViewDataList = new ArrayList<>();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        // 测量当前视图(先测量,方便后面的计算)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 测量所有的子布局(先测量,方便后面的计算)
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        // 清除数据
        measureSubViewDataList.clear();

        // 宽度模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        // 当前视图的宽度,需要去除当前视图本身的padding值
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        if(widthMode == MeasureSpec.EXACTLY){
            // 填充MeasureSubViewData集合
            for (int index = 0; index < getChildCount(); index++){
                // 获取子view
                View subView = getChildAt(index);
                MeasureSubViewData measureSubViewData = new MeasureSubViewData();
                measureSubViewData.setSubView(subView);
                measureSubViewData.setMaxFactor(((FactorParam)subView.getLayoutParams()).getMaxFactor());
                measureSubViewData.setMinFactor(((FactorParam)subView.getLayoutParams()).getMinFactor());
                measureSubViewData.setWidth(subView.getMeasuredWidth());

                // 计算之前,先去除子view本身的margin
                MarginLayoutParams lp = (MarginLayoutParams) measureSubViewData.getSubView().getLayoutParams();
                measureWidth = measureWidth - lp.getMarginStart() - lp.getMarginEnd();

                // 处理没有设置因子的view
                if(measureSubViewData.getMinFactor() == -1 && measureSubViewData.getMaxFactor() == -1){
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) subView.getLayoutParams();
                    if(marginLayoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT){
                        measureSubViewData.setMinFactor(0);
                        measureSubViewData.setMaxFactor(1);
                    }else {
                        float currentFactor = subView.getMeasuredWidth() / (measureWidth * 1.0f);
                        measureSubViewData.setMinFactor(currentFactor);
                        if(currentFactor > 1){
                            measureSubViewData.setMaxFactor(currentFactor);
                        }else{
                            measureSubViewData.setMaxFactor(1);
                        }
                    }
                }else if(measureSubViewData.getMaxFactor() < 0){ // 如果只设置了最小因子
                    if(measureSubViewData.getMinFactor() > 0 && measureSubViewData.getMinFactor() <= 1){
                        measureSubViewData.setMaxFactor(1);
                        // 最小宽度
                        float minWidth = measureSubViewData.getMinFactor() * measureWidth;
                        if(minWidth > measureSubViewData.getWidth()){
                            measureSubViewData.setWidth(minWidth);
                        }
                    }else if(measureSubViewData.getMinFactor() > 1){
                        float minWidth = measureSubViewData.getMinFactor() * measureWidth;
                        measureSubViewData.setWidth(minWidth);
                        measureSubViewData.setMaxFactor(measureSubViewData.getMinFactor());
                    }else{
                        measureSubViewData.setMinFactor(0);
                        measureSubViewData.setMaxFactor(0);
                        measureSubViewData.setWidth(0);
                    }
                }else if(measureSubViewData.getMaxFactor() < measureSubViewData.getMinFactor()){ // 处理最小因子比最大因子大的情况
                    // 非法数据处理
                    measureSubViewData.setMinFactor(0f);
                    measureSubViewData.setMaxFactor(0f);
                    measureSubViewData.setWidth(0);
                }else{ // 如果最大因子和最小因子都被设置了
                    if(measureSubViewData.getWidth() < measureWidth * measureSubViewData.getMinFactor()){
                        measureSubViewData.setWidth(measureWidth * measureSubViewData.getMinFactor());
                    }else if(measureSubViewData.getWidth() > measureWidth * measureSubViewData.getMaxFactor()){
                        measureSubViewData.setWidth(measureWidth * measureSubViewData.getMaxFactor());
                    }
                }

                measureSubViewDataList.add(measureSubViewData);
            }

            float minFactorSum = 0;
            // 计算最小因子之和
            for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
                minFactorSum += measureSubViewData.getMinFactor();
            }

            // 最小因子之和计算出来之后,就和1比较
            if(minFactorSum > 1){
                // 如果最小因子之和大于等于1,那么子控件宽度之和必然大于等于当前控件的宽度
                for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
                    // 将控件的宽度直接设置为最小值
                    measureSubViewData.setWidth(measureWidth * measureSubViewData.getMinFactor());
                }
            }else{

                float remainMinFactor = 1 - minFactorSum;

                float widthValue = 0;
                int tempIndex = 0;
                for (int measureIndex = 0; measureIndex < measureSubViewDataList.size(); measureIndex++){
                    MeasureSubViewData measureSubViewData = measureSubViewDataList.get(measureIndex);
                    widthValue += measureSubViewData.getWidth();
                    if(widthValue > measureWidth){ // 如果总长度超出了布局最大宽度,则重新计算view的宽度
                        // 偏移量(超出部分)
                        float offset = widthValue - measureWidth;
                        for (int tempMeasureIndex = tempIndex; tempMeasureIndex <= measureIndex; tempMeasureIndex++){
                            // 最小限定长度
                            float minFactorWidth = measureWidth * measureSubViewDataList.get(tempMeasureIndex).getMinFactor();
                            if(measureSubViewDataList.get(tempMeasureIndex).getWidth() - offset < minFactorWidth){
                                // 重新计算偏移量值
                                offset = offset - measureSubViewDataList.get(tempMeasureIndex).getWidth() + minFactorWidth;
                                measureSubViewDataList.get(tempMeasureIndex).setWidth(minFactorWidth);
                                tempIndex = tempMeasureIndex + 1;
                            }else{
                                measureSubViewDataList.get(tempMeasureIndex).setWidth(measureSubViewDataList.get(tempMeasureIndex).getWidth() - offset);
                                break;
                            }
                        }
                    }
                }

            }


            // 开始测量
            for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
                MarginLayoutParams lp = (MarginLayoutParams) measureSubViewData.getSubView().getLayoutParams();
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,0, (int) measureSubViewData.getWidth());
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,0, lp.height);
                measureSubViewData.getSubView().measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }

        }

    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //狸猫换太子
        return new FactorParam(getContext(), attrs);
    }

    class FactorParam extends LinearLayout.LayoutParams{

        // 最大因子
        private float maxFactor;
        // 最小因子
        private float minFactor;

        public FactorParam(Context context, AttributeSet attrs) {
            super(context, attrs);

            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FactorHorizontalLayout);
            // 最大因子
            maxFactor = array.getFloat(R.styleable.FactorHorizontalLayout_maxFactor, -1);
            // 最小因子
            minFactor = array.getFloat(R.styleable.FactorHorizontalLayout_minFactor, -1);
            //释放
            array.recycle();
        }

        public float getMaxFactor() {
            return maxFactor;
        }

        public float getMinFactor() {
            return minFactor;
        }

    }

    /**
     * 存放子View的数据
     */
    class MeasureSubViewData{

        // 子view
        private View subView;
        // 最大因子
        private float maxFactor;
        // 最小因子
        private float minFactor;
        // 宽度
        private float width;

        public View getSubView() {
            return subView;
        }

        public void setSubView(View subView) {
            this.subView = subView;
        }

        public float getMaxFactor() {
            return maxFactor;
        }

        public void setMaxFactor(float maxFactor) {
            this.maxFactor = maxFactor;
        }

        public float getMinFactor() {
            return minFactor;
        }

        public void setMinFactor(float minFactor) {
            this.minFactor = minFactor;
        }

        public float getWidth() {
            return width;
        }

        public void setWidth(float width) {
            this.width = width;
        }
    }
}

为了完成这样的布局,本人调试的时间也不短,因为它考虑到的细节特别多。
经过多方面的测试,应该基本没有问题了,在此说明一下,如果大家在使用过程中遇到问题,还请告知。

[本章完...]

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