美文网首页
Android 自定义View及流程

Android 自定义View及流程

作者: wuchao226 | 来源:发表于2019-03-06 18:13 被阅读0次

    自定义View绘制流程:


    概述


    自定义View的基本方法

    自定义 View 的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View 在 Activity 中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout 和 draw。

    • 测量:onMeasure() 决定 View 的大小;
    • 布局:onLayout() 决定 View 在 ViewGroup 中的位置;
    • 绘制:onDraw() 决定绘制这个 View。

    自定义 View 控件分类

    • 自定义 View: 只需要重写 onMeasure() 和 onDraw()
    • 自定义 ViewGroup: 则只需要重写 onMeasure() 和 onLayout()
    1. 自定义ViewGroup

    自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。

    例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。

    2. 自定义View

    在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。

    例如:制作一个支持自动加载网络图片的ImageView,制作图表等

    PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦等诸多问题。

    自定义View基础


    View 类简介

    • View 类是Android中各种组件的基类,如View是ViewGroup基类
    • View表现为显示在屏幕上的各种视图

    Android中的UI组件都由View、ViewGroup组成。

    • View的构造函数:共有4个
      构造函数是View的入口,可以用于初始化一些内容,和获取自定义属性
      View的构造函数有四种重载分别如下:
      // 如果View是在Java代码里面new的,则调用第一个构造函数
      public void SloopView(Context context) {}
      // 如果View是在.xml里声明的,则调用第二个构造函数 
      // 自定义属性是从AttributeSet参数传进来的 public
      public void SloopView(Context context, AttributeSet attrs) {}
      // 不会自动调用
      // 一般是在第二个构造函数里主动调用 
      // 如 view 有 style 属性时
      public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
      // API 21 之后才使用 
      // 不会自动调用 
      // 一般是在第二个构造函数里主动调用 
      // 如View有style属性时
      public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}
    

    可以看出,关于View构造函数的参数有多有少。有四个参数的构造函数在API21的时候才添加上,暂不考虑

    有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明:

        public ImageButton(Context context, AttributeSet attrs) {
            //调用了三个参数的构造函数,明确指定第三个参数
            this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
        }
    
        public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
            //此处调了四个参数的构造函数,无视即可
            this(context, attrs, defStyleAttr, 0); 
        }
    

    注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。

    由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。

    排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下:

     //一般在直接New一个View的时候调用。
      public void SloopView(Context context) {}
      
      //一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。
      public void SloopView(Context context, AttributeSet attrs) {}
    

    以下方法调用的是一个参数的构造函数:

      //在Avtivity中
      SloopView view = new SloopView(this);
    

    以下方法调用的是两个参数的构造函数:

      //在layout文件中 - 格式为: 包名.View名
      <com.sloop.study.SloopView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

    AttributeSet 与自定义属性

    系统自带的 View 可以在 xml 中配置属性,对于写的好的自定义 View 同样可以在 xml 中配置属性,为了使自定义的 View 的属性可以在 xml 中配置,需要以下4个步骤:

    1. 通过 <declare-styleable>为自定义 View 添加属性
    2. 在 xml 中为相应的属性声明属性值
    3. 在运行时(一般为构造函数)获取属性值
    4. 将获取到的属性值应用到 View

    View 视图结构

    1. PhoneWindow 是 Android 系统中最基本的窗口系统,继承自 Windows 类,负责管理界面显示以及事件响应。它是 Activity 与 View 系统交互的接口
    2. DecorView 是 PhoneWindow 中的起始节点 View,继承于 View 类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个 FrameLayout。
    3. ViewRoot 在 Activity 启动时创建,负责管理、布局、渲染窗口 UI 等。

    对于多 View 的视图,结构是树形结构:最顶层是 ViewGroup,ViewGroup下可能有多个 ViewGroup 或 View,如下图:

    无论是 measure 过程、layout 过程还是 draw 过程,永远都是从 View 树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个 View 树中各个 View,最终确定整个 View 树的相关属性。

    Android 坐标系及位置获取方式

    Android 中的坐标系

    Android 中颜色相关内容

    Android 支持的颜色模式:

    以 ARGB8888 为例介绍颜色定义:

    测量View大小(onMeasure)

    View 的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。
    测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据:

      @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //取出宽度的确切数值
            int widthsize = MeasureSpec.getSize(widthMeasureSpec);    
            //取出宽度的测量模式 
            int widthmode = MeasureSpec.getMode(widthMeasureSpec);      
           
            //取出高度的确切数值
            int heightsize = MeasureSpec.getSize(heightMeasureSpec);   
            //取出高度的测量模式
            int heightmode = MeasureSpec.getMode(heightMeasureSpec);    
        }
    

    从上面可以看出 onMeasure 函数中有 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的参数, 毫无疑问他们是和宽高相关的, 但它们其实不是宽和高, 而是由宽、高和各自方向上对应的测量模式来合成的一个值:

    测量模式一共有三种, 被定义在 Android 中的 View 类的一个内部类View.MeasureSpec中:

    模式 二进制数值 描述
    UNSPECIFIED 00 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
    EXACTLY 01 表示父控件已经确切的指定了子View的大小。
    AT_MOST 10 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

    在int类型的32位二进制位中,31-30这两位表示测量模式,29~0这三十位表示宽和高的实际值
    用 MeasureSpec 的 getSize是获取数值, getMode是获取模式即可。

    注意:
    如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。

    确定View大小(onSizeChanged)

    这个函数在视图大小发生改变时调用。
    Q: 在测量完View并使用setMeasuredDimension函数之后View的大小基本上已经确定了,那么为什么还要再次确定View的大小呢?

    A: 这是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。

    onSizeChanged如下:

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
        }
    

    可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。
    我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。

    确定子View布局位置(onLayout)

    确定布局的函数是onLayout,它用于确定子View的位置,在自定义ViewGroup中会用到,他调用的是子View的layout函数。

    在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。

     child.layout(l, t, r, b);
    

    四个参数分别为:

    名称 说明 对应的函数
    l View左侧距父View左侧的距离 getLeft();
    t View顶部距父View顶部的距离 getTop();
    r View右侧距父View左侧的距离 getRight();
    b View底部距父View顶部的距离 getBottom();

    具体可以参考 坐标系 这篇文章。

    5.绘制内容(onDraw)

    onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。

     @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
        }
    

    getMeasureWidth 与 getWidth 的区别

    • getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
    • getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.

    LayoutParams

    LayoutParams 翻译过来就是布局参数,子 View 通过 LayoutParams 告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来 LayoutParams 与 ViewGroup 是息息相关的,因此脱离 ViewGroup 谈 LayoutParams 是没有意义的。

    事实上,每个 ViewGroup 的子类都有自己对应的 LayoutParams 类,典型的如 LinearLayout.LayoutParams 和 FrameLayout.LayoutParams 等,可以看出来 LayoutParams 都是对应 ViewGroup 子类的内部类

    MarginLayoutParams

    MarginLayoutParams 是和外间距有关的。事实也确实如此,和 LayoutParams 相比,MarginLayoutParams 只是增加了对上下左右外间距的支持。实际上大部分 LayoutParams 的实现类都是继承自 MarginLayoutParams,因为基本所有的父容器都是支持子 View 设置外间距的。

    • 属性优先级问题
      MarginLayoutParams 主要就是增加了上下左右4种外间距。在构造方法中,先是获取了 margin 属性;如果该值不合法,就获取 horizontalMargin;如果该值不合法,再去获取 leftMargin 和 rightMargin 属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级

    margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

    • 属性覆盖问题
      优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释

    Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

    LayoutParams 与 View 如何建立联系

    • 在XML中定义 View
    • 在 Java 代码中直接生成 View 对应的实例对象

    addView

    /**
     * 重载方法1:添加一个子View
     * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
     */
    public void addView(View child) {
        addView(child, -1);
    }
    
    /**
     * 重载方法2:在指定位置添加一个子View
     * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
     * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
     */
    public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    
    /**
     * 重载方法3:添加一个子View
     * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
     */
    public void addView(View child, int width, int height) {
        final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams
        params.width = width;
        params.height = height;
        addView(child, -1, params);
    }
    
    /**
     * 重载方法4:添加一个子View,并使用传入的LayoutParams
     */
    @Override
    public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }
    
    /**
     * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
     */
    public void addView(View child, int index, LayoutParams params) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
    
        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    
    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
        .....
        if (mTransition != null) {
            mTransition.addChild(this, child);
        }
    
        if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
            params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
        }
    
        if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
            child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
        } else {
            child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
        }
    
        if (index < 0) {
            index = mChildrenCount;
        }
    
        addInArray(child, index);
    
        // tell our children
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }
        .....
    }
    

    自定义LayoutParams

    1. 创建自定义属性
    <resources>
        <declare-styleable name="xxxViewGroup_Layout">
            <!-- 自定义的属性 -->
            <attr name="layout_simple_attr" format="integer"/>
            <!-- 使用系统预置的属性 -->
            <attr name="android:layout_gravity"/>
        </declare-styleable>
    </resources>
    
    1. 继承MarginLayout
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        public int simpleAttr;
        public int gravity;
    
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            // 解析布局属性
            TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
            simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
            gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
    
            typedArray.recycle();//释放资源
        }
    
        public LayoutParams(int width, int height) {
            super(width, height);
        }
    
        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }
    
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
    
    1. 重写ViewGroup中几个与LayoutParams相关的方法
    // 检查LayoutParams是否合法
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
        return p instanceof SimpleViewGroup.LayoutParams;
    }
    
    // 生成默认的LayoutParams
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
        return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    
    // 对传入的LayoutParams进行转化
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
        return new SimpleViewGroup.LayoutParams(p);
    }
    
    // 对传入的LayoutParams进行转化
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
        return new SimpleViewGroup.LayoutParams(getContext(), attrs);
    }
    

    LayoutParams常见的子类

    在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

    • ViewGroup.MarginLayoutParams
    • FrameLayout.LayoutParams
    • LinearLayout.LayoutParams
    • RelativeLayout.LayoutParams
    • RecyclerView.LayoutParams
    • GridLayoutManager.LayoutParams
    • StaggeredGridLayoutManager.LayoutParams
    • ViewPager.LayoutParams
    • WindowManager.LayoutParams

    MeasureSpec

    定义

    测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize 是指在某种 SpecMode 下的参考尺寸,其中 SpecMode 有如下三种:

    • UNSPECIFIED
      父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
    • EXACTLY
      父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
    • AT_MOST
      你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

    MeasureSpecs 的意义

    通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法

    MeasureSpec值的确定

    MeasureSpec值到底是如何计算得来的呢?

    子 View 的 MeasureSpec 值是根据子 View 的布局参数(LayoutParams)和父容器的 MeasureSpec 值计算得来的,具体计算逻辑封装在 getChildMeasureSpec()

      /**
         *
         * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
         * 最可能符合条件的child view的测量规格。  
    
         * @param spec 父控件的测量规格
         * @param padding 父控件里已经占用的大小
         * @param childDimension child view布局LayoutParams里的尺寸
         * @return child view 的测量规格
         */
        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec); //父控件的测量模式
            int specSize = MeasureSpec.getSize(spec); //父控件的测量大小
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
            case MeasureSpec.EXACTLY:
                //如果child的布局参数有固定值,比如"layout_width" = "100dp"
                //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } 
    
                //如果child的布局参数是"match_parent",也就是想要占满父控件
                //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                }
                //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
                //比如TextView根据设置的字符串大小来决定自己的大小
                //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
                //所以测量模式就是AT_MOST,测量大小就是父控件的size
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
            case MeasureSpec.AT_MOST:
                //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } 
                //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
                //但同样的,child的尺寸上限也是父控件的尺寸上限size
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                //child想要根据自己逻辑决定大小,那就自己决定呗
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    针对上表,这里再做一下具体的说明

    • 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
    • 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
      1. 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
      2. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
      3. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
      4. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。

    实例 流式布局

    public class FlowLayout extends ViewGroup {
    
      /**
       * 每个item 横向间距
       */
      private final int mHorizontalSpacing = dp2px(16);
      /**
       * 每个item 竖向间距
       */
      private final int mVerticallSpacing = dp2px(16);
    
      /**
       * 记录所有的行,一行一行的存储,用于layout
       */
      private List<List<View>> allLines = new ArrayList<>();
    
      /**
       * 记录每一行的行高,用于layout
       */
      private List<Integer> lineHeights = new ArrayList<>();
    
      public FlowLayout(Context context) {
        super(context);
      }
    
      public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
      }
    
      public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
      }
    
      public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
      }
    
      private void clearMeasureParams() {
        allLines.clear();
        lineHeights.clear();
      }
    
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 内存 抖动
        clearMeasureParams();
        // 先测量孩子
        int childCount = getChildCount();
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();
        int paddingTop = getPaddingTop();
        //记录这行已经使用了多宽的size
        int lineWidthUsed = 0;
        // ViewGroup解析的父亲给我的宽度
        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
        // ViewGroup解析的父亲给我的高度
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
        // 保存一行中的所有的view
        List<View> lineView = new ArrayList<>();
        // 一行的行高
        int lineHeight = 0;
        // measure过程中,子View要求的父ViewGroup的宽
        int parentNeededWidth = 0;
        // measure过程中,子View要求的父ViewGroup的高
        int parentNeededHeight = 0;
        for (int i = 0; i < childCount; i++) {
          View childView = getChildAt(i);
          LayoutParams childLP = childView.getLayoutParams();
          if (childView.getVisibility() != View.GONE) {
            // 将layoutParams转变成为 measureSpec
            // 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
            // 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
            // 参数说明
            //  * @param spec 父view的详细测量值(MeasureSpec)
            //  * @param padding view当前尺寸的的内边距和外边距(padding,margin)
            //  * @param childDimension 子视图的布局参数(宽/高)
            int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
            int childHeigthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
            // 测量子view的方法,就把孩子测量完了
            childView.measure(childWidthMeasureSpec, childHeigthMeasureSpec);
            // 获取子view的测量宽高
            int childMesauredWidth = childView.getMeasuredWidth();
            int childMeasuredHeight = childView.getMeasuredHeight();
            // 这里需要换行,等于说 接下来要放置的控件放不下了,需要换行
            if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
              // 一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
              allLines.add(lineView);
              lineHeights.add(lineHeight);
              // 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
              parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
              parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
    
              lineView = new ArrayList<>();
              lineHeight = 0;
              lineWidthUsed = 0;
            }
            // view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
            lineView.add(childView);
            // 每行也需要加上空格
            lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
            // 获取每行最高的高度
            lineHeight = Math.max(lineHeight, childMeasuredHeight);
            //处理最后一行数据
            if (i == childCount - 1) {
              allLines.add(lineView);
              lineHeights.add(lineHeight);
              // 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
              parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
              parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
            }
          }
        }
    
        // setMeasuredDimension 此接口是设置自己的大小,并且保存起来
        // 在度量自己,并保存,父亲想要获取的时候,直接调用孩子的 child.getMeasureWidth就行
        // setMeasuredDimension(width,height);
    
        // 再测量自己,保存
        // 根据子View的度量结果,来重新度量自己ViewGroup
        // 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
        int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
        // 这个传递的是具体的size,不是MeasureSpec
        setMeasuredDimension(realWidth, realHeight);
      }
    
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 总共的行数
        int lineCount = allLines.size();
        int curT = getPaddingTop();
        int curL = getPaddingLeft();
        for (int i = 0; i < lineCount; i++) {
          List<View> lineViews = allLines.get(i);
          int lineHeight = lineHeights.get(i);
          // 每行的view进行布局
          for (int j = 0; j < lineViews.size(); j++) {
            View view = lineViews.get(j);
            int left = curL;
            int top = curT;
            // getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
            // int right = left + view.getWidth();
            // int bottom = top + view.getHeight();
            // getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.
            int right = left + view.getMeasuredWidth();
            int bottom = top + view.getMeasuredHeight();
            view.layout(left, top, right, bottom);
            curL = right + mHorizontalSpacing;
          }
          curL = getPaddingLeft();
          curT = curT + lineHeight + mVerticallSpacing;
        }
      }
    
      public static int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
      }
    }
    

    效果图:


    相关文章

      网友评论

          本文标题:Android 自定义View及流程

          本文链接:https://www.haomeiwen.com/subject/pekxpqtx.html