判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚
1. 坐标系
在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:
除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。
2. 自定义属性
Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:
- 自定义一个View
- 编写values/attrs.xml,在其中编写styleable和item等标签元素
- 在布局文件中View使用自定义的属性(注意namespace)
- 在View的构造方法中通过TypedArray获取
自定义View属性很重要,但是并不复杂,需要的话再查一下就好了
3. View绘制流程
View的绘制基本由measure()、layout()、draw()这个三个函数完成
| 函数 | 作用 | 相关方法 |
|---|---|---|
| measure() | 测量View的宽高 | measure(),setMeasuredDimension(),onMeasure() |
| layout() | 计算当前View以及子View的位置 | layout(),onLayout(),setFrame() |
| draw() | 视图的绘制工作 | draw(),onDraw() |
3.1 MeasureSpec
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec= mode+ size。
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY和
AT_MOST。
对于View来说,MeasureSpec的mode和Size有如下意义
| 模式 | 意义 | 对应 |
|---|---|---|
| EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
| AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
| UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 | 一般系统内部使用 |
3.2 Layout()
layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。
3.3 Draw()
draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在View的draw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。
- 如果需要,绘制背景。
- 有过有必要,保存当前canvas。
- 绘制View的内容。
- 绘制子View。
- 如果有必要,绘制边缘、阴影等效果。
- 绘制装饰,如滚动条等等。
View的流程图表示:
ViewGroup的流程图表示:
4. 布局过程的自定义:
方式: 重写布局过程的相关方法\
1. 测量过程: onMeasure()
2. 布局过程: onLayout()
具体:
1. 重写onMeasure()来修改已有的View的尺寸
2. 重写onMeasure()来全新计算自定义View的尺寸
3. 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局
4.1 重写onMeasure()来修改已有的View的尺寸
public class SquareImageView extends AppCompatImageView {
private static final String TAG = "SquareImageView";
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先执行原测量算法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取原先的测量结果
int measureWidth = getMeasuredWidth();
int measureHeight = getMeasuredHeight();
Log.d(TAG, "onMeasure11" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");
// 利用原先的测量结果计算出新尺寸
if (measureWidth > measureHeight) {
measureWidth = measureHeight;
} else {
measureHeight = measureWidth;
}
Log.d(TAG, "onMeasure22" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");
// 保存计算后的结果
setMeasuredDimension(measureWidth, measureHeight);
}
}
重写onMeasure() 修改尺寸
1. 重写 onMeasure() 修改尺寸,并调用super.onMeasure触发原先的测量
2. 用getMeasuredWidth() 和 getMeasuredHeight() 取到之前测得的尺寸,利用这两个尺寸来计算出最终尺寸。
3. 使用 setMeasuredDimension() 保存尺寸
4.2 重写onMeasure()来全新计算自定义View的尺寸
public class SquareImageView extends AppCompatImageView {
private static final String TAG = "SquareImageView";
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 不用写super.onMeasure() 需要自己计算尺寸
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 设置尺寸
int measureWidth = 100;
int measureHeight = 100;
// 矫正尺寸
measureWidth = resolveSize(measureWidth, widthMeasureSpec);
measureHeight = resolveSize(measureHeight, heightMeasureSpec);
// 保存计算后的结果
setMeasuredDimension(measureWidth, measureHeight);
}
// 注:下面这段代码已经过简化,不是resolveSize()的源码
public static int resolveSize(int size, int measureSpec) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
return size;
case MeasureSpec.AT_MOST:
if (size <= specSize) {
return size;
} else {
return specSize;
}
case MeasureSpec.EXACTLY:
return specSize;
default:
return size;
}
}
}
4.3 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局
4.3.1 可用空间 的判断方法\
根据自己的 MeasureSpec 中 mode 的不同:
- EXACTLY/AT_MOST:
可用空间:MeasureSpec 中的 size - UNSPECIFIED:
可用空间:无限大
4.3.2 onMeasure() 的重写
- 调用每个子 View 的 measure(), 让子View 自我测量
- 根据子 View 给出的尺寸,得出子View的位置,并保存他们的位置和尺寸
- 根据子 View 的位置和尺寸计算出自己的尺寸,并用 setMeasureDimension() 保存
4.3.3 关于保存子 View 位置的两点说明
-
不是所有的Layout都需要保存子View的位置(因为有的Layout可以在布局阶段实时推导出子View的位置,例如LinearLayout)
-
有时候对某些子 View 需要重复测量两次或多次才能得到正确的尺寸和位置
4.3.4 Layout 内部布局的自定义
- 重写onMeasure() 来计算内部布局
- 重写onLayout() 来摆放子 View
public class SomeLayout extends ViewGroup {
private static final String TAG = "SomeLayout";
public SomeLayout(Context context) {
super(context);
}
public SomeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidthSpec;
int usedWidth = 0;
LayoutParams lp = childView.getLayoutParams();
// lp.width: 对应 layout_width
// lp.height: 对应 layout_height
/**
* wrap_content => WRAP_CONTENT
* match_parent => MATCH_PARENT
* xxdp / xxsp => 具体的像素值
*
*/
int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
switch (lp.width) {
case MATCH_PARENT:
if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, EXACTLY);
} else {
childWidthSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
}
break;
case WRAP_CONTENT:
if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, AT_MOST);
} else {
childWidthSpec = MeasureSpec.makeMeasureSpec(0, UNSPECIFIED);
}
break;
default:
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
break;
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
}
}
}
4.3.5 onMeasure onSizeChanged onLayout 和 onDraw 执行顺序
打印结果如下:可以看出onMeasure执行了2次,onSizeChanged在onMeasure之后,onDraw最后执行
2021-09-02 17:21:19.842 3685-3685/com.example.javademo D/SquareImageView: onMeasure
2021-09-02 17:21:19.865 3685-3685/com.example.javademo D/SquareImageView: onMeasure
2021-09-02 17:21:19.873 3685-3685/com.example.javademo D/SquareImageView: onSizeChanged
2021-09-02 17:21:19.874 3685-3685/com.example.javademo D/SquareImageView: onLayout
2021-09-02 17:21:19.916 3685-3685/com.example.javademo D/SquareImageView: onDraw
5. get到的新知识点
像View中layout_width layout_height以layout_开头的,都是给父view看的,其他的属性才是给自己看的
像RleativeLayout LinearLayout这种以layout结尾的是ViewGroup 像ImageView和TextView以View结尾的是View