自定义UI 自定义布局

系列文章目录

  1. 自定义UI 基础知识
  2. 自定义UI 绘制饼图
  3. 自定义UI 圆形头像
  4. 自定义UI 自制表盘
  5. 自定义UI 简易图文混排
  6. 自定义UI 使用Camera做三维变换
  7. 自定义UI 属性动画
  8. 自定义UI 自定义布局

前言

这系列的文章主要是基于扔物线的HenCoderPlus课程的源码来分析学习。



这一篇文章主要介绍的是属性动画,更多细节请见以下文章:

如果大家有“财力”,建议支持下扔物线。大佬给我们提供了很详细的学习资源。

布局流程

部分内容摘录自:HenCoder Android 自定义 View 2-1 布局基础

简介

程序在运行时利用布局文件的代码来计算出实际尺寸的过程。

具体流程

以下讲的是自定义布局时,可能需要重写的一些方法,及其作用介绍。


流程流程名称流程说明
1onMeasure(int, int)调用以确定此视图及其所有子级的大小要求。
2onLayout(boolean, int, int, int, int)在此视图应为其所有子级分配大小和位置时调用。

还有一个比较关键的流程:我们需要在视图大小发生变化后,重新计算视图的参数。

流程流程名称流程说明
1onSizeChanged(int, int, int, int)在此视图的大小发生变化时调用。

实现源码

注意:自定义UI用到的方法,传递的参数单位都是像素(px),请注意单位的转换。

View#onMeasure

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    /**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overridden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

	/**
     * <p>This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.</p>
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    /**
     * Sets the measured dimension without extra processing for things like optical bounds.
     * Useful for reapplying consistent values that have already been cooked with adjustments
     * for optical bounds, etc. such as those from the measurement cache.
     *
     * @param measuredWidth The measured width of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     * @param measuredHeight The measured height of this view.  May be a complex
     * bit mask as defined by {@link #MEASURED_SIZE_MASK} and
     * {@link #MEASURED_STATE_TOO_SMALL}.
     */
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    	// 保存计算出来的值
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
}

View#onMeasure 方法的作用:

  • 计算视图的大小。
  • 调用 View#setMeasuredDimension 保存计算的尺寸。
    • setMeasuredDimension
    • setMeasuredDimensionRaw

View#onLayout

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
}

View#onMeasure 方法并没有做任何的布局操作。

ViewGroup#onMeasure

@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	
	// 没有重写 onMeasure 方法
}

ViewGroup#onLayout

@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

	// ViewGroup#onLayout 定义为 abstract 方法,意味着子类必须实现。
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

}

以下部分内容摘录自:HenCoder Android 自定义 View 2-1 布局基础

  • 让我们看看 android.widget.FrameLayout 的实现:
@RemoteView
public class FrameLayout extends ViewGroup {

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法,
		// 测量他们的尺寸并计算它们的位置;
	}    

	@Override
    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) {
		// 从上到下递归地调用每个 View 或者 ViewGroup 的 layout() 方法,
		// 把测得的它们的尺寸和位置赋值给它们。
	}
}

布局过程:

  1. 测量阶段 onMeasure:从上到下递归地调用每个 View 或者 ViewGroupmeasure() 方法,测量他们的尺寸并计算它们的位置;
  2. 布局阶段 onLayout:从上到下递归地调用每个 View 或者 ViewGrouplayout() 方法,把测得的它们的尺寸和位置赋值给它们。

onMeasure 入参解释

onMeasure 这两个参数表示父 View 对子 View 宽高的限制。

  • widthMeasureSpec :horizontal space requirements as imposed by the parent.
  • heightMeasureSpec :vertical space requirements as imposed by the parent.

自定义布局

自定义 onMeasure

关键内容

  1. 了解自定义 View#onMeasure 流程。
  2. 通过 View#setMeasuredDimension 修改 View#onMeasure 计算出来的视图宽高。

这部分内容也可以看扔物线介绍:HenCoder Android 自定义 View 2-1 布局基础

示意图 示意图
正方形 ImageView
第一张:宽度100dp,高度200dp
第二张:宽度300dp,高度100dp
<LinearLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:gravity="center"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.hencoder.a10_layout.view.SquareImageView
        android:layout_width="100dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/avatar_rengwuxian" />

</LinearLayout>
<LinearLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:gravity="center"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.hencoder.a10_layout.view.SquareImageView
        android:layout_width="300dp"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/avatar_rengwuxian" />

</LinearLayout>

/**
 * 自定义正方形 ImageView
 */
public class SquareImageView extends AppCompatImageView {

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 使用默认的计算流程,计算出视图的宽高
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取通过 super.onMeasure 计算出来的视图的宽高
        // super.onMeasure 计算视图宽高
        // -> setMeasuredDimension 保存视图宽高
        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();
        // 找出最大值
        int size = Math.max(measuredWidth, measuredHeight);

        // 更新视图的宽高
        setMeasuredDimension(size, size);
    }
}

完全自定义 onMeasure

关键内容

  1. View#onMeasure 中,计算完毕后,使用 View#setMeasuredDimension 保存计算出来的视图宽高。
  2. 计算出来结果后,需要使用 View#resolveSize 修正结果,确保符合父View的限制。

这部分内容也可以看扔物线介绍:HenCoder Android 自定义 View 2-2 全新定义 View 的尺寸

先上效果图:

示意图
public class CircleView extends View {
    // 圆形的半径
    private static final int RADIUS = (int) Utils.dpToPixel(80);
    // 内圆距离父 View 的 Padding
    private static final int PADDING = (int) Utils.dpToPixel(30);

    // 抗锯齿
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = (PADDING + RADIUS) * 2;
        int height = (PADDING + RADIUS) * 2;

        // 根据布局的要求计算出合适的视图宽高
        width = resolveSize(
                width,              // 计算出来的宽度
                widthMeasureSpec    // 父View对子View的限制
        );
        height = resolveSize(
                height,             // 计算出来的高度
                widthMeasureSpec    // 父View对子View的限制
        );
        // 保存计算出来的视图宽高
        setMeasuredDimension(width, height);
    }

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

        // 背景
        canvas.drawColor(Color.RED);
        // 内部圆
        canvas.drawCircle(
                PADDING + RADIUS,
                PADDING + RADIUS,
                RADIUS,
                paint
        );
    }
}

上述代码中,完全自定义了 View#onMeasure 方法,自行定义了 CircleView 视图的宽高,并通过 View#resolveSize 修正了计算出来的结果,使之符合父 View 的要求。最后,通过 View#setMeasuredDimension 方法保存了视图的最终宽高。


我们来看下 View#resolveSize 的源码:

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	/**
     * Version of {@link #resolveSizeAndState(int, int, int)}
     * returning only the {@link #MEASURED_SIZE_MASK} bits of the result.
     */
    public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }

    /**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size
     * is imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
     * resulting size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be.
     * @param measureSpec Constraints imposed by the parent.
     * @param childMeasuredState Size information bit mask for the view's
     *                           children.
     * @return Size information bit mask as defined by
     *         {@link #MEASURED_SIZE_MASK} and
     *         {@link #MEASURED_STATE_TOO_SMALL}.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

	public static class MeasureSpec {
        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
	}
}

源码中包含了位运算,看起来会比较吃力。我先把相关的位运算去掉,直接看核心逻辑:

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

    public static int resolveSizeAndState(int size, int measureSpec) {
        final int specMode = View.MeasureSpec.getMode(measureSpec);
        final int specSize = View.MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            // 父View限制了子View的最大值
            case View.MeasureSpec.AT_MOST:
                result = Math.min(specSize, size);
                break;
            // 父View给子View具体的宽/高度
            case View.MeasureSpec.EXACTLY:
            	// 计算的值白算了,只能用父View给的值
                result = specSize;
                break;
            // 父View没有限制,想多宽/高就多宽/高
            case View.MeasureSpec.UNSPECIFIED:
            default:
            	// 想要多少就多少
                result = size;
        }
        return result;
    }
}

结合HenCoder Android 自定义 View 2-2 全新定义 View 的尺寸文章总结下:

  • 父View对子View限制的原因:
    • 开发者的要求(布局文件中 layout_ 打头的属性)经过父 View 处理计算后的更精确的要求。
  • 限制的分类:
    • UNSPECIFIED:不限制。
    • EXACTLY:限制固定值。
    • AT_MOST:限制最大值。

自定义 TagLayout

结合 HenCoder UI 2-3 定制 Layout 的内部布局 看,体验更佳,o( ̄▽ ̄)d。

按照惯例,线上效果图:

  1. 基于 rengwuxian/HenCoderPlus
    1. ColoredTextView.java:文本标签。
    2. TagLayout.java:布局实现。支持标签换行和标签 layout_margin 属性。
  2. 这个示例已经支持了子 Viewlayout_margin 属性。

示意图

实现难点:

  1. 确认绘制 TagLayout 需要的宽高。
  2. 确保 Tag 标签在合适的时机折行。
示意图

onMeasure实现思路

  1. 获取父类对 TagLayout 要求的宽度 W l i m i t W_{limit} Wlimit、高度和 Mode
    1. android.view.View.MeasureSpec#getMode:测量模式。
    2. android.view.View.MeasureSpec#getSize :要求的视图尺寸。
  2. 获取子 View 的测量宽度,判断是否需要折行。
    1. 获取子 View 测量宽度 W c h i l d W_{child} Wchildandroid.view.ViewGroup#measureChildWithMargins
    2. 判断折行的前提条件:MeasureSpec.AT_MOSTMeasureSpec.EXACTLY
      1. MeasureSpec.UNSPECIFIED 表示对子 View 的尺寸不做限制,不需要折行。
    3. 折行的判断条件:
      1. 令当前行已使用的宽度为: W u s e d W_{used} Wused
      2. W u s e d + W c h i l d > W l i m i t W_{used} + W_{child} > W_{limit} Wused+Wchild>Wlimit 时,需要发生折行。
  3. 计算 TagLayout 的视图宽高。
    1. 计算出 TagLayout 每一行使用的宽度,保留最大值作为 TagLayout 的宽度。
      1. 令第 n n n行的宽度为: w n w_n wn
      2. TagLayout 的高度 W = max ⁡ x ∈ w 1 , w 2 , ⋯   , w n f ( x ) W = \max \limits_{x \in {w_1, w_2, \cdots ,w_n}}f(x) W=xw1,w2,,wnmaxf(x)
    2. 计算出 TagLayout 每一行中 Tag 的最大高度,累加值就是 TagLayout 的高度。
      1. 比如上图中上海市是第一行高度最大的 Tag ,行高 h = B y − A y h = B_y - A_y h=ByAy
      2. TagLayout 的高度 H = h 1 + h 2 + ⋯ + h n H = h_1 + h_2 + \cdots + h_n H=h1+h2++hn
  4. 保存测量值:android.view.View#setMeasuredDimension

onLayout实现思路

按照计算出来的结果,挨个布局。(最难的还是 onMeasure,╮(╯▽╰)╭)

小细节

使用 android.view.ViewGroup#measureChildWithMargins 方法,需要重写以下方法,不然会发生崩溃:

public class TagLayout extends ViewGroup {
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

具体崩溃栈如下所示:

Process: com.hencoder.a10_layout, PID: 19744
    java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6948)
        at com.hencoder.a10_layout.view.TagLayout.onMeasure(TagLayout.java:33)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1204)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:723)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:143)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at androidx.appcompat.widget.ActionBarOverlayLayout.onMeasure(ActionBarOverlayLayout.java:401)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at com.android.internal.policy.DecorView.onMeasure(DecorView.java:747)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3397)
        at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2228)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2486)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        at android.view.Choreographer.doFrame(Choreographer.java:731)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)


android.view.ViewGroup#measureChildWithMargins 源码解析:

@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    private static final String TAG = "ViewGroup";
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        		// 去除当前View的Padding和子View的margin值
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        		// 去除当前View的Padding和子View的margin值
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

		// 在计算的时候,将View的margin、padding都去掉了。
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            ...省略...
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            ...省略...
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            ...省略...
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}

measureChildWithMargins 源码收获:

  1. 计算时去掉了当前 ViewPadding 值和子 Viewmargin 值。这意味着我们需要在自定义 onMeasure 的时候,将当前 ViewPadding 值和子 Viewmargin 值考虑进去。

上面的结论也是我下面代码这么写的原因:

public class TagLayout extends ViewGroup {
	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        
        ...省略...

        int childWith, childHeight;
        // 计算子View的宽高
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 需要重写android.view.ViewGroup.generateLayoutParams(android.util.AttributeSet)方法
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            // 获取margin值
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            childWidth = child.getMeasuredWidth() + params.leftMargin;
            childHeight = child.getMeasuredHeight() + params.topMargin;

			...省略...
					// 绘制当前child视图需要的宽高没有超出父View要求的宽度
                    if (lineWidthUsed + childWidth + params.rightMargin <= specWidth) {
                        break;
                    }
            ...省略...
    	}
	}
}

附录

源码

TagLayout

原代码出处:TagLayout.java

public class TagLayout extends ViewGroup {
    List<Rect> childrenBounds = new ArrayList<>();

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 整体宽度
        int widthUsed = 0;
        // 整体高度
        int heightUsed = 0;
        // 当前行使用的宽度
        int lineWidthUsed = 0;
        // 当前行最大高度
        int lineMaxHeight = 0;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specWidth = MeasureSpec.getSize(widthMeasureSpec);

        int childWidth, childHeight;
        // 计算子View的宽高
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 需要重写android.view.ViewGroup.generateLayoutParams(android.util.AttributeSet)方法
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            // 获取margin值
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            childWidth = child.getMeasuredWidth() + params.leftMargin;
            childHeight = child.getMeasuredHeight() + params.topMargin;
            // 换行逻辑
            switch (specMode) {
                case MeasureSpec.AT_MOST:
                case MeasureSpec.EXACTLY:
                    // 绘制当前child视图需要的宽高没有超出父View要求的宽度
                    if (lineWidthUsed + childWidth + params.rightMargin <= specWidth) {
                        break;
                    }
                    // 换行逻辑
                    lineWidthUsed = 0;
                    // 计算父View的最大高度
                    heightUsed += lineMaxHeight;
                    lineMaxHeight = 0;
                    // 重新计算子View的宽高
                    measureChildWithMargins(
                            child,
                            widthMeasureSpec,
                            0,
                            heightMeasureSpec,
                            heightUsed
                    );
                    break;
                case MeasureSpec.UNSPECIFIED:
                default:
                    break;
            }
            Rect childBound;
            if (childrenBounds.size() <= i) {
                childBound = new Rect();
                childrenBounds.add(childBound);
            } else {
                childBound = childrenBounds.get(i);
            }
            childBound.set(
                    lineWidthUsed + params.leftMargin,
                    heightUsed + params.topMargin,
                    lineWidthUsed + childWidth,
                    heightUsed + childHeight
            );
            // 父View已经使用的视图宽度
            lineWidthUsed += childWidth + params.rightMargin;
            // 计算父View的最大宽度
            widthUsed = Math.max(widthUsed, lineWidthUsed);
            // 一行中,存在多种高度的View,需要每次循环都更新一遍,确保数据准确。
            lineMaxHeight = Math.max(lineMaxHeight, childHeight + params.bottomMargin);
        }

        int width = widthUsed;
        // 补充最后一行的高度
        int height = heightUsed + lineMaxHeight;
        // 保存当前视图宽高
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Rect childBounds;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            childBounds = childrenBounds.get(i);
            // 按照计算出来的结果,挨个布局
            child.layout(
                    childBounds.left,
                    childBounds.top,
                    childBounds.right,
                    childBounds.bottom
            );
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        // 使用View#measureChildWithMargins方法,需要重写该方法。
        return new MarginLayoutParams(getContext(), attrs);
    }
}

这里附上xml部分配置:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff"
    android:gravity="center_horizontal"
    android:padding="16dp"
    tools:context=".MainActivity">

    <com.hencoder.a10_layout.view.TagLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.hencoder.a10_layout.view.ColoredTextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="6dp"
            android:text="北京市" />
		
		...省略...

    </com.hencoder.a10_layout.view.TagLayout>
</LinearLayout>

ColoredTextView

public class ColoredTextView extends AppCompatTextView {
    private static final int[] COLORS = {
            Color.parseColor("#E91E63"),
            Color.parseColor("#673AB7"),
            Color.parseColor("#3F51B5"),
            Color.parseColor("#2196F3"),
            Color.parseColor("#009688"),
            Color.parseColor("#FF9800"),
            Color.parseColor("#FF5722"),
            Color.parseColor("#795548")
    };
    private static final int[] TEXT_SIZES = {
            16, 22, 28
    };
    private static final Random random = new Random();
    private static final int CORNER_RADIUS = (int) Utils.dpToPixel(4);
    private static final int X_PADDING = (int) Utils.dpToPixel(4);
    private static final int Y_PADDING = (int) Utils.dpToPixel(2);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

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

    {
        setTextColor(Color.WHITE);
        setTextSize(TEXT_SIZES[random.nextInt(3)]);
        paint.setColor(COLORS[random.nextInt(COLORS.length)]);
        setPadding(X_PADDING, Y_PADDING, X_PADDING, Y_PADDING);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRoundRect(0, 0, getWidth(), getHeight(), CORNER_RADIUS, CORNER_RADIUS, paint);
        super.onDraw(canvas);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值