Android 坐标系与 View 绘制流程

发布于 2024-05-03 10:04:08 字数 15177 浏览 37 评论 0

涉及知识

绘制过程

类别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 的位置

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 的构造函数有四种重载方法,分别如下:

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。

测量类型对应数值描述
UNSPECIFIED0父容器不对 view 有任何限制,要多大给多大
EXACTLY1父容器已经检测出 view 所需要的大小,比如固定大小 xxdp
AT_MOST2父容器指定了一个大小, 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 坐标

图示如下:

View 坐标系

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

三、小结

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

PieChart 效果图如下:

PieChart

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

妳是的陽光

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

玍銹的英雄夢

文章 0 评论 0

我不会写诗

文章 0 评论 0

十六岁半

文章 0 评论 0

浸婚纱

文章 0 评论 0

qq_kJ6XkX

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文