Android-自定义View-onDraw方法起步
考驾照了哟,我们就从onDraw方法开始吧。简单粗暴。或许我们不熟悉自定义View以前,还去看了Android艺术探索里面的详解,有可能看的是一脸懵逼,现在回过头来,我们一点点去实践,作为小白的我觉得这是我的方式,你或许有你的入门方式。
上一篇我们已经对View有了初步的认识,可以直接开始定义这个类了,然后把基本的结构先写了:
**1. **MyTextView01.java - 一般我会先把结构先基本写好,顺便把要做的一些todo一下
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/*
*@Description: 自定义绘制圆圈
*@Author: hl
*@Time: 2018/10/12 9:37
*/
public class MyTextView01 extends View {
private Context context;///< 上下文
public MyTextView01(Context context) {
this(context, null);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
///< 1\. 做一些绘制初始化
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
}
}
布局文件里面添加一下控件: activity_main.xml (添加了一个背景,方便进行绘制的观察) - 200*200就是上篇提到的视图占据的矩形区域,也就是我们画布,你可以在该区域任意涂抹
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<me.heyclock.hl.customcopy.MyTextView01
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/colorPrimary" />
</android.support.constraint.ConstraintLayout>
**2. **开始进行绘制之前了,我们了解下绘制的一些个知识(不一定每个人都那么熟)
Canvas | Android Developers --- 关于画布的知识
Paint | Android Developers --- 关于画笔的知识
目的:用画笔在画布上瞎几把涂抹
画布 - 第二个构造函数眼熟不?以前有对Bitmap进行诸多操作的同学会眼熟哈...:

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">可以基于Bitmap进行画布的创建,绘制内容将映射到Bitmap上</figcaption>
--画布绘制 - 比如画个圈圈吧:

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圈圈中心点,半径,以及需要的画笔,没画笔画毛呀</figcaption>
画笔 -- 基本能看到哈!至于第二个构造函数不太懂,需要研究下。这篇没专门分析这些,所以我们用第一个就行:

--画笔的简单设置 - 比如颜色

**3. **好了,我们开始进行一些初始化和简单绘制吧
MyTextView01.java
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/*
*@Description: 自定义绘制文本
*@Author: hl
*@Time: 2018/10/12 9:37
*/
public class MyTextView01 extends View {
/* 官方文档:
https://developer.android.google.cn/reference/android/graphics/Canvas
https://developer.android.google.cn/reference/android/graphics/Paint
*/
private Context context;///< 上下文
private Canvas canvas; ///< 画布
private Paint paint; ///< 画笔
public MyTextView01(Context context) {
this(context, null);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
///< 1\. 做一些绘制初始化
canvas = new Canvas(); ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
paint = new Paint();
paint.setColor(Color.parseColor("#F50808"));
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
canvas.drawCircle(100, 100, 50, paint);
}
}
看效果哟 - 圆圈出来了呀呀,但是没有居中,不好看? 布局中我们控件是200dp*200dp, 然而我们的中心点是100px, 100xp, 然后半径是50px, So, 需要做一个dp转px的转换:

**3.1 **进一步完善下效果
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
/*
*@Description: 自定义绘制文本
*@Author: hl
*@Time: 2018/10/12 9:37
*/
public class MyTextView01 extends View {
/* 官方文档:
https://developer.android.google.cn/reference/android/graphics/Canvas
https://developer.android.google.cn/reference/android/graphics/Paint
public void drawText (CharSequence text,
int start,
int end,
float x,
float y,
Paint paint)*/
private Context context;///< 上下文
private Canvas canvas; ///< 画布
private Paint paint; ///< 画笔
public MyTextView01(Context context) {
this(context, null);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
///< 1\. 做一些绘制初始化
canvas = new Canvas(); ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
paint = new Paint();
paint.setColor(Color.parseColor("#F50808"));
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
canvas.drawCircle(dp2px(context, 100), dp2px(context,100), dp2px(context,50), paint);
}
/**
* dp转px
* @param dp
* @return
*/
public static int dp2px(Context context, int dp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}

看样子还行的样子....
咿呀!这里我们就简单的定义了一个控件,这个控件特别丑,就中间一个小圈圈而已...
**4. **小白希望可以点击的时候,这个红色的圈能有呼吸灯的效果
{大概想法:点击的时候启动一个定时器,每500ms重新绘制一次,每次都增大半径10, 然后增量达到50时,就开始减10, 减量到50后开始重复循环}
4.1 第一步就是先解决点击事件的实现哈
根据前面的学习,我们可以重载
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
返回值解释:以前可能经常听说事件拦截啥的,这里如果返回true,表示消费掉该事件,且不希望其他回调方法再次处理。否则返回false。 我们不在往上级Viewgroup传递,所以返回true就行。
4. 2 重点我们看下这个MotionEvent | Android Developers, 可以先大概了解下有哪些方法可以供我们使用,用于处理点击事件逻辑。
比如下面的一些方法

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("test", "getX=" + event.getX());
Log.e("test", "getY=" + event.getY());
Log.e("test", "getRawX=" + event.getRawX());
Log.e("test", "getRawY=" + event.getRawY());
return true;
}

up 当点击红色中心点时, 从上面看这个rawY明显比y大,具体可以看解释:


一个getX是基于控件本身的x左边, 一个getRawX是基于屏幕的x左边, 都是左上角哈!
为了便于验证我们搞一个系统文本控件放在中间点击看看效果吧(目前我们自定义的控件还没有进行位置布局,暂时还不能通过xml设置把它挪动过去哈, I think):
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<me.heyclock.hl.customcopy.MyTextView01
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginLeft="100dp"
android:background="@color/colorPrimary" />
<TextView
android:id="@+id/testTouch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00ddd0"
android:text="你个扑街"
android:textColor="#000000"
android:textSize="50sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
代码设置Touch监听:
TextView textView = findViewById(R.id.testTouch);
textView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("textView", "getX=" + event.getX());
Log.e("textView", "getY=" + event.getY());
Log.e("textView", "getRawX=" + event.getRawX());
Log.e("textView", "getRawY=" + event.getRawY());
return true;
}
});

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">点击中间</figcaption>

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">点击左下角</figcaption>
是不是已经证明了get和getRaw的区别,哇咔咔...至于event.getAction()我们看下文档就行:


**4.4 **所以我们可以用get,把自定义控件的点击事件实现->限制在红心区域
MyTextView01.java
///< 做红色点击区域限制
private boolean bIsDownInRedRegion = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("test", "getX=" + event.getX());
Log.e("test", "getY=" + event.getY());
//Log.e("test", "getRawX=" + event.getRawX());
//Log.e("test", "getRawY=" + event.getRawY());
int x = (int) event.getX();
int y = (int) event.getY();
///< 控件大小是: dp2px(context, 200) * dp2px(context, 200)
///< 圆心坐标是: dp2px(context, 100) * dp2px(context, 100)
///< 圆半径是: dp2px(context, 50)
///< 所以点击区域就是左上角范围(dp2px(context, 50), dp2px(context, 50))
///< 右下角范围:(dp2px(context, 150), dp2px(context, 150))
int min = dp2px(context, 50);
int max = dp2px(context, 150);
Log.e("test", "x=" + x);
Log.e("test", "y="+ y);
Log.e("test", "min=" + min);
Log.e("test", "max="+ max);
Log.e("test", "event.getAction()=" + event.getAction());
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (x >= min && x <= max &&
y >= min && y <= max){
bIsDownInRedRegion = true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (bIsDownInRedRegion){
bIsDownInRedRegion = false;
if (x >= min && x <= max &&
y >= min && y <= max){
///< 抬手时我们就可以启动定时器进行绘制刷新了
Log.e("test", "红色区域点击了呀,sb");
}
}
break;
}
return true;
}
}
**4.5 **接下来进行绘制+刷新 - 新增updateDraw方法
/**
* 刷新绘制+增量变化
*/
private static final int STEP_RADIUS = 10; ///< 每次半径增加10
private int changeRadius = 0; ///< 变化量记录,达到50时则开始减;达到0就开始增加
private boolean addFlag = true;
private void updateDraw(){
changeRadius = addFlag ? (changeRadius += STEP_RADIUS) : (changeRadius -= STEP_RADIUS);
if (changeRadius > 50){
addFlag = false;
}else if (changeRadius < 0){
addFlag = true;
}
invalidate();
}
然后就可以进行绘制调用和刷新了
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
canvas.drawCircle(dp2px(context, 100), dp2px(context,100), dp2px(context,50 + changeRadius), paint);
}

然后可以看到每次点击我们就可以看到变化:

**4.5.1 **接下来我们在onTouchEvent里面增加一个定时器进行刷新
if (null == timer){
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
///< Handler也行
((Activity)context).runOnUiThread(new Runnable() {
@Override
public void run() {
updateDraw();
}
});
}
}, 0, 100);
}else{
timer.cancel();
timer = null;
}

Ok,到此基本绘制我们算是有所了解了呀呀呀!还是有收获,而且过程有时候也会犯一些错误,比如**private boolean bIsDownInRedRegion **= false;放到onTouchEvent内部去定义,导致怎么都不能做区域限制?(因为我们onDraw是可能会多次被调用,每次都会把变量的默认值冲掉)。
再比如关于绘制区域限制为什么要判断down,因为你点击外部,然后滑动到红色区域抬手,这个时候抬手范围可能符合要求。但是我们必须是点击的红色区域,并且抬手范围也是红色区域才能算是一次点击。
绘制我们先到这里,一些官方的文档,包括touch事件的地址也粘贴了。后面如果做复杂的自定义绘制控件,应该会经常用到。所以不要慌,慢慢来......
附上完整自定义View-基本绘制+点击事件+粗糙呼吸效果+整理下代码
package me.heyclock.hl.customcopy;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import java.util.Timer;
import java.util.TimerTask;
/*
*@Description: 自定义绘制文本
*@Author: hl
*@Time: 2018/10/12 9:37
*/
public class MyTextView01 extends View {
/* 官方文档:
https://developer.android.google.cn/reference/android/graphics/Canvas
https://developer.android.google.cn/reference/android/graphics/Paint
*/
private Context context;///< 上下文
private Canvas canvas; ///< 画布
private Paint paint; ///< 画笔
///< 做红色点击区域限制
private boolean bIsDownInRedRegion = false;
///< 定时刷新
private Timer timer = null;
/**
* 刷新绘制+增量变化
*/
private static final int STEP_RADIUS = 10; ///< 每次半径增加10
private int changeRadius = 0; ///< 变化量记录,达到50时则开始减;达到0就开始增加
private boolean addFlag = true; ///< 标记是否增加增量
public MyTextView01(Context context) {
this(context, null);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
///< 1\. 做一些绘制初始化
canvas = new Canvas(); ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
paint = new Paint();
paint.setColor(Color.parseColor("#F50808"));
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
canvas.drawCircle(dp2px(context, 100), dp2px(context,100), dp2px(context,50 + changeRadius), paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("test", "getX=" + event.getX());
Log.e("test", "getY=" + event.getY());
//Log.e("test", "getRawX=" + event.getRawX());
//Log.e("test", "getRawY=" + event.getRawY());
int x = (int) event.getX();
int y = (int) event.getY();
///< 控件大小是: dp2px(context, 200) * dp2px(context, 200)
///< 圆心坐标是: dp2px(context, 100) * dp2px(context, 100)
///< 圆半径是: dp2px(context, 50)
///< 所以点击区域就是左上角范围(dp2px(context, 50), dp2px(context, 50))
///< 右下角范围:(dp2px(context, 150), dp2px(context, 150))
int min = dp2px(context, 50);
int max = dp2px(context, 150);
Log.e("test", "x=" + x);
Log.e("test", "y="+ y);
Log.e("test", "min=" + min);
Log.e("test", "max="+ max);
Log.e("test", "event.getAction()=" + event.getAction());
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (x >= min && x <= max &&
y >= min && y <= max){
bIsDownInRedRegion = true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (bIsDownInRedRegion){
bIsDownInRedRegion = false;
if (x >= min && x <= max &&
y >= min && y <= max){
///< 抬手时我们就可以启动定时器进行绘制刷新了
Log.e("test", "红色区域点击了呀,sb");
if (null == timer){
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
///< Handler也行
((Activity)context).runOnUiThread(new Runnable() {
@Override
public void run() {
updateDraw();
}
});
}
}, 0, 100);
}else{
timer.cancel();
timer = null;
}
}
}
break;
}
return true;
}
/**
* 刷新绘制+增量变化
*/
private void updateDraw(){
changeRadius = addFlag ? (changeRadius += STEP_RADIUS) : (changeRadius -= STEP_RADIUS);
if (changeRadius > 50){
addFlag = false;
}else if (changeRadius < 0){
addFlag = true;
}
invalidate();
}
/**
* dp转px
* @param dp
* @return
*/
public static int dp2px(Context context, int dp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}