Android开发Android开发经验谈Android技术知识

Android 自定义View之Layout过程

2020-08-25  本文已影响0人  小鱼人爱编程

前言

在上篇文章:Android 自定义View之Measure过程,我们分析了Measure过程,本次将会掀开承上启下的Layout过程神秘面纱,
通过本篇文章,你将了解到:

1、关于Layout 简单类比
2、一个简单Demo
3、View Layout过程
4、ViewGroup Layout过程
5、View/ViewGroup 常用方法分析
6、为什么说Layout是承上启下的作用

关于Layout 简单类比

在上篇文章的比喻里,我们说过:

老王给三个儿子,大王(大王儿子:小小王)、二王、三王分配了具体的良田面积,三个儿子(小小王)也都确认了自己的需要的良田面积。这就是:Measure过程
既然知道了分配给各个儿孙的良田大小,那他们到底分到哪一块呢,是靠边、还是中间、还是其它位置呢?先分给谁呢?
老王想按到这个家的时间先后顺序来吧(对应addView 顺序),大王是自己的长子,先分配给他,于是从最左侧开始,划出3亩田给大王。现在轮到二王了,由于大王已经分配了左侧的3亩,那么给二王的5亩地只能从大王右侧开始划分,最后剩下的就分给三王。这就是:ViewGroup onLayout 过程
大王拿到老王给自己指定的良田的边界,将这个边界(左、上、右、下)坐标记录下来。这就是:View Layout过程
接着大王告诉自己的儿子小小王:你爹有点私心啊,从爷爷那继承的5亩田地不能全分给你,我留一些养老。这就是设置:padding 过程
如果二王在最开始测量的时候就想:我不想和大王、三王的田离得太近,那么老王就会给大王、三王与二王的土地之间留点缝隙。这就是设置:margin 过程

一个简单Demo

自定义ViewGroup

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int usedWidth = 0;
        int maxHeight = 0;
        int childState = 0;

        //测量子布局
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
            measureChildWithMargins(childView, widthMeasureSpec, usedWidth, heightMeasureSpec, 0);
            usedWidth += layoutParams.leftMargin + layoutParams.rightMargin + childView.getMeasuredWidth();
            maxHeight = Math.max(maxHeight, layoutParams.topMargin + layoutParams.bottomMargin + childView.getMeasuredHeight());
            childState = combineMeasuredStates(childState, childView.getMeasuredState());
        }

        //统计子布局水平,记录尺寸值
        usedWidth += getPaddingLeft() + getPaddingRight();
        maxHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(resolveSizeAndState(usedWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //父布局传递进来的位置信息
        int parentLeft = getPaddingLeft();
        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;

        //遍历子布局
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
            left = parentLeft + layoutParams.leftMargin;
            right = left + childView.getMeasuredWidth();
            top = getPaddingTop() + layoutParams.topMargin;
            bottom = top + childView.getMeasuredHeight();
            //子布局摆放
            childView.layout(left, top, right, bottom);
            //横向摆放
            parentLeft += right;
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParam(getContext(), attrs);
    }

    //自定义LayoutParams
    static class MyLayoutParam extends MarginLayoutParams {
        public MyLayoutParam(Context c, AttributeSet attrs) {
            super(c, attrs);
        }
    }

该ViewGroup 重写了onMeasure(xx)和onLayout(xx)方法:

  • onMeasure(xx) 测量子布局大小,并根据子布局测算结果来决定自己的尺寸
  • onLayout(xx) 摆放子布局位置

自定义View

public class MyView extends View {

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.GREEN);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int defaultSize = 100;
        setMeasuredDimension(resolveSize(defaultSize, widthMeasureSpec), resolveSize(defaultSize, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }
}

该View 重写了onMeasure(xx)和onLayout(xx)方法:

  • onMeasure(xx) 测量自身大小,并记录尺寸值
  • onLayout(xx) 什么都没做

为MyViewGroup 添加子布局

<?xml version="1.0" encoding="utf-8"?>
<com.fish.myapplication.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_gravity="center_vertical"
    android:background="#000000"
    android:paddingLeft="10dp"
    tools:context=".MainActivity">
    <com.fish.myapplication.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </com.fish.myapplication.MyView>

    <Button
        android:layout_marginLeft="10dp"
        android:text="hello Button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </Button>
</com.fish.myapplication.MyViewGroup>

MyViewGroup里添加了MyView、Button两个控件,最终运行的效果如下:


image.png

可以看出,MyViewGroup 里子布局的是横向摆放的。我们重点关注Layout过程。实际上,MyViewGroup里我们只重写了onLayout(xx)方法,MyView也是重写了onLayout(xx)方法。
接下来,分析View Layout过程。

View Layout过程

View.layout(xx)

与Measure过程类似,连接ViewGroup onLayout(xx)和View onLayout(xx)之间的桥梁是View layout(xx)。

#View.java
    public void layout(int l, int t, int r, int b) {
        //PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 在measure时候可能会设置
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //记录当前的坐标值
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //新(父布局给的)的坐标值与当前坐标值不一致,则认为有改变
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //坐标改变或者是需要重新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //调用onLayout方法,传入父布局传入的坐标
            onLayout(changed, l, t, r, b);
            ...

            //清空请求layout标记
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //监听的onLayoutChange回调,通过addOnLayoutChangeListener 设置
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        ...
    }

    public static boolean isLayoutModeOptical(Object o) {
        //设置了阴影,发光等属性
        //只有ViewGroup有这属性
        //设置    android:layoutMode="opticalBounds" 或者    android:layoutMode="clipBounds"
        //则返回true,默认没设置以上属性
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        //如果设置了阴影、发光灯属性
        //则获取其预留的尺寸
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        //重新改变坐标值,并调用setFrame(xx)
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

可以看出,最终都调用了setFrame(xx)方法。

#View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        //当前坐标值与新的坐标值不一致,则重新设置
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            //记录PFLAG_DRAWN标记位
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            //记录新、旧宽高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //新、旧宽高是否一样
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //不一样,走inValidate,最终执行Draw流程
            invalidate(sizeChanged);

            //将新的坐标值记录
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //设置坐标值给RenderNode
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            //标记已经layout过
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            
            if (sizeChanged) {
                //调用sizeChange,在该方法里,我们已经能够拿到View宽、高值
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            ...
        }
        return changed;
    }

对于Measure过程在onMeasure(xx)里记录了尺寸的值,而对于Layout过程则在layout(xx)里记录了坐标值,具体来说是在setFrame(xx)里,该方法两个重点地方:

1、将新的坐标值记录到成员变量mLeft、mTop、mRight、mBottom里
2、将新的坐标值记录到RenderNode里,当调用Draw过程的时候,Canvas绘制起点就是RenderNode里的位置

View.onLayout(xx)

#View.java
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

View.onLayout(xx)是空实现
从layout(xx)和onLayout(xx)声明可知,这两个方法都是可以被重写的,接下来看看ViewGroup是否重写了它们。

ViewGroup Layout过程

ViewGroup.layout(xx)

#ViewGroup.java
    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            //没有被延迟,或者动画没在改变坐标
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            //父类方法,其实就是View.layout(xx)
            super.layout(l, t, r, b);
        } else {
            //被延迟,那么设置标记位,动画完成后根据标志位requestLayout,重新发起layout过程
            mLayoutCalledWhileSuppressed = true;
        }
    }

ViewGroup.layout(xx)虽然重写了layout(xx),但是仅仅做了简单判断,最后还是调用了View.layout(xx)。

ViewGroup.onLayout(xx)

#ViewGroup.java
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

这重写后将onLayout变为抽象方法,也就是说继承自ViewGroup的类必须重写onLayout(xx)方法。
我们以FrameLayout为例,分析其onLayout(xx)做了什么。

FrameLayout.onLayout(xx)

#FrameLayout.java
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        //子布局个数
        final int count = getChildCount();
        //前景padding,意思是子布局摆放的时候不要侵占该位置
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //遍历子布局
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //GONE状态下无需layout
            if (child.getVisibility() != GONE) {
                //获取LayoutParams
                final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
                //获取之前在Measure过程确定的测量值
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                
                int childLeft;
                int childTop;
                //摆放重心落在哪
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                //布局方向,左到右还是右到左,默认左到右
                final int layoutDirection = getLayoutDirection();
                //水平反向的Gravity
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //垂直方向的Gravity
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        //若子布局水平居中,则它的水平方向起始点
                        //扣除父布局padding剩下的位置
                        //结合子布局宽度,使得子布局在剩下位置里居中
                        //再将子布局margin考虑进去
                        //从这里可以看出,若是xml里有居中,也有margin,先考虑居中,然后再考虑margin
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        //靠右,则改变横向的开始坐标值
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                        //默认是从左到右
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                //垂直方向与水平方向类似
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                
                //确定了child的坐标位置
                //传递给child
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

FrameLayout.onLayout(xx)为子布局Layout的时候,起始坐标都是以FrameLayout为基准,并没有记录上一个子布局占了哪块位置,因此子布局的摆放位置可能会重叠,这也是FrameLayout布局特性的由来。而我们之前的Demo在水平方向上记录了上一个子布局的摆放位置,下一个摆放时只能在它之后,因此就形成了水平摆放的功能。
由此类推,我们常说的某个子布局在父布局里的哪个位置,决定这个位置的即是ViewGroup.onLayout(xx)。

View/ViewGroup 常用方法分析

上边我们分析了View.layout(xx)、View.onLayout(xx)、ViewGroup.layout(xx)、ViewGroup.onLayout(xx),这四者什么关系呢?
View.layout(xx)

将摆放位置坐标记录到成员变量里并给RenderNode设值

View.onLayout(xx)

空实现

ViewGroup.layout(xx)

调用View.layout(xx)**

ViewGroup.onLayout(xx)

抽象方法,子类必须重写。子类重写时候需要为每一个子布局计算出摆放位置,并传递给子布局

View/ViewGroup 子类需要重写哪些方法:

继承自ViewGroup必须重写onLayout(xx),为子布局计算位置坐标
继承自View 无需重写layout(xx)和onLayout(xx),因为它已经没有子布局可以摆放

用图表示:


image.png

为什么说Layout是承上启下的作用

通过上述的描述,我们发现Measure过程和Layout过程里定义的方法比较类似:

measure(xx)<----->layout(xx)
onMeasure(xx)<----->onLayout(xx)

它俩的套路比较类似:measure(xx)、layout(xx)一般不需要我们重写,measure(xx)里调用onMeasure(xx),layout(xx)为调用者设置坐标值。
若是ViewGroup:onMeasure(xx)里遍历子布局,并测量每个子布局,最后将结果汇总,设置自己测量的尺寸;onLayout(xx)里遍历子布局,并设置每个子布局的坐标。
若是View:onMeasure(xx)则测量自身,并存储测量尺寸;onLayout(xx)不需要做什么。

承上

Measure过程虽然比Layout过程复杂,但仔细分析后就会发现其本质就是为了设置两个成员变量:

设置 mMeasuredWidth 和 mMeasuredHeight

而Layout过程虽然比较简单,其本质是为了设置坐标值

1、设置mLeft、mRight、mTop、mBottom 这四个值确定一个矩形区域
2、mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom) 给RnederNode设置坐标

将Measure设置的变量和Layout设置的变量联系起来:

mRight、mBottom 是根据mLeft、mRight 结合mMeasuredWidth、mMeasuredHeight 计算而得的

这就是Layout的承上作用

启下

我们知道View的绘制需要依靠Canvas绘制,而Canvas是有作用区域限制的。例如我们使用:

canvas.drawColor(Color.GREEN);

Cavas绘制的起点是哪呢?
正是通过Layout过程中设置的RenderNode坐标。
这就是Layout的启下作用
以上即是Measure、Layout、Draw三者的内在联系。
当然Layout的"承上"还需要考虑margin、gravity等参数的影响。具体用法参见最开始的Demo。

经典问题

getMeasuredWidth()/getMeasuredHeight 与 getWidth/getHeight区别
我们以获取width为例,分别来看看其方法:

#View.java
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getWidth() {
        return mRight - mLeft;
    }

getMeasuredWidth():获取测量的宽,属于"临时值"
getWidth():获取View真实的宽
在Layout过程之前,getWidth() 默认为0
何时可以获取真实的宽、高

1、重写View.onSizeChanged(xx)方法获取
2、注册View.addOnLayoutChangeListener(xx),在onLayoutChange(xx)里获取
3、重写View.onLayout(xx)方法获取

下篇将分析Draw()过程,我们将分析"一切都是draw出来的"道理

本篇基于 Android 10.0

上一篇下一篇

猜你喜欢

热点阅读