转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/74729925
本文出自:【顾林海的博客】
个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
前言
自定义View一直是初中级程序员的痛,在之前也写过很多关于自定义控件的文章,很多人也看了一些关于自定义控件的文章或是相关源码,效果不是很好,那么怎样才能学会自定义View呢,我认为基础很重要,先对自定义View相关的方法和知识点有了一定的了解,才能对产品提出的自定义效果了然于胸,本篇文章是关于Paint、Canvas和PorterDuffXfermode的相关用法,内容比较简单,后续也会撰写自定义View相关知识点的文章。
一个简单的自定义View
一开始我们定义一个继承View的类,比如下面这种,当然,如果这样定义的话IDE会报错,告诉我们要添加相应的构造器。
public class MyView extends View {
}
添加四个构造器:
public class MyView extends View {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
这四个构造器的说明如下:
- 第一个构造器 :当不需要使用xml声明或者不需要使用inflate动态加载时候,实现此构造函数。 - 第二个构造器 :当需要在xml中声明此控件,则需要实现此构造函数。并且在构造函数中把自定义的属性与控件的数据成员连接起来。 - 第三个构造器: 与第二个构造器一样,并接受一个style资源 - 第四个构造器:与第三个构造器一样,当defStyleAttr属性没有赋值时,使用defStyleRes进行主题定义。
定义完MyView后,在xml中引用它:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.glh.project20170707.view.MyView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
并在Activity中设置布局文件:
package com.glh.project20170707;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
发现屏幕上什么都没有,这是因为我们还有没在MyView进行相关操作,不管怎么说, 至少我们已经向前迈进了一小步。在日常生活中,假如我们要画一幅图,需要准备笔和纸,在Android中自定义View也是如此,需要Paint(笔)和Canvas(纸),Paint可以通过new实例化一个,Canvas可以通过重写onDraw方法获取。
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.glh.project20170707.util.WindowUtil;
public class MyView extends View {
//画图需要的笔
private Paint mPaint;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initPaint();
}
/**
* 初始化笔
*/
private void initPaint() {
//创建一支笔,并打开抗锯齿
this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//我要一支红色的笔
this.mPaint.setColor(Color.RED);
//设置画笔的粗细度
this.mPaint.setStrokeWidth(5);
//设置画笔样式
this.mPaint.setStyle(Paint.Style.FILL);
/*
* 画笔样式分三种:
* 1.Paint.Style.STROKE:描边
* 2.Paint.Style.FILL_AND_STROKE:描边并填充
* 3.Paint.Style.FILL:填充
*/
this.mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
相关工具类:
package com.glh.project20170707.util;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
public class WindowUtil {
public static int getWindowWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metric = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metric);
return metric.widthPixels;
}
public static int getWindowHeight(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metric = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metric);
return metric.heightPixels;
}
}
上面程序中创建了一支粗细度为5(像素:px)的抗锯齿笔,样式描边,并设置成红色,这样我们在画图时就有了颜色,那么笔(Paint)和纸(Canvas)都有了,是不是该画点什么,看看Canvas都提供了哪些方法用于绘图。
方法很多,比如drawCircle方法绘制一个圆、drawArc方法绘制圆弧、drawBitmap方法绘制Bitmap、drawPoint方法绘制点等等。方法这么多,我们不需要全部去记住,当用到的什么再去了解,现在绘制一个圆试试。
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.glh.project20170707.util.WindowUtil;
public class MyView extends View {
//画图需要的笔
private Paint mPaint;
private Context mContext;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
this.initPaint();
}
/**
* 初始化笔
*/
private void initPaint() {
//创建一支笔,并打开抗锯齿
this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//我要一支红色的笔
this.mPaint.setColor(Color.RED);
//设置画笔的粗细度
this.mPaint.setStrokeWidth(5);
//设置画笔样式
this.mPaint.setStyle(Paint.Style.FILL);
/*
* 画笔样式分三种:
* 1.Paint.Style.STROKE:描边
* 2.Paint.Style.FILL_AND_STROKE:描边并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(WindowUtil.getWindowWidth(mContext) / 2, WindowUtil.getWindowHeight(mContext) / 2, WindowUtil.getWindowWidth(mContext) / 2, mPaint);
}
}
drawCircle方法的第一个和第二个参数是设定圆的中心点,这里设置成屏幕的中心,第三个参数是圆的半径,第四个参数就是之前定义的Paint。运行效果如下:
接下来修改程序如下:
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.glh.project20170707.util.WindowUtil;
public class MyView extends View {
//画图需要的笔
private Paint mPaint;
//圆中心X坐标
private int mCircleX;
//圆中心Y坐标
private int mCircleY;
//圆半径
private int mRadius;
//圆最大半径
private int mMaxRadius;
//是否运行
private boolean mRunning;
private MyThread mMyThread;
private Context mContext;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
this.initPaint();
this.initCircle();
}
/**
* 初始化笔
*/
private void initPaint() {
//创建一支笔,并打开抗锯齿
this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//我要一支红色的笔
this.mPaint.setColor(Color.RED);
//设置画笔的粗细度
this.mPaint.setStrokeWidth(5);
//设置画笔样式
this.mPaint.setStyle(Paint.Style.FILL);
/*
* 画笔样式分三种:
* 1.Paint.Style.STROKE:描边
* 2.Paint.Style.FILL_AND_STROKE:描边并填充
* 3.Paint.Style.FILL:填充
*/
mPaint.setStyle(Paint.Style.STROKE);
}
/**
* 初始化绘制圆的相关属性
*/
private void initCircle() {
this.mCircleX = WindowUtil.getWindowWidth(mContext) / 2;
this.mCircleY = WindowUtil.getWindowHeight(mContext) / 2;
this.mMaxRadius = WindowUtil.getWindowWidth(mContext) / 2;
this.mRadius = this.mMaxRadius;
this.mRunning = false;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(this.mCircleX, this.mCircleY, this.mRadius, mPaint);
}
class MyThread extends Thread {
@Override
public void run() {
while (mRunning) {
try {
sleep(100);
mRadius-=20;
if (mRadius <= 0) {
mRadius = mMaxRadius;
}
postInvalidate();
} catch (InterruptedException ignored) {
}
}
}
}
/**
* 开启
*/
public void start() {
this.mRunning = true;
if (null == this.mMyThread) {
this.mMyThread = new MyThread();
}
this.mMyThread.start();
}
public void stop() {
if (this.mMyThread != null) {
this.mRunning = false;
this.mMyThread.interrupt();
this.mMyThread = null;
this.mMyThread = null;
}
}
public boolean isRunning() {
return this.mRunning;
}
}
程序中,定义了一个线程,用于不停的减少半径值,并通过postInvalidate方法进行重绘。运行程序如下:
到这里我们已经自定义了一个不停变小的圆环,在自定义View这条路上已经迈了一大步,总结到现在的知识点:
1. 通过继承View创建自定义控件,并重写四个构造器。 2. 重写onDraw方法进行绘制并获取画布Canvas。 3. 创建画笔Paint,并设置相关属性。 4. 通过drawCircle方法绘制圆。 5. 通过postInvalidate在非UI线程中进行重绘。
##PorterDuffXfermode
在日常绘图时,如果需要将我们绘制的图像进行混合形成新的图像的,可以使用PorterDuffXfermode,通过它可以很轻松的完成多个图像间混合,方法如下:
this.mRectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
实例化PorterDuffXfermode时,需要传入PorterDuff内部的枚举类Mode,通过Mode枚举类提供的模式来实现我们的想要的效果,下图使用PorterDuffXfermode时的各种模式。
按照上图,挑选几个进行模式来演示,先编写自定义MyView,代码如下:
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
//图形画笔
private Paint mCirclePaint;
//矩形画笔
private Paint mRectPaint;
private Context mContext;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
this.initCirclePaint();
this.initRectPaint();
}
private void initCirclePaint() {
this.mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
this.mCirclePaint.setColor(Color.RED);
}
private void initRectPaint() {
this.mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
this.mRectPaint.setColor(Color.GRAY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(300, 400, 200, this.mCirclePaint);
canvas.drawRect(280, 380, 700, 800, this.mRectPaint);
}
}
程序中通过onDraw方法绘制了两个图像,分别是圆和矩形。效果如下:
1、CLEAR模式
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
//图形画笔
private Paint mCirclePaint;
//矩形画笔
private Paint mRectPaint;
private Context mContext;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
this.initCirclePaint();
this.initRectPaint();
}
private void initCirclePaint() {
this.mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
this.mCirclePaint.setColor(Color.RED);
}
private void initRectPaint() {
this.mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
this.mRectPaint.setColor(Color.GRAY);
this.mRectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
int layerId = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
canvas.drawCircle(300, 400, 200, this.mCirclePaint);
canvas.drawRect(280, 380, 700, 800, this.mRectPaint);
this.mRectPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}
其余的大家可以自行尝试,接着利用上面的混合模式来自定义一个刮刮乐的效果,效果如下:
代码如下:
package com.glh.project20170707.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.glh.project20170707.R;
import com.glh.project20170707.util.WindowUtil;
public class MyView extends View {
private Bitmap mBitmap;
private Bitmap mForegroundBitmap;
private Canvas mCanvas;
private Paint mPaint;
private Path mPath;
private Context mContext;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
this.init();
}
private void init() {
// 实例化路径对象
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
// 设置画笔透明
mPaint.setColor(android.R.color.transparent);
// 设置混合模式为DST_IN
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(30);
//底图
mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.image);
//前背景
mForegroundBitmap = Bitmap.createBitmap(WindowUtil.getWindowWidth(mContext), WindowUtil.getWindowHeight(mContext), Bitmap.Config.ARGB_8888);
mForegroundBitmap = Bitmap.createScaledBitmap(mForegroundBitmap, mBitmap.getWidth(), mBitmap.getHeight(), true);
mCanvas = new Canvas(mForegroundBitmap);
mCanvas.drawColor(Color.GRAY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, null);
canvas.drawBitmap(mForegroundBitmap, 0, 0, null);
mCanvas.drawPath(mPath, mPaint);
}
private float mDownX;
private float mDownY;
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(x, y);
mDownX = x;
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - mDownX);
float dy = Math.abs(y - mDownY);
if (dx >= 2 || dy >= 2) {
mPath.quadTo(mDownX, mDownY, (x + mDownX) / 2, (y + mDownY) / 2);
mDownX = x;
mDownY = y;
}
break;
}
invalidate();
return true;
}
}
程序中创建了mBitmap也就是我们的刮刮乐的图片,并将它绘制在屏幕上,mForegroundBitmap是前景图,我们的mCanvas作用在它之上,并设置混合模式DST_IN,通过drawPath方法绘制路径,并通过Path的quadTo方法绘制圆滑的曲线,实现手指实时刮图,就需要监听手指的触摸事件,这里重写了onTouchEvent方法来记录手指的滑动起始到滑动所在位置的距离,通过invalidate方法重绘。