自定义UI 自定义布局
Posted Notzuonotdied
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义UI 自定义布局相关的知识,希望对你有一定的参考价值。
系列文章目录
文章目录
前言
这系列的文章主要是基于扔物线的HenCoderPlus课程的源码来分析学习。
- 扔物线课程源码:(这一块涉及的代码比较多)
- SquareImageView.java:自定义正方形
ImageVIew
。 - CircleView.java:自定义圆形
View
。 - ColoredTextView.java
- TagLayout.java
- OneHundredView.java:没看明白干啥用的 ╮(╯▽╰)╭
- SquareImageView.java:自定义正方形
- android官方文档:
这一篇文章主要介绍的是属性动画,更多细节请见以下文章:
- HenCoder Android 自定义 View 2-1 布局基础
- 对应练习项目:hencoder/PracticeLayout1
- 对应视频地址:HenCoder Android 布局 2-1
- HenCoder Android 自定义 View 2-2 全新定义 View 的尺寸
- 对应练习项目:╮(╯▽╰)╭
- 对应视频地址:HenCoder UI-2-2
- HenCoder Android 自定义 View 2-3 定制 Layout 的内部布局
- 对应练习项目:╮(╯▽╰)╭
- 对应视频地址:HenCoder UI 2-3 定制 Layout 的内部布局
如果大家有“财力”,建议支持下扔物线。大佬给我们提供了很详细的学习资源。
布局流程
简介
程序在运行时利用布局文件的代码来计算出实际尺寸的过程。
具体流程
以下讲的是自定义布局时,可能需要重写的一些方法,及其作用介绍。
流程 | 流程名称 | 流程说明 |
---|---|---|
1 | onMeasure(int, int) | 调用以确定此视图及其所有子级的大小要求。 |
2 | onLayout(boolean, int, int, int, int) | 在此视图应为其所有子级分配大小和位置时调用。 |
还有一个比较关键的流程:我们需要在视图大小发生变化后,重新计算视图的参数。
流程 | 流程名称 | 流程说明 |
---|---|---|
1 | onSizeChanged(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() 方法,
// 把测得的它们的尺寸和位置赋值给它们。
布局过程:
- 测量阶段
onMeasure
:从上到下递归地调用每个View
或者ViewGroup
的measure()
方法,测量他们的尺寸并计算它们的位置; - 布局阶段
onLayout
:从上到下递归地调用每个View
或者ViewGroup
的layout()
方法,把测得的它们的尺寸和位置赋值给它们。
onMeasure 入参解释
onMeasure
这两个参数表示父 View
对子 View
宽高的限制。
widthMeasureSpec
:horizontal space requirements as imposed by the parent.heightMeasureSpec
:vertical space requirements as imposed by the parent.
自定义布局
自定义 onMeasure
关键内容:
- 了解自定义
View#onMeasure
流程。 - 通过
View#setMeasuredDimension
修改View#onMeasure
计算出来的视图宽高。
正方形 ImageView这部分内容也可以看扔物线介绍:HenCoder Android 自定义 View 2-1 布局基础。
第一张:宽度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
关键内容:
View#onMeasure
中,计算完毕后,使用View#setMeasuredDimension
保存计算出来的视图宽高。- 计算出来结果后,需要使用
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。
按照惯例,线上效果图:
- 基于 rengwuxian/HenCoderPlus :
- ColoredTextView.java:文本标签。
- TagLayout.java:布局实现。支持标签换行和标签
layout_margin
属性。
- 这个示例已经支持了子
View
的layout_margin
属性。
实现难点:
- 确认绘制
TagLayout
需要的宽高。 - 确保
Tag
标签在合适的时机折行。
onMeasure实现思路
- 获取父类对
TagLayout
要求的宽度 W l i m i t W_limit Wlimit、高度和Mode
。android.view.View.MeasureSpec#getMode
:测量模式。android.view.View.MeasureSpec#getSize
:要求的视图尺寸。
- 获取子
View
的测量宽度,判断是否需要折行。- 获取子
View
测量宽度 W c h i l d W_child W以上是关于自定义UI 自定义布局的主要内容,如果未能解决你的问题,请参考以下文章
- 获取子