自定义UI 自定义布局

Posted Notzuonotdied

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义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 W以上是关于自定义UI 自定义布局的主要内容,如果未能解决你的问题,请参考以下文章

      自定义UI 自定义布局

      自定义UI 自定义布局

      自定义UI 自定义布局

      由于自动布局没有计算尺寸,将子视图添加到自定义视图最终会出错

      Android自定义View之自定义一个简单的阶梯式布局

      Android自定义View之自定义一个简单的阶梯式布局