Android坐标系与View绘制流程

自定义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内部拥有四个函数,用于获取View的位置

1
2
3
4
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轴之间的距离

图示如下:

View坐标系

二、绘制过程

1、构造函数

  构造函数用于读取一些参数、属性对View进行初始化操作

  View的构造函数有四种重载方法,分别如下:

1
2
3
4
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:上下文,新建时传入,如:

1
BaseChart baseChart = new BaseChart(this);

AttributeSet:是节点的属性集合,如:

1
2
3
4
5
<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中添加

1
<attr name="base_chart_style" format="reference" />

引用的是styles.xml文件中

1
2
3
4
<style name="base_chart_style">
<item name="attr2">@string/attr2</item>
<item name="attr3">@string/attr3</item>
</style>

在当前默认主题中添加这个style

1
2
3
4
5
<style name="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar">
...
<item name="base_chart_style">@stylebase_chart_style</item>
...
</style>



defStyleRes:默认风格,只有当defStyleAttr无效时,才会使用这个值,如:

在style.xml中添加

1
2
3
4
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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中添加

1
2
3
4
5
6
7
<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

运行后显示如下:

1
2
3
4
5
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:

1
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.base_chart, defStyleAttr,0);

相当于在布局文件中设置:

1
app:theme="@style/base_chart_style"

运行后显示如下:

1
2
3
4
5
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

1
2
3
4
5
6
<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"/>

运行后结果如下:

1
2
3
4
5
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不会生效:

1
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.base_chart, 0 ,R.style.base_chart_res);

运行后输入结果如下:

1
2
3
4
5
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中取出宽高及其他属性:

1
2
3
4
5
6
7
8
9
10
@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内部的一个静态类,下面给出它的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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方法如下:

1
2
3
4
5
6
7
8
9
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的影响。

1
2
3
4
@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进行绘制。也是后面几篇文章所要分享的内容。

1
2
3
4
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}

======

6、其他方法以及监听回调

如onTouchEvent、invalidate、setOnTouchListener等方法。

onTouchEvent用于处理传递到的View手势事件。

1
2
3
4
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}

当返回true时,说明该View消耗了触摸事件,后续的触摸事件也由它来进行处理。返回false时,说明该View对触摸事件不感兴趣,事件继续传递下去。

触屏事件类型被封装在MotionEvent中,MotionEvent提供了很多类型的事件,主要关心如下几种类型:

事件类型 描述
ACTION_DOWN 手指按下
ACTION_MOVE 手指移动
ACTION_UP 手指抬起



事件效果如下:

屏幕触摸事件

在MotionEvent中有两组可以获得触摸位置的函数

1
2
3
4
event.getX(); //触摸点相对于View坐标系的X坐标
event.getY(); //触摸点相对于View坐标系的Y坐标
event.getRawX(); //触摸点相对于屏幕坐标系的X坐标
event.getRawY(); //触摸点相对于屏幕坐标系的Y坐标

图示如下:

View坐标系

onWindowFocusChanged运行于onMeasure与onLayout之后,可以获取到正确的width、height、top、left等属性值。

三、小结

  简单分析了自定义View的入门准备知识,包括屏幕坐标系、View坐标、View的绘制过程中的主要函数、以及屏幕触摸事件。后面的内容将会围绕onDraw函数展开,在完成涉及知识点的分析之后,将会实战去编写PieView的代码。如果在阅读过程中,有任何疑问与问题,欢迎与我联系。

博客:www.idtkm.com

GitHub:https://github.com/Idtk

微博:http://weibo.com/Idtk

邮箱:IdtkMa@gmail.com

PieChart效果图如下:

PieChart

文章作者:Idtk

本文标题:Android坐标系与View绘制流程

原始链接:http://www.idtkm.com/2016/05/08/1、CoordinateAndProcess/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。