最近有点时间,准备补补自定义 View,我就直接去鸿洋大神的博客了。按照博客上的文章说明,自己实现四位验证码的效果。这个过程中遇到了不少问题,也从中学到了不少,把一些知识盲点给清除了。
鸿洋博客:Android 自定义 View (一)
可以参考鸿洋大神的博客一步步的来学习和进阶。
最基本的知识,想必大家都知道,自定义 View 有 3 种。
第一种,继承控件,通过继承已有控件进行扩展,实现一些自带控件没有的功能。
第二种,组合控件,通过多种自带控件组合在一起,形成新的控件。
第三种,绘制控件,继承自 View 完全靠 canvas 绘制出想要的图形和文字。
继承控件和组合控件,想必大家都会了,绘制控件很多人都讳莫如深,有着深深的恐惧。最初,我也是有这样的恐惧,也曾花些时间学习了一下,但是学的一知半解,由于时间关系,没有继续深入,很多知识点也没有梳理清楚。现在准备花点时间,一一揭开他们的面纱。
先说一下实现自定义 View 的步骤:
1、自定义 View 的属性
2、在构造方法中获取自定义的属性
3、重写 onMeasure()
4、重写 onDraw()
1、最基本的操作
自定义 View 最基本的操作,就是定义一个类,继承自 View,实现三个构造方法。如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class MyVerifyCode extends View { public MyVerifyCode(Context context) { this(context, null); } public MyVerifyCode(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { } } |
三个构造方法有什么区别呢?为什么要写三个构造方法?
当我们在 Java 代码中使用某些控件时,会直接 new 出来一个对象,这时候我们一般会传入 Context,此时调用的就是一个参数的构造方法。因为此时我们只是简单的想要一个对象,至于对象的属性和方法还没有相应的操作,在后面的代码中才会通过调用方法给对象的属性赋值。当我们在 xml 布局文件中使用控件时,会通过一些属性来赋值,此时会调用第二个构造方法,第二个参数 attrs 就是接受 xml 文件中传入的属性值。当我们在 xml 布局文件中使用控件的同时,附加了 style 样式属性,那么就会调用第三个构造方法,也就是第三个参数 defStyleAttr 来接受添加的样式。所以,自定义 View 一般都是会实现这三种构造方法。
当然了,你说你自定义的 View 只需要在 Java 代码中使用,那么你只实现第一个构造方法也是可以的。如果不使用样式,只实现前两个构造方法也可以。
1 2 3 4 | <com.sendtion.customview.view.MyVerifyCode android:id="@+id/my_verify_code" android:layout_width="200dp" android:layout_height="100dp"/> |
另外有一点需要注意,为了在初始化的时候统一调用,一般会在前两个构造方法中使用 this 调用,这样的话我们只需要在第三个构造方法中做初始化操作。
2、onMeasure 测量
上面实现了自定义 View 最基本的操作,你说我就实现这么多,不做别的什么操作了行不行?当然可以啊,但是这样的代码有什么卵用呢?
有两个重要的方法,一个是 onMeasure(widthMeasureSpec, heightMeasureSpec),一个是 onDraw(Canvas canvas),这两个方法是我们主要操作的地方。
onMeasure 方法不是必须的,它是用来测量 View 的宽高的,默认有系统自动测量。
当你自定义的 View 在布局中设置宽高为 match_parent 或者固定宽高时,得到的效果是正确的。此时,是不需要重写 onMeasure 方法的。
1 2 3 4 | <com.sendtion.customview.view.MyVerifyCode android:id="@+id/my_verify_code" android:layout_width="match_parent" android:layout_height="match_parent"/> |
然而,当你的 View 宽高设置为 wrap_content 时,得到的效果仍然是 march_parent。此时,需要重写 onMeasure 方法,自己实现测量。
1 2 3 4 | <com.sendtion.customview.view.MyVerifyCode android:id="@+id/my_verify_code" android:layout_width="wrap_content" android:layout_height="wrap_content"/> |
自己实现测量时,要根据 widthMeasureSpec 和 heightMeasureSpec 两个参数来确定 View 的宽高。
widthMeasureSpec 和 heightMeasureSpec 是一个 32 位的 int 值,高 2 位代表测量模式,低 30 位代表测量的大小,用于辅助 View 的测量。
重写之前先了解 MeasureSpec 的 specMode 测量模式,一共三种类型:
EXACTLY:一般是设置了明确的值或者是 MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为 WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面重写 onMeasure 方法,经过上面的分析,我们只需要处理 AT_MOST 模式就可以了,当然 EXACTLY 也要给一个值才行,所以我们的实现代码如下:
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 | @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY){ width = widthSize; } else { paint.setTextSize(vcTextSize); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); float textWidth = rect.width(); Log.e("---", "onMeasure: textWidth= " + textWidth); //width = (int) textWidth; //这样也可以,但是没计算padding width = (int) (getPaddingLeft() + textWidth + getPaddingRight()); } if (heightMode == MeasureSpec.EXACTLY){ height = heightSize; } else { paint.setTextSize(vcTextSize); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); float textHeight = rect.height(); Log.e("---", "onMeasure: textHeight= " + textHeight); //height = (int) textHeight; //这样也可以,但是没计算padding height = (int) (getPaddingTop() + textHeight + getPaddingBottom()); } setMeasuredDimension(width, height); } |
从以上代码可以看出,在 EXACTLY 模式下面,直接使用系统测量的数据,否则就自己计算。计算方式也很简单,就是字体内容的宽高加上 padding 数值。
View 的宽度为字体的宽度 rect.width()+paddingLeft+paddingRight,View 的高度为字体的高度 rect.height()+paddingTop+paddingBottom。
1 2 3 4 5 | <com.sendtion.customview.view.MyVerifyCode android:id="@+id/my_verify_code" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp"/> |
3、onDraw 绘制
对于 Android 的 2D 绘图,我们主要借助于 graphics 包,其中主要包含了 Canvas 类、Paint 类、Color 类和 Bitmap 类等。
要绘制图形就得有画笔,根据自己的需要对画笔设置相关的属性,然后通过 canvas 来绘制。
画笔的常见属性如下:
setAntiAlias: 设置画笔的锯齿效果。
setColor: 设置画笔颜色 。
setARGB: 设置画笔的 a,r,p,g 值。
setAlpha: 设置 Alpha 值 。
setTextSize: 设置字体尺寸。
setStyle: 设置画笔风格,空心或者实心。
setStrokeWidth: 设置空心的边框宽度。
getColor: 得到画笔的颜色 。
getAlpha: 得到画笔的 Alpha 值
详细的属性解析,请参考此文:Paint 的效果研究
Canvas 我们可以称之为画布,能够在上面绘制各种东西,是安卓平台 2D 图形绘制的基础,在使用该类前需要设置好 paint。
Canvas 常用操作速查表:
除了进行基本的绘制外,Canvas 也可以进行一些基本操作。位移、缩放、旋转、倾斜以及快照和回滚。
具体操作参考:Canvas 操作
下面是具体的实现,以下操作全部在 onDraw 方法中。
1 2 3 4 5 6 7 | @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画背景 paint.setColor(vcBackground); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); } |
首先,画一个矩形的纯色背景。通过画笔的 setColor()方法设置矩形的颜色,然后通过画布的 drawRect()方法画出矩形。
传入的参数为:drawRect(left, top, right, bottom, paint); 左上点到右下点,也就是设置 View 的宽高和画笔。
1 2 | paint.setColor(vcBackground); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); |
然后,在矩形中绘制文字。通过画笔的 setColor()方法设置文字的颜色,画笔的 getTextBounds()方法获取文字的宽高。通过计算公式获得文字开始绘制的起始位置,通过画布的 drawText()方法绘制出文字。
文字的宽高从 getTextBounds()方法的 Rect 参数中获取,但是这种获取方式不太精确,获取到的文字宽度会小一点,更精确的方式是 paint 的 measureText()方法。如下所示:
1 2 | float textWidth = mBound.width();//这样宽度会不全,比系统的textView短 float textWidth = mPaint.measureText(mTitleText);//比较精确的测量文本宽度的方式 |
传入的参数为:drawText(text, x, y, paint); 传入要绘制的文字,开始绘制的起始坐标,画笔。
1 2 3 4 5 6 | //画文字 paint.setColor(vcTextColor); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); int startX = getWidth()/2 - rect.width()/2; int startY = getHeight()/2 - rect.height()/2; canvas.drawText(verifyCode, startX, startY-offset, paint); |
这样就实现了在一个矩形的红色背景上,绘制出白色的四位验证码。
然而,我们发现绘制的结果并不能令我们满意,文字并不是居中显示的,而是有点偏上的位置。这是因为文字的绘制比较特殊,它有一个基线的存在,文字的绘制以基线为基准。后面再说明文字绘制的居中显示。先说解决方案。
1 2 3 4 | Paint.FontMetricsInt fm = paint.getFontMetricsInt(); startX = (int) (getWidth() / 2 - paint.measureText(verifyCode) / 2); startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent; canvas.drawText(verifyCode, startX, startY, paint); |
4、自定义属性
想要自定义属性,一般我们会在 res/values 目录下创建 attrs.xml 文件,通过 declare-styleable 来指定自定义属性的归属,name 属性一般使用自定义 View 类名称,通过 attr 来定义需要的属性,name 指定属性名,format 指定属性值的类型。
1 2 3 4 5 6 7 8 9 | <com.sendtion.customview.view.MyVerifyCode android:id="@+id/my_verify_code" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" app:vc_background="@color/colorAccent" app:vc_auto_refresh="false" app:vc_text_size="16sp" app:vc_text_color="@color/color_f"/> |
format 的取值类如下所示:
boolean 表示 attr 取值为 true 或者 false
color 表示 attr 取值是颜色类型,例如#ff3344,或者是一个指向 color 的资源 id,例如 R.color.colorAccent.
dimension 表示 attr 取值是尺寸类型,例如例如取值 16sp、16dp,也可以是一个指向 dimen 的资源 id,例如 R.dimen.dp_16
float 表示 attr 取值是整形或者浮点型
fraction 表示 attr 取值是百分数类型,只能以%结尾,例如 30%
integer 表示 attr 取值是整型
string 表示 attr 取值是 String 类型,或者一个指向 String 的资源 id,例如 R.string.testString
reference 表示 attr 取值只能是一个指向资源的 id。
enum 表示 attr 取值只能是枚举类型。
flag 表示 attr 取值是 flag 类型。
需要注意的是:refrence , 表示 attr 取值只能是一个指向资源的 id。比如:app:vc_text_color=”@color/colorAccent”
自定义属性的值需要在构造方法中取出,实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyVerifyCode); vcAutoRefresh = typedArray.getBoolean(R.styleable.MyVerifyCode_vc_auto_refresh, false); vcTextSize = typedArray.getDimensionPixelSize(R.styleable.MyVerifyCode_vc_text_size, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); vcTextColor = typedArray.getColor(R.styleable.MyVerifyCode_vc_text_color, getResources().getColor(R.color.color_f)); vcBackground =typedArray.getColor(R.styleable.MyVerifyCode_vc_background, getResources().getColor(R.color.colorAccent)); typedArray.recycle(); init(); } |
需要注意的是,TypedArray 使用完后需要回收。
另外,自定义属性的 dp 和 sp 值需要转换为 px 才行,否则以实际输入值为 px 值,比如默认字体大小为 16sp,就需要转换为 px 单位,具体的转换方式如代码所示:
1 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())) |
取大小单位时,有三个方法可以使用,这 3 个方法都是把 dimens.xml 文件中的 dp 或 sp 数值乘以屏幕 scale 来换算成 px 单位,那相乘之后可能会有小数。
三个方法的区别是:
getDimension() 返回 float 型 px 值 精确
getDimensionPixelOffset() 返回 int 型 px 值 直接把小数删除
getDimensionPixelSize() 返回 int 型 px 值 进行四舍五入
各方法使用场景:
如果你的代码中可以用 float 作为长度单位的话,就用 getDimension()方法,因为最精确;
如果只能接收 int 为长度单位的的话,那就看你自己的需求来选要用 getDimensionPixelOffset()或 getDimensionPixelSize();
如果你在写代码的时候不记得这 3 个方法的区别了,无所谓了,随便用哪一个都可以,不就是相差小数点那一点点的大小而已嘛,差别很小可以忽略不计了;
5、文字居中绘制
为什么把文字居中绘制单独拿出来呢?因为文字绘制这一块有一些特殊的知识点,如果不了解,绘制的文字就不是自己想要的效果。
我们先来看一下绘制文字的方法和参数:
方法的参数很简单: text 是文字内容,x 和 y 是文字的坐标。这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。y 的坐标并不是文字的底部,也不是文字的中间,而是底部靠上一点的位置。这是因为 y 坐标是文字基线的位置。
下面我会画出几条线,分别是矩形的中间位置十字交叉线,文字的范围线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //画背景 paint.setColor(vcBackground); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); //画文字 paint.setColor(vcTextColor); paint.setStrokeWidth(2); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); int startX = getWidth()/2 - rect.width()/2; int startY = getHeight()/2 - rect.height()/2; canvas.drawText(verifyCode, startX, startY, paint); //画十字交叉线 paint.setColor(Color.GREEN); paint.setStrokeWidth(2); canvas.drawLine(0, getHeight()/2, getWidth(), getHeight()/2, paint); canvas.drawLine(getWidth()/2, 0, getWidth()/2, getHeight(), paint); //画范围线 canvas.drawLine(0, getHeight()/2-rect.height()/2, getWidth(), getHeight()/2-rect.height()/2, paint); canvas.drawLine(0, getHeight()/2+rect.height()/2, getWidth(), getHeight()/2+rect.height()/2, paint); canvas.drawLine(getWidth()/2-rect.width()/2, 0, getWidth()/2-rect.width()/2, getHeight(), paint); canvas.drawLine(getWidth()/2+rect.width()/2, 0, getWidth()/2+rect.width()/2, getHeight(), paint); //画起始点 paint.setColor(Color.BLUE); paint.setStrokeWidth(10); canvas.drawPoint(getWidth()/2-rect.width()/2, getHeight()/2-rect.height()/2, paint); |
据图所示,我们看到文字的位置不是我们想要的,文字在偏上的位置,绘制的起始点也不是我们以为的位置。起始点所在的横向直线就是我们要说的基线。从其他博客借一张图。
Baseline 是基线,在 android 中,文字的绘制都是从 Baseline 处开始的,Baseline 往上至字符“最高处”的距离我们称之为 ascent(上坡度),Baseline 往下至字符“最低处”的距离我们称之为 descent(下坡度);
leading(行间距)则表示上一行字符的 descent 到该行字符的 ascent 之间的距离;
top 和 bottom 文档描述地很模糊,其实这里我们可以借鉴一下 TextView 对文本的绘制,TextView 在绘制文本的时候总会在文本的最外层留出一些内边距,因为 TextView 在绘制文本的时候考虑到了类似读音符号,下图中的 A 上面的符号就是一个拉丁文的类似读音符号的东西:
Baseline 是基线,Baseline 以上是负值,以下是正值,因此 ascent,top 是负值, descent 和 bottom 是正值。
所以,原本的文字高度获取方式:
1 | float textHeight = rect.height(); |
可以修改为比较精确的测量文本高度的方式:
1 2 | Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int textHeight = fontMetrics.bottom - fontMetrics.top; |
因为 top 是负值,bottom 是正值,所以用 bottom-top 获取文字的高度。
那么,我们获取绘制的起始点 y 坐标就可以修改一下了:
1 | int startY = getHeight() / 2 + (fm.bottom - fm.top) / 2 - fm.bottom; |
当然了,也可以使用 ascent 和 descent,有一点小小的误差,因为 ascent 和 descent 包含文字音标之类的,所以他们的值比 top 和 bottom 要大一些,计算的结果也更准确一点。
1 | int startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent; |
最终实现效果如下:
完整的代码如下:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | /** * 自定义验证码 */ public class MyVerifyCode extends View { private String verifyCode; private Paint paint; private Rect rect; private boolean vcAutoRefresh; private int vcTextSize = 56;//单位 private int vcTextColor = getResources().getColor(R.color.color_f); private int vcBackground = getResources().getColor(R.color.colorAccent); public MyVerifyCode(Context context) { this(context, null); } public MyVerifyCode(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyVerifyCode(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyVerifyCode); vcAutoRefresh = typedArray.getBoolean(R.styleable.MyVerifyCode_vc_auto_refresh, false); vcTextSize = typedArray.getDimensionPixelSize(R.styleable.MyVerifyCode_vc_text_size, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); vcTextColor = typedArray.getColor(R.styleable.MyVerifyCode_vc_text_color, getResources().getColor(R.color.color_f)); vcBackground =typedArray.getColor(R.styleable.MyVerifyCode_vc_background, getResources().getColor(R.color.colorAccent)); typedArray.recycle(); init(); } private void init() { //会出现三位数的情况 verifyCode = String.valueOf((int) (Math.random() * 10000));//四位验证码 verifyCode = "abcdefg"; paint = new Paint(); //paint.setTextAlign(Paint.Align.CENTER); paint.setTextSize(vcTextSize); //Paint.ANTI_ALIAS_FLAG :抗锯齿标志 //Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志 //Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志 //Paint.UNDERLINE_TEXT_FLAG : 下划线 //Paint.STRIKE_THRU_TEXT_FLAG : 中划线 //Paint.FAKE_BOLD_TEXT_FLAG : 加粗 //Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志 //Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志 //Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志 paint.setFlags(Paint.ANTI_ALIAS_FLAG); rect = new Rect(); if (vcAutoRefresh) { } setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { verifyCode = String.valueOf((int) (Math.random() * 10000));//四位验证码 postInvalidate(); } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY){ width = widthSize; } else { paint.setTextSize(vcTextSize); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); float textWidth = rect.width(); Log.e("---", "onMeasure: textWidth= " + textWidth); //width = (int) textWidth; //这样也可以,但是没计算padding width = (int) (getPaddingLeft() + textWidth + getPaddingRight()); } if (heightMode == MeasureSpec.EXACTLY){ height = heightSize; } else { paint.setTextSize(vcTextSize); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); float textHeight = rect.height(); Log.e("---", "onMeasure: textHeight= " + textHeight); //height = (int) textHeight; //这样也可以,但是没计算padding height = (int) (getPaddingTop() + textHeight + getPaddingBottom()); } setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画背景 paint.setColor(vcBackground); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); //画文字 Log.e("---", "onDraw: getWidth=" + getWidth() + ", getHeight=" + getHeight()); paint.setColor(vcTextColor); paint.setStrokeWidth(2); paint.getTextBounds(verifyCode, 0, verifyCode.length(), rect); //第一种方式,计算的基准点偏上 int startX = getWidth()/2 - rect.width()/2; int startY = getHeight()/2 - rect.height()/2; //canvas.drawText(verifyCode, startX, startY, paint); //第二种方式,貌似更准确,计算的基准点更准,内容变化也会居中显示 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); startX = (int) (getWidth() / 2 - paint.measureText(verifyCode) / 2); startY = getHeight() / 2 + (fm.bottom - fm.top) / 2 - fm.bottom; //startY = getHeight() / 2 + (fm.descent - fm.ascent) / 2 - fm.descent; canvas.drawText(verifyCode, startX, startY, paint); //画十字交叉线 paint.setColor(Color.GREEN); paint.setStrokeWidth(2); canvas.drawLine(0, getHeight()/2, getWidth(), getHeight()/2, paint); canvas.drawLine(getWidth()/2, 0, getWidth()/2, getHeight(), paint); //画基准线 canvas.drawLine(0, getHeight()/2-rect.height()/2, getWidth(), getHeight()/2-rect.height()/2, paint); canvas.drawLine(0, getHeight()/2+rect.height()/2, getWidth(), getHeight()/2+rect.height()/2, paint); canvas.drawLine(getWidth()/2-rect.width()/2, 0, getWidth()/2-rect.width()/2, getHeight(), paint); canvas.drawLine(getWidth()/2+rect.width()/2, 0, getWidth()/2+rect.width()/2, getHeight(), paint); //画基准点 paint.setColor(Color.BLUE); paint.setStrokeWidth(10); canvas.drawPoint(startX, startY, paint); } } |
全部源码:github 地址
参考文章:
https://www.jianshu.com/p/8c10a8a8e669
https://blog.csdn.net/SilenceOO/article/details/73498331
https://www.jianshu.com/p/1728b725b4a6
本文完结。