Android 坐标系与 View 绘制流程
涉及知识
绘制过程
类别 | API | 描述 |
---|---|---|
布局 | onMeasure | 测量 View 与 Child View 的大小 |
onLayout | 确定 Child View 的位置 | |
onSizeChanged | 确定 View 的大小 | |
绘制 | onDraw | 实际绘制 View 的内容 |
事件处理 | onTouchEvent | 处理屏幕触摸事件 |
重绘 | invalidate | 调用 onDraw 方法,重绘 View 中变化的部分 |
Canvas 涉及方法
类别 | API | 描述 |
---|---|---|
绘制图形 | drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc | 依次为绘制点、直线、矩形、圆角矩形、椭圆、圆、扇形 |
绘制文本 | drawText, drawPosText, drawTextOnPath | 依次为绘制文字、指定每个字符位置绘制文字、根据路径绘制文字 |
画布变换 | translate, scale, rotate, skew | 依次为平移、缩放、旋转、倾斜(错切) |
画布裁剪 | clipPath, clipRect, clipRegion | 依次为按路径、按矩形、按区域对画布进行裁剪 |
Paint 涉及方法 |
类别 | API | 描述 |
---|---|---|
颜色 | setColor, setARGB, setAlpha | 依次为设置画笔颜色、透明度 |
类型 | setStyle | 填充(FILL),描边(STROKE),填充加描边(FILL_AND_STROKE) |
抗锯齿 | setAntiAlias | 画笔是否抗锯齿 |
字体大小 | setTextSize | 设置字体大小 |
字体测量 | getFontMetrics(),getFontMetricsInt() | 返回字体的测量,返回值依次为 float、int |
文字宽度 | measureText | 返回文字的宽度 |
文字对齐方式 | setTextAlign | 左对齐(LEFT),居中对齐(CENTER),右对齐(RIGHT) |
宽度 | setStrokeWidth | 设置画笔宽度 |
笔锋 | setStrokeCap | 默认(BUTT),半圆形(ROUND),方形(SQUARE) |
PS:因 API 较多,只列出了涉及的方法,想了解更多,请查看 官方文档
一、坐标系
1、屏幕坐标系
屏幕坐标系以手机屏幕的左上角为坐标原点,过的原点水平直线为 X 轴,向右为正方向;过原点的垂线为 Y 轴,向下为正方向。
2、View 坐标系
View 坐标系以父视图的左上角为坐标原点,过的原点水平直线为 X 轴,向右为正方向;过原点的垂线为 Y 轴,向下为正方向。
View 内部拥有四个函数,用于获取 View 的位置
getTop(); //View 的顶边到其 Parent View 的顶边的距离,即 View 的顶边与 View 坐标系的 X 轴之间的距离
getLeft(); //View 的左边到其 Parent View 的左边的距离,即 View 的左边与 View 坐标系的 Y 轴之间的距离
getBottom(); //View 的底边到其 Parent View 的顶边的距离,即 View 的底边与 View 坐标系的 X 轴之间的距离
getRight(); //View 的右边到其 Parent View 的左边的距离,即 View 的右边与 View 坐标系的 Y 轴之间的距离
图示如下:
二、绘制过程
1、构造函数
构造函数用于读取一些参数、属性对 View 进行初始化操作
View 的构造函数有四种重载方法,分别如下:
public BaseChart(Context context) {}
public BaseChart(Context context, AttributeSet attrs) {}
public BaseChart(Context context, AttributeSet attrs, int defStyleAttr) {}
public BaseChart(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}
context:上下文,新建时传入,如:
BaseChart baseChart = new BaseChart(this);
AttributeSet:是节点的属性集合,如:
<com.customview.BaseChart
android:layout_width="match_parent"
android:layout_height="match_parent"
app:attr1="attr1 from xml"
app:attr2="attr2 from xml"/>
即 com.customview.PieChart 节点中的属性集合
defStyleAttr:默认风格,是指它在当前 Application 或 Activity 所用的 Theme 中的默认 Style,如:
在 attrs.xml 中添加
<attr name="base_chart_style" format="reference" />
引用的是 styles.xml 文件中
<style name="base_chart_style">
<item name="attr2">@string/attr2</item>
<item name="attr3">@string/attr3</item>
</style>
在当前默认主题中添加这个 style
<style name="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="base_chart_style">@stylebase_chart_style</item>
...
</style>
defStyleRes:默认风格,只有当 defStyleAttr 无效时,才会使用这个值,如:
在 style.xml 中添加
<style name="base_chart_res">
<item name="attr4">attr4 from base_chart_res</item>
<item name="attr5">attr5 from base_chart_res</item>
</style>
一个实例——BaseChart
新建 BaseChart 类机成自 view
public class BaseChart extends View {
private String TAG = "BaseChart";
public BaseChart(Context context) {
this(context,null);
}
public BaseChart(Context context, AttributeSet attrs) {
this(context, attrs,R.attr.base_chart_style);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,R.style.base_chart_res);
}
public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.base_chart, defStyleAttr,defStyleRes);
int n = array.getIndexCount();
for (int i=0; i<n; i++){
int attr = array.getIndex(i);
switch (attr){
case R.styleable.base_chart_attr1:
Log.d(TAG,"attr1 =>" + array.getString(attr));
break;
case R.styleable.base_chart_attr2:
Log.d(TAG,"attr2 =>" + array.getString(attr));
break;
case R.styleable.base_chart_attr3:
Log.d(TAG,"attr3 =>" + array.getString(attr));
break;
case R.styleable.base_chart_attr4:
Log.d(TAG,"attr4 =>" + array.getString(attr));
break;
case R.styleable.base_chart_attr5:
Log.d(TAG,"attr5 =>" + array.getString(attr));
break;
}
}
}
obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
新增加的 attrs
属性说明如下:
attrs:默认属性,告诉系统需要获取那些属性的值,有多种 Value 类型,这里使用 string 类型,如:
在 attrs.xml
中添加
<declare-styleable name="base_chart">
<attr name="attr1" format="string" />
<attr name="attr2" format="string"/>
<attr name="attr3" format="string"/>
<attr name="attr4" format="string"/>
<attr name="attr5" format="string"/>
</declare-styleable>
使用上面提到的变量属性和布局文件
a、defStyleAttr 与 defStyleRes 参数先设置为 0
运行后显示如下:
BaseChart: attr1 =>attr1 from xml
BaseChart: attr2 =>attr2 from xml
BaseChart: attr3 =>null
BaseChart: attr4 =>null
BaseChart: attr5 =>null
attr1 与 attr2 输出均来自布局文件的设置
b、修改 BaseView.java 设置,引入 defStyleAttr:
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.base_chart, defStyleAttr,0);
相当于在布局文件中设置:
app:theme="@style/base_chart_style"
运行后显示如下:
BaseChart: attr1 =>attr1 from xml
BaseChart: attr2 =>attr2 from xml
BaseChart: attr3 =>attr3 from BaseChartStyle
BaseChart: attr4 =>null
BaseChart: attr5 =>null
attr1:仅在布局文件中设置,所以输出为 attr1 from xml
attr2:在布局文件与默认主题的 base_chart_style
都进行了设置,布局文件中的设置优先级更高,所以输出为 attr2 from xml
attr3:仅在默认主题 base_chart_style
中进行了设置,所以输出为 attr3 from BaseChartStyle
c、在布局文件中增加自定义的 style
<com.customview.BaseChart
android:layout_width="match_parent"
android:layout_height="match_parent"
app:attr1="attr1 from xml"
app:attr2="attr2 from xml"
style="@style/xml_style"/>
运行后结果如下:
BaseChart: attr1 =>attr1 from xml
BaseChart: attr2 =>attr2 from xml
BaseChart: attr3 =>attr3 from xml_style
BaseChart: attr4 =>attr4 from xml_style
BaseChart: attr5 =>null
- attr1:仅在布局文件中设置,所以输出为
attr1 from xml
- attr2:在布局文件与默认主题的 base_chart_style 都进行了设置,布局文件中的设置优先级更高,所以输出为
attr2 from xml
- attr3:在默认主题 base_chart_style 与自定义主题的 xml_style 都进行了设置,自定义主题优先级更高,所以输出为
attr3 from xml_style
- attr4:仅在自定义主题 xml_style 中进行了设置,所以输出为
attr4 from xml_style
d、修改 BaseView.java 设置,引入 defStyleRes,修改 defStyleAttr 为 0,否则引入的 R.style.base_chart_res 不会生效:
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.base_chart, 0 ,R.style.base_chart_res);
运行后输入结果如下:
BaseChart: attr1 =>attr1 from xml
BaseChart: attr2 =>attr2 from xml
BaseChart: attr3 =>attr3 from xml_style
BaseChart: attr4 =>attr4 from xml_style
BaseChart: attr5 =>attr5 =>attr5 from base_chart_res
- attr1:仅在布局文件中设置,所以输出为
attr1 from xml
- attr2:仅在布局文件中进行了设置,所以输出为
attr2 from xml
- attr3:仅在自定义主题 xml_style 中进行了设置,所以输出为
attr3 from xml_style
- attr4:在自定义主题 xml_style 和 defStyleRes 中都进行了设置,自定义主题优先级更高,所以输出为
attr4 from xml_style
- attr5:仅在 defStyleRes 中进行了设置,所以输出为
attr5 from base_chart_res
2、onMeasure
View 会在此函数中完成自己的 Measure 以及递归的遍历完成 Child View 的 Measure,某些情况下需要多次 Measure 才能确定 View 的大小。
可以从 onMeasure 中取出宽高及其他属性:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//Width
int widthMode = MeasureSpec.getMode(widthMeasureSpec);//宽度值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);//宽度测量模式
//Height
int heightMode = MeasureSpec.getMode(heightMeasureSpec);//高度值
int heightSize = MeasureSpec.getSize(heightMeasureSpec);//高度测量模式
}
由此可见 widthMeasureSpec, heightMeasureSpec 并不仅仅是宽高的值,还对应了宽高的测量模式。
MeasureSpec 是 View 内部的一个静态类,下面给出它的部分源码:
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(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
可以看出 MeasureSpec 代表一个 32 的 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表测量值 SpecSize。拥有 3 种测量模式,分别为 UNSPECIFIED、EXACTLY、AT_MOST。
测量类型 | 对应数值 | 描述 |
---|---|---|
UNSPECIFIED | 0 | 父容器不对 view 有任何限制,要多大给多大 |
EXACTLY | 1 | 父容器已经检测出 view 所需要的大小,比如固定大小 xxdp |
AT_MOST | 2 | 父容器指定了一个大小, view 的大小不能大于这个值 |
3、onLayout
用于确定 View 以及其子 View 的布局位置,在 ViewGroup 中,当位置被确定后,它在 onLayout 中会遍历所有的 child 并调用其 layout,然后 layout 内部会再调用 child 的 onLayout 确定 child View 的布局位置。
layout 方法如下:
public void layout(int l, int t, int r, int b) {
...
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
...
}
mLeft, mTop, mBottom, mRight 四个参数分别通过 getLeft(),getTop(),getRight(),getBottom() 四个函数获得。这一组 old 值会在位置改变时,调用 onLayoutChange 时使用到。
4、onSizeChanged
如其名,在 View 大小改变时调用此函数,用于确定 View 的大小。至于 View 大小为什么会改变,因为 View 的大小不仅由本身确定,同时还受父 View 的影响。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
这里的 w、h 就是确定后的宽高值,如果查看 View 中的 onLayoutChange 也会看到类似的情况,拥有 l, t, r, b, oldL, oldT, oldR, oldB,新旧两组参数。
5、onDraw
onDraw 是 View 的绘制部分,给了我们一张空白的画布,使用 Canvas 进行绘制。也是后面几篇文章所要分享的内容。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
6、其他方法以及监听回调
如 onTouchEvent、invalidate、setOnTouchListener 等方法。
onTouchEvent 用于处理传递到的 View 手势事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
当返回 true 时,说明该 View 消耗了触摸事件,后续的触摸事件也由它来进行处理。返回 false 时,说明该 View 对触摸事件不感兴趣,事件继续传递下去。
触屏事件类型被封装在 MotionEvent 中,MotionEvent 提供了很多类型的事件,主要关心如下几种类型:
事件类型 | 描述 |
---|---|
ACTION_DOWN | 手指按下 |
ACTION_MOVE | 手指移动 |
ACTION_UP | 手指抬起 |
事件效果如下:
在 MotionEvent 中有两组可以获得触摸位置的函数
event.getX(); //触摸点相对于 View 坐标系的 X 坐标
event.getY(); //触摸点相对于 View 坐标系的 Y 坐标
event.getRawX(); //触摸点相对于屏幕坐标系的 X 坐标
event.getRawY(); //触摸点相对于屏幕坐标系的 Y 坐标
图示如下:
onWindowFocusChanged 运行于 onMeasure 与 onLayout 之后,可以获取到正确的 width、height、top、left 等属性值。
三、小结
简单分析了自定义 View 的入门准备知识,包括屏幕坐标系、View 坐标、View 的绘制过程中的主要函数、以及屏幕触摸事件。后面的内容将会围绕 onDraw 函数展开,在完成涉及知识点的分析之后,将会实战去编写 PieView 的代码。
PieChart 效果图如下:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论