Android-自定义View前传-View的三大流程-Layout

Android-自定义View前传-View的三大流程-Layout

参考

  • 《Android开发艺术探索》
  • https://github.com/hongyangAndroid/FlowLayout

写在前头

在之前的文章中 , 我们学习了Android View的 Measure的流程, 本篇文章来学习一下View的 Layout 的过程。 学完了这一篇文章后,我们可以尝试自己去自定义一个自己的Layout。

Overview

我对于Layout过程的理解:Layout的过程就是给Child安家的过程

Layout的过程主要是放在 ViewGroup 中的,ViewGroup不仅需要定位自己,还需要定位Child。

View和ViewGroup

Layout流程的起点也是在 ViewRootImpl 中的 performTraversals 方法中。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;
    final View host = mView;
    if (host == null) {
        return;
    }

    try {
        //首先调用了host的layout方法 host = mView = DecorView
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        //....
    }

我们接着来看View的layout方法

public void layout(int l, int t, int r, int b) {
    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;
    //调用 setOpticalFrame或者 setFrame 来确定自己的位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {、
        //调用onLayout
        onLayout(changed, l, t, r, b);
        //.....
}

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;

        // Remember our drawn bit
        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 our old position
        invalidate(sizeChanged);
        //确定4个点
        //这四个点一旦确定了那么View在ViewGroup中的位置也就确定了
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;

        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        //......

View的 layout 方法主要是做了:

  1. 通过 setFrame 确定了自己的位置,一篇Left,Top,Right,Bottom这几个值确定了,那么View的位置也就确定了。
  2. 紧接着调用了onLayout 方法。

LinearLayout的onLayout

View是不需要实现onLayout方法的,只用ViewGroup才需要实现。由于各种ViewGroup的布局方式的不同,无法统一,所以ViewGroup也并没有实现onLayout. 而是将onLayout的过程放到了子类中。我们还是通过 LinearLayout 来学习。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

onLayout根据 Orientation 属性来调用 layoutVertical 或者 layoutHorizontal

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;

    // Space available for child
    //获取Child可用的空间
    int childSpace = width - paddingLeft - mPaddingRight;
    //Child的Group
    final int count = getVirtualChildCount();

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    //根据Gravity确定初始的childTop
    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }
    //LayoutView
    //遍历
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {//过滤Child的Visibility是GONE的情况
            //获取Child的大小
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            //获取LP
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            //处理Child的Gravity
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                //水平居中
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;
                //居右
                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }
            //childTop 加上 Divider
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            //加上Margin
            childTop += lp.topMargin;

            //调用Child的layout方法
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

上面的代码还是比较清晰的,首先是有一个childTop变量,来确定child与ViewGroup顶部的距离,通过不断的遍历Child然后不断增加childTop的值,这样就实现了LinearLayout的垂直布局的效果。当然其中也有处理LinearLayout和Child的Gravity的过程。

FlowLayout

通过学习LinearLayout的Layout的过程,发现其实Layout的过程就是确定View的Left,Top,Right,Bottom 4个值的过程,学习了 MeasureLayout 的过程以后,我们就已经可以着手做一个自己的Layout了,这里我选的是模仿 hongyang大神的FlowLayout。效果图如下:

package layout

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

/**
 * Created by ShyCoder on 2019/1/16.
 */
class MyFlowLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
    : ViewGroup(context, attrs, defStyleAttr) {
    constructor(context: Context?, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context?) : this(context, null)

    init {

    }

    /**
     * 存贮所有的View根据行来存贮
     * */
    private val mAllViews = mutableListOf<List<View>>()

    /**
     * 存贮每一行View的高度
     * */
    private val mHeightList = mutableListOf<Int>()

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //从MeasureSpec获取Mode和Size
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getMode(heightMeasureSpec)

        //计算Wrap_content的情况
        var totalHeight = 0//总高度
        var totalWidth = 0//总宽度

        var lineWidth = 0//当前行的宽度
        var lineHeight = 0//当前行的高度

        val viewCount = childCount

        for (i in 0.until(viewCount)) {
            val child = getChildAt(i)
            //如果是GONE状态不用测量
            if (child.visibility == View.GONE) {
                continue
            }
            //测量Child
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            val lp = child.layoutParams as MarginLayoutParams

            //计算child的width = 测量后宽度+左右的两个Margin
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            //计算child的Height = 测量后高度 + 上下两个Margin
            val childHeight = child.measuredHeight + lp.bottomMargin + lp.topMargin

            //需要换行时候的处理方式
            //如果已有的行宽+当前child的宽度> FlowLayout的宽度(减去左右的Padding)
            if (childWidth + lineWidth > widthSize - this.paddingLeft + this.paddingRight) {
                totalWidth = Math.max(totalWidth, lineWidth)
                lineWidth = childWidth
                totalHeight += lineHeight
                lineHeight = childHeight
            } else {//如果不需要换行增加行宽
                lineWidth += childWidth
                //获取最大高度
                lineHeight = Math.max(lineHeight, childHeight)
            }
            //最后一个View
            if (i == viewCount - 1) {
                totalWidth = Math.max(totalWidth, lineWidth)
                totalHeight += lineHeight
            }
        }
        this.setMeasuredDimension(
                //如果MeasureSpec的Mode是EXACTLY 的话,测量后大小等会传进来的测量大小,
                //否则则是我们自己计算的大小
                if (widthMode == MeasureSpec.EXACTLY) widthSize else totalWidth,
                if (heightMode == MeasureSpec.EXACTLY) heightSize else totalHeight
        )
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        this.mAllViews.clear()
        this.mHeightList.clear()

        val viewCount = this.childCount

        var lineHeight = 0
        var lineWidth = 0

        var lineViews = mutableListOf<View>()

        for (i in 0.until(viewCount)) {
            val child = this.getChildAt(i)
            if (child.visibility == View.GONE) {
                continue
            }
            val lp = child.layoutParams as MarginLayoutParams

            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin

            //行上无法继续放置View
            if (childWidth + lineWidth > this.width - this.paddingLeft - this.paddingRight) {
                //添加line height
                mHeightList.add(lineHeight)
                lineWidth = 0
                //添加正行的View到集合中
                this.mAllViews.add(lineViews)
                lineViews = mutableListOf()

            }
            //区当前行的View的最大高度
            lineHeight = StrictMath.max(lineHeight, childHeight)
            lineWidth += childWidth
            //向行上添加View
            lineViews.add(child)
        }

        //进行Layout
        var left = this.paddingLeft
        var top = this.paddingTop

        for (i in 0.until(this.mAllViews.size)) {
            val lineViews = mAllViews[i]
            val lineHeight = mHeightList[i]
            left = this.paddingLeft

            for (j in 0.until(lineViews.size)) {
                val child = lineViews[j]
                val lp = child.layoutParams as MarginLayoutParams

                //view的四个边
                val l = left + lp.leftMargin
                val t = top + lp.topMargin
                val r = l + child.measuredWidth
                val b = t + child.measuredHeight

                //调用Child的Layout方法
                child.layout(l, t, r, b)

                left += child.measuredWidth + lp.leftMargin + lp.rightMargin
            }
            top += lineHeight
        }
    }
}

在onMeasure对wrap_content的情况进行了处理,计算出所需要的大小。

在onLayout方法中,首先计算出每一行的高度并存储和获取每一行的View的List进行存储,当这些都计算完之后,进行layout操作。

在layout操作的时候,首先是在同一行上的View的top是统一的,每当这一行的View处理完成之后,就执行换行的操作,即增加view的大小。

写在最后

本篇文章就到此结束了,Layout的过程还是相对简单的,在下一篇文章呢,我们将会学习View的最后一个流程 Draw 流程。

原文地址:https://www.cnblogs.com/slyfox/p/10304353.html

时间: 2024-10-28 15:20:11

Android-自定义View前传-View的三大流程-Layout的相关文章

【转】ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

原文地址:http://blog.csdn.net/a396901990/article/details/36475213 简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量——onMeasure():决定View的大小 2.布局——onLayout():决定View在ViewGroup中的位置 3.绘制——onDraw():如何绘制这个View. 而第3步的onDraw系统已经封装的很好了,基本不用我们来操心,只需要专注到1,2两个步骤就中好了. 而这篇文章就来谈谈第一步

Android 自定义可拖拽View,界面渲染刷新后不会自动回到起始位置

以自定义ImageView为例: /** * 可拖拽ImageView * Created by admin on 2017/2/21. */ public class FloatingImageView extends ImageView{ public FloatingImageView(Context context) { super(context); } public FloatingImageView(Context context, AttributeSet attrs) { su

Android 自定义View之BounceProgressBar

之前几天下载了很久没用了的桌面版酷狗来用用的时候,发现其中加载歌曲的等待进度条的效果不错(个人感觉),如下: 然后趁着这周末两天天气较冷,窝在宿舍放下成堆的操作系统作业(目测要抄一节多课的一堆堆文字了啊...啊..)毅然决定把它鼓捣出来,最终的效果如下(总感觉有点不和谐啊·): 对比能看出来的就是多了形状的选择还有使用图片了,那么接下来就是它的实现过程. 对自定义View实现还不明白的建议看下郭神的博客(View系列4篇): Android LayoutInflater原理分析,带你一步步深入了

ANDROID自定义视图——仿瀑布布局(附源码)

简介: 在自定义view的时候,其实很简单,只需要知道3步骤: 1.测量--onMeasure():决定View的大小 2.布局--onLayout():决定View在ViewGroup中的位置 3.绘制--onDraw():如何绘制这个View. 第3步的onDraw系统已经封装的很好了,基本不用我们来操心,只需要专注到1,2两个步骤就中好了. 第一步的测量,可以参考:(ANDROID自定义视图--onMeasure,MeasureSpec源码 流程 思路详解) 第二步的布局,可以参考:(AN

android 自定义toast width height 背景图片

Android 自定义toast 宽高大小 背景图片 RelativeLayout layout = (RelativeLayout) getLayoutInflater().inflate(R.layout.layout_custom_toast,null); ((TextView) layout.findViewById(R.id.tvCheckoutWay)).setText("11111"); ((TextView) layout.findViewById(R.id.tvPer

android 自定义view 前的基础知识

本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了解View(二) Android视图状态及重绘流程分析,带你一步步深入了解View(三) Android自定义View的实现方法,带你一步步深入了解View(四) 这位大哥的系列博文,相当于自己看这些的一个思考吧. 一.首先学layoutInflater. 相信接触Android久一点的朋友对于La

Android自定义view之view显示流程

自定义view之measure.layout.draw三大流程 一个view要显示出来,需要经过测量.布局和绘制这三个过程,本章就这三个流程详细探讨一下.View的三大流程具体分析起来比较复杂,本文不会从根源详细地分析,但是可以保证能达到实用的地步. 1. measure过程 1.1 理解MeasureSpec View的测量方法为public final void measure(int widthMeasureSpec, int heightMeasureSpec)和protected vo

Android——自定义View(学习Android开发与艺术探索)

ViewRoot和DecorView ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的.在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联. View的绘制流程从ViewRoot的performTraversals开始,经过

Android自定义view详解

从继承开始 懂点面向对象语言知识的都知道:封装,继承和多态,这是面向对象的三个基本特征,所以在自定义View的时候,最简单的方法就是继承现有的View 通过上面这段代码,我定义了一个SketchView,继承自View对象,并且复写了它的三个构造方法,我主要来分析一下这三个构造方法: 第一个构造方法就是我们普通在代码中新建一个view用到的方法,例如 SketchView sketchView = new SketchView(this); 就这样,一个自定义的view就被新建出来了,然后可以根