自定义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_{chi

      以上是关于自定义UI 自定义布局的主要内容,如果未能解决你的问题,请参考以下文章

      自定义UI 自定义布局

      自定义UI 自定义布局

      自定义UI 自定义布局

      ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局

      片段中的自定义列表视图。未找到布局

      ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局