自定义控件基础
自定义控件基础
自定义 View 的基本方法
自定义 View 的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw();
View 在 Activity 中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout 和 draw。
onMeasure 测量
onMeasure() 决定 View 的大小;
onLayout
onLayout() 决定 View 在 ViewGroup 中的位置
onDraw
onDraw() 决定绘制这个 View
无论是 measure 过程、layout 过程还是 draw 过程,永远都是从 View 树的根节点开始测量或计算(即从
树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个 View 树中各个 View,最终确
定整个 View 树的相关属性。
View 视图结构
- PhoneWindow 是 Android 系统中最基本的窗口系统,继承自 Windows 类,负责管理界面显示以及事件响应。它是 Activity 与 View 系统交互的接口
- DecorView 是 PhoneWindow 中的起始节点 View,继承于 View 类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个 FrameLayout
- ViewRoot 在 Activtiy 启动时创建,负责管理、布局、渲染窗口 UI 等等
多 View 结构,结构是树形结构:最顶层是 ViewGroup,ViewGroup 下可能有多个 ViewGroup 或 View,如下图:
坐标系
Android 的坐标系定义为:
- 屏幕左上角为坐标原点
- 向右 x 轴增大
- 向下 y 轴增大
视图坐标系/View 坐标系/Canvas 坐标系
View 左上角,不变
绘图坐标系
canvas.translate 会变化
LayoutParams
LayoutParams 翻译过来就是布局参数,子 View 通过 LayoutParams 告诉父容器(ViewGroup)应该如何放置自己。
从这个定义中也可以看出来 LayoutParams 与 ViewGroup 是息息相关的,因此脱离 ViewGroup 谈 LayoutParams 是没
有意义的。
事实上,每个 ViewGroup 的子类都有自己对应的 LayoutParams 类,典型的如 LinearLayout.LayoutParams 和
FrameLayout.LayoutParams 等,可以看出来 LayoutParams 都是对应 ViewGroup 子类的内部类
LayoutParams
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// xml提供width/height(从layout_width和layout_height)
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
// 手动指定width和height
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
// clone已有的
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
MarginLayoutParams
MarginLayoutParams 是和外间距有关的。事实也确实如此,和 LayoutParams 相比,MarginLayoutParams 只是增
加了对上下左右外间距的支持。实际上大部分 LayoutParams 的实现类都是继承自 MarginLayoutParams,因为基本
所有的父容器都是支持子 View 设置外间距的
- 间隔属性优先级问题
在构造方法中,先是获取了 margin 属性;如果该值不合法,就获取 horizontalMargin;如果该值不合法,再去获取 leftMargin 和
rightMargin 属性(verticalMargin、topMargin 和 bottomMargin 同理)。我们可以据此总结出这几种属性的优先级
margin > horizontalMargin 和 verticalMargin > leftMargin 和 RightMargin、topMargin 和 bottomMargin
- 属性覆盖问题
优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释
LayoutParams 与 View 如何建立联系
1. xml 定义 view
root 不为 null 时,会用 root 的 generateLayoutParams 设置给 view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// LayoutInflater
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
if (TAG_MERGE.equals(name)) {
// ...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// ...
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
}
2. 代码 addView 方式
如果 child 存在 layoutParams 复用这个,如果没有,调用 ViewGroup#generateDefaultLayoutParams 生成默认 LayoutParams
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ViewGroup
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
自定义 LayoutParams
- 创建自定义属性
1
2
3
4
5
6
7
8
<resource>
<declare-styleable name="xxxViewGroup_Layout">
<!-- 自定义的属性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系统预置的属性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
- 继承 MarginLayout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FLowLayoutLayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析布局属性
TypedArray typedArray = c.obtainStyledAttributes(attrs,
R.styleable.SimpleViewGroup_Layout);
simpleAttr =
typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity,
-1);
typedArray.recycle();//释放资源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
- 重写 ViewGroup 中几个与 LayoutParams 相关的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// LayoutInflater用到,xml布局用到
override fun generateLayoutParams(attrs: AttributeSet?): FLowLayoutLayoutParams {
return FLowLayoutLayoutParams(context, attrs)
}
// 生成默认的LayoutParams, addView没有设置LayoutParams,调用该方法生成默认的
override fun generateDefaultLayoutParams(): FLowLayoutLayoutParams {
return FLowLayoutLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
// 对传入的LayoutParams进行转化
override fun generateLayoutParams(p: LayoutParams?): FLowLayoutLayoutParams? {
return FLowLayoutLayoutParams(p)
}
// 检查LayoutParams是否合法
override fun checkLayoutParams(p: LayoutParams?): Boolean {
return p is FLowLayoutLayoutParams && super.checkLayoutParams(p)
}
LayoutParams 常见的子类
在为 View 设置 LayoutParams 的时候需要根据它的父容器选择对应的 LayoutParams,否则结果可能与预期不一致,
这里简单罗列一些常见的 LayoutParams 子类:
1
2
3
4
5
6
7
8
9
ViewGroup.MarginLayoutParams
FrameLayout.LayoutParams
LinearLayout.LayoutParams
RelativeLayout.LayoutParams
RecyclerView.LayoutParams
GridLayoutManager.LayoutParams
StaggeredGridLayoutManager.LayoutParams
ViewPager.LayoutParams
WindowManager.LayoutParams
MeasureSpec
什么是 MeasureSpec
MeasureSpec 测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize 是指
在某种 SpecMode 下的参考尺寸,其中 SpecMode 有如下三种:
- EXACTLY 父控件已经知道你所需的精确大小,你的最终大小就是这么大
- AT_MOST 你的大小不能大于父控件给你指定的 size,具体是多少,得看自己的实现
- UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,一般用于系统内部
MeasureSpec 组成
由一个 int 类型的值组成,最高两位是 mode,后面 30 位是 size
- 获取 mode 和 size MeasureSpec.getMode() MeasureSpec.getSize()
其原理,就是通过
1
2
private static final int MODE_SHIFT = 30; // 30,表示的是30位size
private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 二进制11,左移30位,就是1100 0000 0000 0000 0000 0000 0000 0000(后面30位0)
getMode() 就是通过 MODE_MASK 与&运算,保留 mode 的两位,size 位全部为 0
1
2
3
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
getSize(),就是通过 ~MODE_MASK
,反码,得到 30 位的 size
1
2
3
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec 和 LayoutParams 关系
ViewGroup 的静态方法 getChildMeasureSpec,会将子 view 的 LayoutParams 参数转化为 MeasureSpec
1
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {}
自定义属性
见 自定义属性
ViewGroup 生命周期
getMeasureWidth 与 getWidth 的区别
- getMeasureWidth 在 measure() 过程结束后就可以获取到对应的值; 通过 setMeasuredDimension() 方法来进行设置的.
- getWidth 在 layout() 过程结束后才能获取到; 通过视图右边的坐标减去左边的坐标计算出来的.
View 树绘制流程
View 树由谁负责 ViewRoot
view 树的绘制流程是通过 ViewRoot 去负责绘制的,ViewRoot 这个类的命名有点坑,最初看到这个名字,翻译过来是
view 的根节点,但是事实完全不是这样,ViewRoot 其实不是 View 的根节点,它连 view 节点都算不上,它的主要作用
是 View 树的管理者,负责将 DecorView 和 PhoneWindow” 组合 “ 起来,而 View 树的根节点严格意义上来说只有
DecorView;每个 DecorView 都有一个 ViewRoot 与之关联,这种关联关系是由 WindowManager 去进行管理的;
View 的添加
View 的绘制流程
measure 流程
1
2
3
4
5
6
7
8
9
10
// ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查发起布局请求的线程是否为主线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
layout 流程
View 执行 onMeasure/onLayout 的次数 (measure 重入规则)
分析 ViewRootImpl 的源码,scheduleTraversales() 内部会执行 postCallBack 触发 mTraversalRunnable 重新走一遍 performTraversals(),第二次执行 performTraversals() 就会触发 performDraw()。所以 performTraversals() 走了两次,那么肯定会走 2 次 measure 方法,但不一定走 2 次 onMeasure(),读过 View measure 方法源码的都应知道 measure 方法做了 2 级测量优化:
- 如果 flag 不为 forceLayout 或者与上次测量规格(MeasureSpec)相比未改变,那么将不会进行重新测量(执行 onMeasure 方法),直接使用上次的测量值;
- 如果满足非强制测量的条件,即前后二次测量规格不一致,会先根据目前测量规格生成的 key 索引缓存数据,索引到就无需进行重新测量; 如果 targetSDK 小于 API 20 则二级测量优化无效,依旧会重新测量,不会采用缓存测量值。
draw 流程
onDraw() 和 dispatchDraw() 的区别
- onDraw
绘制 View 本身的内容,通过调用 View.onDraw(canvas) 函数实现 - dispatchDraw 绘制自己的孩子通过 dispatchDraw(canvas)实现
draw 过程会调用 onDraw(Canvas canvas) 方法,然后就是 dispatchDraw(Canvas canvas) 方法, dispatchDraw() 主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是 onDraw() 方法。值得注意的是 ViewGroup 容器组件的绘制,当它没有背景时直接调用的是 dispatchDraw() 方法, 而绕过了 draw() 方法,当它有背景的时候就调用 draw() 方法,而 draw() 方法里包含了 dispatchDraw() 方法的调用。因此要在 ViewGroup 上绘制东西的时候往往重写的是 dispatchDraw() 方法而不是 onDraw() 方法,或者自定制一个 Drawable,重写它的 draw(Canvas c) 和 getIntrinsicWidth() 方法,然后设为背景。
requestLayout/invalidate/postInvalidate
invalidate 和 postInvalidate 区别
二者都会出发刷新 View,并且当这个 View 的可见性为 VISIBLE 的时候,View 的 onDraw() 方法将会被调用,invalidate() 方法在 UI 线程中调用,重绘当前 UI。postInvalidate() 方法在非 UI 线程中调用,通过 Handler 通知 UI 线程重绘。
requestLayout
requestLayout() 也可以达到重绘 view 的目的,但是与前两者不同,它会先调用 onLayout() 重新排版,再调用 ondraw() 方法。当 view 确定自身已经不再适合现有的区域时,该 view 本身调用这个方法要求 parent view(父类的视图)重新调用他的 onMeasure、onLayout 来重新设置自己位置。特别是当 view 的 layoutparameter 发生改变,并且它的值还没能应用到 view 上时,这时候适合调用这个方法 requestLayout()。
如何在 onCreate 中拿到 View 的宽度和高度
- View.post(runnable)
1
2
3
4
5
6
7
8
9
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth();
int measuredWidth = view.getMeasuredWidth();
Log.i(TAG, "width: " + width);
Log.i(TAG, "measuredWidth: " + measuredWidth);
}
});
利用 Handler 通信机制,发送一个 Runnable 到 MessageQueue 中,当 View 布局处理完成时,自动发送消息,通知 UI 进程。借此机制,巧妙获取 View 的高宽属性,代码简洁,相比 ViewTreeObserver 监听处理,还不需要手动移除观察者监听事件。
- ViewTreeObserver.addOnGlobalLayoutListener()
监听 View 的 onLayout() 绘制过程,一旦 layout 触发变化,立即回调 onLayoutChange 方法。
注意,使用完也要主要调用 removeOnGlobalListener() 方法移除监听事件。避免后续每一次发生全局 View 变化均触发该事件,影响性能。
1
2
3
4
5
6
7
8
9
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
Log.i(TAG, "width: " + view.getWidth());
Log.i(TAG, "height: " + view.getHeight());
}
});
View 测量布局绘制总结
invalidate/requestLayout
invalidate() 不能在子线程调用?
可以,具体分 Android8.0,ViewRootImpl 是否创建,是否硬件加速来看
requestLayout 会不会触发 performDraw?不一定触发 onDraw
会,performDraw 执行的条件很简单,只要 window 可见且调用 performTraversals 则执行
1
2
3
4
5
6
7
8
9
10
11
12
13
// ViewRootImpl
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
invalidate 会不会触发 performMeasure、performLayout?
不会
requestLayout 触发 performMeasure、performLayout 的原因?
当 mLayoutRequested 为 true 时会执行 performMeasure 和 performLayout,而 requestLayout 会将 mLayoutRequested 设置为 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ViewRootImpl
...
if (layoutRequested) {
...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...
}
draw
view.setLayerType(LAYER_TYPE_SOFTWARE, null) 真的关闭了硬件加速吗?
不是,view.setLayerType 只是将当前 view 的 layerType 设置为了 LAYER_TYPE_SOFTWARE,并不能改变硬件加速的事实,android 也未提供 view 层面的关闭硬件加速的方法。硬件加速下当 view 的 layerType 为 LAYER_TYPE_SOFTWARE 时,view 走的是软件图层绘制的方案,即使用 bitmap 绘制挂载在 displayList 下,即不需要构建 DrawOp 且 GPU 绘制则不需要执行此 view 的构建命令,直接绘制 bitmap,从而间接实现当前 view 绕过硬件绘制(gpu 绘制)。
硬件加速
view.invalidate,重叠的脏数据区域的 view,不管是父 view 还是兄弟 view 在软件绘制下都会重试,而硬件绘制下,只会更新对应的 RenderNode
硬件加速,属性动画设置特定属性(例如 Alpha 或旋转)不需要重绘?
属性动画的 alpha 和旋转动等动画并未改变 view 的属性,只是改变了 view 的相关属性,跟着源码来看或许更能清晰的认知这一过程,以 setAlpha 为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
ensureTransformationInfo();
if (mTransformationInfo.mAlpha != alpha) {
setAlphaInternal(alpha);
if (onSetAlpha((int) (alpha * 255))) {
mPrivateFlags |= PFLAG_ALPHA_SET;
// subclass is handling alpha - don't optimize rendering cache invalidation
invalidateParentCaches();
invalidate(true);
} else {
mPrivateFlags &= ~PFLAG_ALPHA_SET;
invalidateViewProperty(true, false);
//重新设置renderNode的alpha值
mRenderNode.setAlpha(getFinalAlpha());
}
}
}