admin管理员组文章数量:1534198
《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。张三丰又慢吞吞使了一遍,问他记住多少,张无忌说只记得几招了。张三丰最后又示范了一遍,张无忌想了想说,这次全忘光了。张三丰很满意,于是放心让张无忌与八臂神剑去比试。
首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想放弃,想放弃的原因不是我自己没有弄清楚,而是觉得自己叙事脉络已经紊乱了,感觉无法让读者整明白,怕把读者带到沟里面去,怕自己让人觉得罗嗦废话。但最后,我决定还是坚持下去,因为在反复纠结 –> 不甘 –> 探索 –> 论证 –> 质疑的过程循环中,我完成了对自己的升华,弄明白长久以来的一些困惑。所以,此文最大的目的是给自己作为一些学习记录,如果有幸帮助你解决一些困惑,那么我心宽慰。如果有错的地方,也欢迎指出批评。
如果你有这样的困扰:
1. 一个 View 的 parent 一定是 ViewGroup 吗?
Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。
Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。
在 xml 中设置一个 View 的属性 layout_width 为 wrap_content 或者 match_parent 而不是具体数值 50dp 时,为什么 view 也有正常的尺寸。
你或多或者知道 Android 测量时的 3 种布局模式:MeasureSpec.EXACTLY、Measure.AT_MOST、Measure.UNSPECIFIED。但你不大能够把握它们。
你不但对自定义 View 没有问题,对于自定义 ViewGroup 也不在话下,你明白 Android 给出的 3 种测量模式的含义,但是你还是没有来得及去思考,3 种测量模式本身是什么。
你也许没有想过 Activity 最外层的 View 是什么。
你也许知道 Activity 最外层的 View 叫做 DecorView。明白它与 PhoneWindow 及 Activity.setContentView() 的联系。但你不知道谁对 DecorView 进行了尺寸测量。
好了,文章正式开始。请深吸一口气,take it easy!
无法忽视的自定义 View 话题
Android 应用层开发绕不开自定义 View 这个话题,在 Android 中官方称之为 Widget,所以本文中的 View 其实与 Widget也就一个意思。虽然现在 Github 上有形形色色的开源库供大家使用,但是作为一名有想法的开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,如果现有的 Widget 无法完成任务,那么我们就应该想到要自定义一个 View 了。
我们或多或少知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
- 测量 onMeasure()
- 布局 onLayout()
- 绘制 onDraw()
没有办法说这三个阶段,那个阶段最重要,只是相对而言,测量阶段对于大多开发者而言难度相对其它两个要大,处理的细节也要多得多,自定义一个 View,正确的测量是第一步,正因为如此今天本文的主题就是讨论 View 中的测量机制和细节。
测量 View 就是测量一个矩形
得益于人们的想象力,Android 系统平台上出现了各种各样的 View。有 Button、TextView、ListView 等系统自带的组件,也有更多开发者自定义的 View。
上面是 Android 系统自带的 Widget 表现,它们用来完成不同功能的交互与效果展示,但对于开发者而言,上面的界面还有这样的一面。
透过另一个视角来观察,所有的 Widget
世界万物都有某些运行的规则,或者是突破不了的樊篱。对于一个 View 而言,它本质上就是一个矩形,一块四方的区域,铺开一张画布,然后利用所有的资源,在现有的规则之下天马行空。
因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!
既然是矩形,那么它肯定有明确的宽高和位置坐标。宽高是在测量阶段得出,然后在布局阶段,根据实际需要确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,它是画家,这个我们很放心。
打个比方,政府做城市规划时,房地产商们告诉政府他们希望的用地面积,政府综合政策和用地面积的实际情况,给地产商划分土地面积,地图上就是一个个圈圈。地产商们拿到明确的地域范围信息后,在规定好的区域建造自己的高楼或者大厦。而自定义 View 就是拿到这个类似政府规划的区域范围参数,只不过现实世界中,政府规划给地产商的土地不一定是四四方方的矩形,但是在 Android 中 View 拿到的区域一定是矩形。
好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。
View 设置尺寸的基本方法
接下来的过程,我将会用一系列比较细致的实验来说明问题,觉得罗嗦无聊的同学可以直接跳过这一小节。
我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
这样屏幕上就出现了一个按钮。
我们再把宽高固定。
<Button
android:layout_width="200dp"
android:layout_height="50dp"
android:text="test"/>
再换一种情况,将按钮的宽度由父容器决定
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="test"/>
上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。
- match_parent 代表这个维度上的值与父窗口一样
- wrap_content 表示这个维度上的值由 View 本身的内容所决定
- 具体数值如 5dp 表示这个维度上 View 给出了精确的值。
实验1
我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。
实验2
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
实验3
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
实验4
<RelativeLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="1000dp"
android:layout_height="wrap_content"
android:text="test"/>
</RelativeLayout>
似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园问光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。
Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。
山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。
那么,它们的协议是什么?
View 和 ViewGroup 之间的测量协议 MeasureSpec
我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。
public class TestView extends View {
public TestView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。
它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。
MeasureSpec
MeasureSpec 是 View.java 中一个静态类
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
......
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
......
}
MeasureSpec 的代码并不是很多,它有最重要的三个静态常量和三个最重要的静态方法。
MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST
MeasureSpec.makeMeasureSpec()
MeasureSpec.getMode()
MeasureSpec.getSize()
MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。
通过 makeMeasureSpec() 方法将 Mode 和 Size 组合成一个 measureSpec 数值。
而通过 getMode() 和 getSize() 却可以逆向地将一个 measureSpec 数值解析出它的 Mode 和 Size。
下面讲解 MeasureSpec 的 3 种测量模式。
MeasureSpec.UNSPECIFIED
此种模式表示无限制,子元素告诉父容器它希望它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。
MeasureSpec.EXACTLY
此模式说明可以给子元素一个精确的数值。
<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="test"/>
当 layout_width 或者 layout_height 的取值为 match_parent 或者 明确的数值如 100dp 时,表明这个维度上的测量模式就是 MeasureSpec.EXACTLY。为什么 match_parent 也有精确的值呢?我们可以合理推断一下,子 View 希望和 父 ViewGroup 一样的宽或者高,对于一个 ViewGroup 而言它显然是可以决定自己的宽高的,所以当它的子 View 提出 match_parent 的要求时,它就可以将自己的宽高值设置下去。
MeasureSpec.AT_MOST
此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是你不能超过我能提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。
了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。
自定义 View
我的目标是定义一个文本框,中间显示黑色文字,背景色为红色。
我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TestView">
<attr name="android:text" />
<attr name="android:textSize" />
</declare-styleable>
</resources>
TestView.java
public class TestView extends View {
private int mTextSize;
TextPaint mPaint;
private String mText;
public TestView(Context context) {
this(context,null);
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
mText = ta.getString(R.styleable.TestView_android_text);
mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
ta.recycle();
mPaint = new TextPaint();
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
canvas.drawColor(Color.RED);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText,cx,cy,mPaint);
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
xmlns:app="http://schemas.android/apk/res-auto"
xmlns:tools="http://schemas.android/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent&
版权声明:本文标题:长谈:关于 View Measure 测量机制,让我一次把话说完 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1726874652a1088211.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论