第四章 双缓存技术
4.1、双缓存
什么是双缓存 ? 说白了,就是两个绘图区。一个是 Bitmap的 Canvas;另一个就是当前 View的 Canvas。先将图像绘制在 Bitmap上,然后再将Bitmap 绘制 View上,也就是说,我们在 View 上看到效果其实就 Bitmap上的内容。这样做有什么有意呢 ? 概括起来,有以下几点:- 提高绘图绘图性能
- 先将内容绘制在 Bitmap撒花姑娘,再统一将内容绘制在 View上,可以提高绘图的性能
- 可以在屏幕上展示绘图的过程
- 将线条直接绘制在 View上 和 先绘制在 Bitmap 上,再绘制在 View上是感受不到这个作用的。但是,如果是画一个矩形呢 ? 情况就完全不一样了。我们用手指在屏幕上按下,斜拉。此时,因该从按下的位置开始,拉出一个随手之变化大小的矩形。因为要想用户展示整个过程,所以需要不断绘制矩形。但是,对,但是,手指抬起后留下的其实只需要最后一个。所以,问题就在这里。怎么解决呢 ?使用双缓存,在 View的 onDraw()方法中绘制用于展示绘制过程的矩形,在手指移动的过层中,会不断刷新重绘,用户总能看到当前应有的矩形。而且不会留下历史痕迹(因为重绘,只是重绘最后一次)
- 保存绘图历史
- 前面提到,因为直接在 View 的Canvas上绘图不会保存历史痕迹,所以也带来了副作用,以前绘制的内容也没有了(可能当前绘制的是第二个矩形),这个时候,双缓存的优势就体现出来了,我们可以将绘制的历史结果保存在一个 Bitmap上,当手指松开时,将最后的矩形绘制在 Bitmap上,同时再将 Bitmap的内容整个绘制在 View上
我们用了大篇幅的形容这个过程,听起来有些拗口,和 模糊。接下来通过示例来呈现问题和解决问题
4.2、在屏幕上绘制曲线
这是一个入门级的讨论,在屏幕上绘制曲线一般不会遇到什么问题,只要知道在屏幕上随手绘曲线的原理就行了。我们简要的分析一下:我们在屏幕上绘制的曲线,本质上就是由无数条直线的连接而成。就算曲线比较平滑,看不到折现,也是有构成曲线的直线足够短。
当手指在屏幕上移动时,会产生三个动作:手指按下(ACTION_DOWN),手指移动(ACTION_MOVE),手指松开(ACTION_UP)。手指按下时,要记录手指所在的坐标,假设此时的 x 方向和 y 方向的坐标分别为 (preX,preY),当手指在屏幕上移动时。系统会每隔一段时间自动告知手指的当前位置,假设手指的当前位置是 x 和 y。现在,上一个点的坐标为(preX,preY),当前坐标是(x,y)。调用 drawLine(preX,preY,x,y)方法可以将这两个点连接起来。同时,当前点的坐标会成为下一条条直线的上一个点的坐标,preX = x,preY = y。如此,循环往复,直到松开手时,一条由若干直线组成的曲线便绘制好了。
另外,虽然我们知道,调用 View的 invalidate()方法重绘时,最终调用的是 onDraw()方法,但是,一定要注意,由于重绘的请求最终会一级级网上提交到 ViewRoot;然后,ViewRoot在调用 scheduleTraversals()方法发起重绘请求。而 scheduleTraversals()发送的是异步消息。所以,再通过手势绘制线条时,为了解这个问题,可以使用 Path绘图。但是,如果要保存绘图历史,就要使用双缓存了。
下面的代码实现了 通过手势在屏幕上绘制曲线的功能:
public class Line1View extends View {
/**上一个点坐标*/
private int preX,preY ;
/**当前点的坐标*/
private int currentX,currentY ;
/**Bitmap 缓存区*/
private Bitmap bitmapBuffer ;
private Canvas bitmapCanvas ;
private Paint paint ;
public Line1View(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setColor(Color.RED) ;
paint.setStrokeWidth(5) ;
}
/**
* 组件大小发生 改变的时候,会调用此方法。 我们在这里初始化Bitmap,宽高设置为 View 的宽高
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(bitmapBuffer == null){
int width = getMeasuredWidth() ;
int height = getMeasuredHeight() ;
//新建 Bitmap对象
bitmapBuffer = Bitmap.createBitmap(width,height,Config.ARGB_8888) ;
bitmapCanvas = new Canvas(bitmapBuffer) ;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将bitmap中的内容绘在 View上
canvas.drawBitmap(bitmapBuffer, 0,0,null) ;
}
/**
* 处理手势
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN: //按下
preX = x ;
preY = y ;
break ;
case MotionEvent.ACTION_MOVE:
currentX = x ;
currentY = y ;
bitmapCanvas.drawLine(preX, preY, currentX, currentY, paint) ;
//重新赋值
preX = currentX ;
preY = currentY ;
invalidate() ;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
我们首先定义了一个名为 bitmapBuffer的 Bitmap对象,为了在该对象上绘图,创建了一个与之关联的 Canvas对象 bitmapCanvas。创建 Bitmap对象时,需要考虑它的大小,在 Line1View类的构造方法中。因为 Line1View尚未创建,还不知宽度和高度,所以,重写了 onSizeChanged()方法。该方法在组件创建后且大小发生改变时回调(View 第一次显示肯定调用),代码中看到,Bitmap对象的宽度和高度与 View相同。手指按下后,将第一次的坐标值保存在 preX 和 preY两个变量中。手指移动时,获取手指所在的新位置,并保存到 currentX 和 currentY 中。此时,已经知道了起点和终点两个点的坐标,将这两个点确定的一条直线绘制到 bitmapBuffer对象。然后,立马又将 bitmapBuffer对象绘制在 View上。最后,重新设置 preX 和 preY的值。确保(preX,preY)成为下一个点的其实坐标
从下面的运行效果中看出,bitmapBuffer对象保存了所有的绘图历史。这也是双缓存的作用之一
上面的案例中,我们直接在 Bitmap关联的 Canvas上绘制直线。其实更好的做法是通过 Path来绘图,不管从功能上还是效率上这都是更有的选择,主要体现在:
- Path 可以用于保存实时绘图坐标,避免调用 invalidate()方法重绘时因 ViewRoot的 scheduleTraversals()方法发送异步请求出现的问题
- Path 可以用来绘制复杂的图形
- 使用 Path 绘图效率更高
public class Line2View extends View {
private Path path ;
/**上一个点坐标*/
private int preX,preY ;
private Paint paint ;
public Line2View(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setColor(Color.RED) ;
paint.setStrokeWidth(5) ;
path = new Path() ;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(path, paint) ;
}
/**
* 处理手势
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN: //按下
path.reset() ;
preX = x ;
preY = y ;
path.moveTo(x, y) ;
break ;
case MotionEvent.ACTION_MOVE:
path.quadTo(preX, preY, x, y) ;
invalidate() ;
preX = x ;
preY = y ;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
上面使用 Path来绘制曲线,Path对象保存了手指从按下到移动到松开的整个运动轨迹,进行第二次绘制时,Path调用了 reset()方法重置,继续进行下一条曲线绘制。通过调用 quadTo()方法绘制二阶贝赛尔曲线。因为需要指定一个起点,所以手指按下时调用了 moveTo(x,y)方法。但是。运行后我们发现,绘制当前曲线没有问题。但是,绘制下一条曲线时候签一条曲线消失了(如下图所示),原因是没有保存绘图历史,这需要通过“双缓存”解决
下面改进方案,我们在代码中加入一个 Bitmap对象,绘图历史保存在该对象中。这样,即可以显示当前的绘图过程,又可以保存绘图历史。所有的内容都不会丢失
public class Line3View extends View {
private Path path ;
/**上一个点坐标*/
private int preX,preY ;
private float currentX ;
private float currentY ;
private Paint paint ;
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;
public Line3View(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setColor(Color.RED) ;
paint.setStrokeWidth(5) ;
path = new Path() ;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(bitmapBuffer == null){
int width = getMeasuredWidth();
int height = getMeasuredHeight();
bitmapBuffer = Bitmap.createBitmap(width, height,Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmapBuffer);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmapBuffer, 0, 0, null);
canvas.drawPath(path, paint) ;
}
/**
* 处理手势
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN: //按下
path.reset() ;
preX = x ;
preY = y ;
path.moveTo(x, y) ;
break ;
case MotionEvent.ACTION_MOVE:
//手指移动过程中显示绘制过程
path.quadTo(preX, preY, x, y) ;
invalidate() ;
preX = x ;
preY = y ;
break;
case MotionEvent.ACTION_UP:
//手指松开后将最终的绘图结果绘制在 bitmapBuffer中,同时绘制到 View上
bitmapCanvas.drawPath(path, paint) ;
invalidate() ;
break;
}
return true;
}
}
我们在画曲线时,使用了 path类的 quadTo()方法,该方法能绘制出相对平滑的贝赛尔曲线,但是控制点和起始点使用同一点。这样的效果不是很理想。现在提供了一种计算控制点的方法。假如:起始点坐标为(x1,y1),终点坐标为(x2,y2),控制点的坐标即为 ((x1 + x2 )/ 2,(y1 + y2) / 2) 修改MotionEvent.ACTION_MOVE处的代码如下
case MotionEvent.ACTION_MOVE:
//手指移动过程中显示绘制过程
//使用贝赛尔曲线进行绘制,需要一个起点(preX,preY)
//一个终点(x,y),一个控制点((x1+x2)/2,(y1+y2)/2)
int controlX = (int) ((x+preX) * 0.5f);
int controlY = (int) ((y+preY) * 0.5f) ;
path.quadTo(controlX, controlY, x, y) ;
invalidate() ;
preX = x ;
preY = y ;
break;
4.3、在屏幕上绘制矩形
绘制矩形的逻辑和曲线不一样,手指按下时,记录初始坐标(firstX,firstY),手指移动过程中,不断获取新的坐标(x,y),然后,(firstX,firstY)为左上角位置,(x,y)为右下角位置画出矩形。矩形的 4 个属性 left,top,right,bottom的指分别是 firstX,firstY,x 和 y。我们首先实现没有使用双缓存的效果public class Rect1View extends View {
private int firstX,firstY ;
private Path path ;
private Paint paint;
public Rect1View(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setColor(Color.RED) ;
paint.setStrokeWidth(5) ;
path = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(path, paint) ;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.reset() ;
firstX = x ;
firstY = y ;
break;
case MotionEvent.ACTION_MOVE:
//绘制矩形时,先清除前一次的结果
path.reset() ;
path.addRect(firstX, firstY,x,y,Direction.CCW);
invalidate() ;
break ;
case MotionEvent.ACTION_UP:
invalidate();
break ;
}
return true;
}
}
和前面的曲线一样,并没有显示历史绘图,因为 invalidate()后绘图历史根本没有保存,path对象中只保存当前正在绘制的矩形信息。要实现正确的效果,必须将每一次灰土都保存在 Bitmap缓存中。这样,Bitmap保存绘图历史,Path中保存当前正在绘制的内容,即实现了功能,又可以展示绘图流程 我们在根据手势绘制矩形的时候,(fristX,firstY)和(x,y)代表不同的角的坐标。它们可以从 4个方向分别绘制矩形。分别是 ↘ ↖ ↗ ↙。上面只是考虑了 ↘ 这样一种情况,其它的情况并没有靠考虑进去。(如下图所示)不同方法去绘制矩形时,矩形的 4个属性值对应值也会不同
通过上面的概括对以上代码进行重新编写
public class Rect2View extends View {
private int firstX,firstY ;
private Path path ;
private Paint paint;
private Bitmap bitmapBuffer; ;
private Canvas bitmapCanvas ;
public Rect2View(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setColor(Color.RED) ;
paint.setStrokeWidth(5) ;
path = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(bitmapBuffer == null){
bitmapBuffer = Bitmap.createBitmap(
getMeasuredWidth(),getMeasuredHeight(),Config.ARGB_8888) ;
bitmapCanvas = new Canvas(bitmapBuffer) ;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmapBuffer, 0,0, paint);
canvas.drawPath(path, paint) ;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = x ;
firstY = y ;
break;
case MotionEvent.ACTION_MOVE:
//绘制矩形时,先清除前一次的结果
path.reset() ;
// ↘ 方向
if(firstX < x && firstY < y){// ↘ 方向
path.addRect(firstX, firstY,x,y,Direction.CCW) ;
}else if(firstX > x && firstY > y ){ //↖ 方向
path.addRect(x, y,firstX,firstY,Direction.CCW) ;
}else if(firstX > x && firstY < y){ //↙ 方向
path.addRect(x, firstY,firstX,y,Direction.CCW) ;
}else if(firstX < x && firstY > y){ //↗ 方向
path.addRect(firstX, y,x,firstY,Direction.CCW) ;
}
invalidate() ;
break;
case MotionEvent.ACTION_UP:
bitmapCanvas.drawPath(path, paint) ;
invalidate();
break ;
}
return true;
}
}
运行效果如下:
4.4、案例绘图示例
接下来实现一个稍微复杂的示例,其中的 撤销功能有问题本示例涉及类如下 :
- ShapeDrawe:绘图的基类,所有的绘图偶必须继承该类
- LineDrawe:继承自 ShapeDrawe,用于绘制曲线
- RectDrawe:继承自 ShapeDrawe,用于绘制矩形,手指按下确定左上角的位置,手指松开确定右下角的位置
- OvalDrawe:继承自 RectDrawe,用于绘制椭圆anxaioqueing
- AttributesToo:用于存储绘制参数,并将绘图参数转化成 Pain对象,使用单例模式
- BitmapBuffe:保存绘图的最终历史结果,很重要的一个类。同时支持撤销操作,使用单例模式
- BitmapHistor:保存了每次绘制的历史结果,用于撤销,使用单例
- ImageVie:绘图 View
- SystemParam:系统参数
- SecondActivity:主窗口
4.4.1 绘图属性
绘制的图形与颜色、线条粗细、类型(空心 or 实心)有关,另外,通过这写图形属性在此封装成 Paint对象,不需要为每一次绘图偶创建这些属性,共享是比较经济的做法。我们定义了一个名为 AttributesTool的类,用于保存这写属性。并且对外提供 Paint对象,考虑到一个对象即可,所以使用了 单例模式——AttributesTool 永远只有一个对象存在。单例模式是一种比较简单的设计模式,在任何情况下,只允许该类创建一个实例,为了防止外部肆无忌惮的创建多个对象,可以将构造方法定义为 private,并提供一个静态方法 getInstance()向用户提供对象
public class AttributesTool {
/**绘图颜色*/
private int color ;
/**线条的宽度*/
private int borderWidth ;
/**是否填充,默认空心*/
private boolean fill ;
private Paint paint ;
private static AttributesTool self ;
/**将构造方法定义成私有,目的防止创建对象*/
private AttributesTool(){
reset() ;
}
/**向外提供实例对象*/
public static AttributesTool getInstance(){
if(self == null){
self = new AttributesTool() ;
}
return self ;
}
/**将当前的绘图属性转成 Paint对象*/
public Paint getPaint(){
if(paint == null){
paint = new Paint() ;
}
paint.setAntiAlias(true) ;
paint.setColor(this.color) ;
paint.setStrokeWidth(borderWidth) ;
paint.setStyle(this.fill ? Style.FILL : Style.STROKE) ;
paint.setTextSize(30) ;
return paint ;
}
/**重置*/
public void reset(){
this.color = Color.BLACK ;
this.borderWidth = 1 ;
this.fill = false ;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getBorderWidth() {
return borderWidth;
}
public void setBorderWidth(int borderWidth) {
this.borderWidth = borderWidth;
}
public boolean isFill() {
return fill;
}
public void setFill(boolean fill) {
this.fill = fill;
}
}
4.4.2 软件参数
SystemParams类定义了几个静态变量,用于保存软件中的共享参数。如:绘图区的宽度和高度、是否撤销标志等public class SystemParams {
/**绘图区的宽度*/
public static int areaWidth ;
/**绘图区的高度*/
public static int areaHeight ;
/**是否撤销*/
public static boolean isRedo ;
}
4.4.3 软件参数
BitmapBuffer类定义了绘图缓冲区,提供了“双缓存”中的 Bitmap对象,该对象显然也只需要保持一个对象即可,在软件的运行周期中,不管绘制什么图形,都统一使用唯一的 Bitmap对象作为缓存区。所以,BitmapBuffer类依然使用了单例模式 BitmapBuffer类还可以获取缓存区管理的 Bitmap对象和它关联的 Canvas画布。同时,提供了 撤销所需的方法public class BitmapBuffer {
private Bitmap bitmap ;
private Canvas canvas ;
private static BitmapBuffer self;
private BitmapBuffer(int width,int height){
init(width,height) ;
}
public static BitmapBuffer getInstance(){
if(self == null){
self = new BitmapBuffer(SystemParams.areaWidth, SystemParams.areaHeight) ;
}
return self ;
}
private void init(int width,int height){
bitmap = Bitmap.createBitmap(width,height,Config.ARGB_8888) ;
canvas = new Canvas() ;
canvas.setBitmap(bitmap) ;
}
/**获取缓冲区的画布*/
public Canvas getCanvas() {
return canvas;
}
/**获取绘图结果*/
public Bitmap getBitmap() {
return bitmap;
}
/**将当前的绘图结果保存到栈中*/
public void pushBitmap(){
BitmapHistory.getInstance().pushBitmap(
bitmap.copy(Config.ARGB_8888,false)) ;
}
/**撤销*/
public void reDo(){
BitmapHistory history = BitmapHistory.getInstance();
if(history.isReDo()){
Bitmap bmp = history.reDo() ;
if(bmp != null){
bitmap = bmp.copy(Config.ARGB_8888, true) ;
//必须重新关联画布
canvas.setBitmap(bitmap) ;
//回收
if(bmp != null && !bmp.isRecycled()){
bmp.recycle() ;
System.gc() ;
bmp = null ;
}
}
}
}
}
因为随时获取 Bitmap对象用于绘图,为了简化获取 Bitmap对象的操作。软件启动后将绘图区的宽度和高度保存在 SystemParams中,获取 Bitmap时就不需要再次指定宽度和高度
4.4.4 撤销操作
本案例使用了简易的撤销实现,在绘图历史类 BitmapHistory中定义了一个栈 Stack对象。这是一种后进先出的数据结构。用户每绘制一笔都会将当前的 Bitmap 缓存区压入栈中,由于 Bitmap消耗资源巨大,Stack中保存 Bitmap对象过多会导致崩溃。我们之定义了 5步撤销。一旦撤销还要及时回收 Bitmap资源:if(bmp !=null && !bmp.isRecycle()){
bmp.recycle();
System.gc();
bmp = null;
}
recycle()用于回收 Bitmap资源。System.gc()提醒 JVM 启用垃圾回收机制。最后,将 bmp置空可以是垃圾更快被回收 我们只需要维护一个 BitmapHistory对象。所以,本类采用了单例模式。Stack类是 Java中定义的栈结构。相当于一个杯子,往杯子中放与杯口直径相同大小的球。是一种后进先出的数据机构.。push()方法表示压栈,将元素压入栈顶。pop()方法表示出栈,也就是将栈顶元素删除。同时,返回被删除的元素。peek()方法是直接读出栈顶的元素,但是并不删除
public class BitmapHistory {
private static Stack<Bitmap> stack ;
private static BitmapHistory self;
private BitmapHistory(){
if(stack == null){
stack = new Stack<Bitmap>() ;
}
}
public static BitmapHistory getInstance(){
if(self == null){
self = new BitmapHistory() ;
}
return self ;
}
/**将当前的历史绘图结果压栈*/
public void pushBitmap(Bitmap bitmap){
int count = stack.size() ;
if(count >= 5){
Bitmap bmp = stack.remove(0) ;
if(!bmp.isRecycled()){
bmp.recycle() ;
System.gc() ;
bmp = null ;
}
}
stack.push(bitmap) ;
}
/**撤销*/
public Bitmap reDo(){
//将顶部的元素删除
Bitmap bmp = stack.pop() ;
//回收位图资源
if(bmp != null && !bmp.isRecycled()){
bmp.recycle() ;
System.gc() ;
bmp = null ;
}
if(stack.empty()) return null ;
//返回撤销后的位图对象
return stack.peek() ;
}
/**判断是否可以撤销
* true 表示不是空,可撤销
* false 表示空,不能撤销
*/
public boolean isReDo(){
return !stack.empty() ;
}
}
4.4.5 图形绘制
本案例支持曲线、矩形、椭圆、和圆的绘制。为此,我们抽象出一个抽象类 shapeDrawer。该类用于处理图形的绘制、逻辑、和手势响应。对应的三个方法:draw()、logic() 和 onTouchEvent()。其中,draw()方法又了基本的实现,即绘制 Bitmap缓存区,当前正在绘制的图形则有子类扩展public abstract class ShapeDrawer {
private View view ;
public ShapeDrawer(View view){
super() ;
this.view = view ;
}
public View getView() {
return view;
}
/**用于绘图
* 用于展示结果的画布
*/
public void draw(Canvas viewCanvas){
//画历史结果
Bitmap bitmap = BitmapBuffer.getInstance().getBitmap() ;
viewCanvas.drawBitmap(bitmap, 0, 0,null);
}
/**用于响应触摸事件*/
public abstract boolean onTouchEvent(MotionEvent event);
/**绘图的逻辑*/
public abstract void logic() ;
}
前面的小节中,我们已经讨论过 曲线和矩形的绘制,在这里。矩形的绘制见 RectDraWer类
public class RectDrawer extends ShapeDrawer {
private float firstX ;
private float firstY ;
private float currentX ;
private float currentY ;
public RectDrawer(View view) {
super(view);
}
@Override
public void draw(Canvas viewCanvas) {
super.draw(viewCanvas);
drawShape(viewCanvas, firstX, firstY, currentX, currentY);
}
protected void drawShape(Canvas canvas, float firstX, float firstY,
float currentX, float currentY) {
Paint paint = AttributesTool.getInstance().getPaint() ;
//判断手指的方向
if(firstX < currentX && firstY < currentY){ //↘
canvas.drawRect(firstX,firstY,currentX,currentY, paint) ;
}else if(firstX > currentX && firstY < currentY){ //↙
canvas.drawRect(currentY, firstY, firstX, currentY, paint) ;
}else if(firstX < currentX && firstY > currentY){ // ↗
canvas.drawRect(firstX, currentY, currentX, firstY, paint) ;
}else if(firstX > currentX && firstY > currentY){ //↖
canvas.drawRect(currentX, currentY, firstX, firstY, paint);
}
}
/**画当前的形状*/
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
firstX = x;
firstY = y;
break;
case MotionEvent.ACTION_MOVE:
currentX = x;
currentY = y;
getView().invalidate();
break;
case MotionEvent.ACTION_UP:
//将最终的矩形绘制在 缓冲区
Canvas canvas = BitmapBuffer.getInstance().getCanvas() ;
drawShape(canvas, firstX, firstY, currentX, currentY) ;
getView().invalidate() ;
//保存到撤销栈中
BitmapBuffer.getInstance().pushBitmap() ;
break;
}
return true;
}
@Override
public void logic() {
// TODO Auto-generated method stub
}
}
drawShape()方法负责沪指矩形,同时考虑到各个方向的绘制。绘制椭圆的时,基本上可以重用 RectDrawer类的代码,重写 drawShape()方法即可
public class OvalDrawer extends RectDrawer {
public OvalDrawer(View view) {
super(view);
// TODO Auto-generated constructor stub
}
@Override
protected void drawShape(Canvas canvas, float firstX, float firstY,
float currentX, float currentY) {
Paint paint = AttributesTool.getInstance().getPaint() ;
//判断手指的方向
if(firstX < currentX && firstY < currentY){ //↘
canvas.drawOval(new RectF(firstX,firstY,currentX,currentY), paint) ;
}else if(firstX > currentX && firstY < currentY){ //↙
canvas.drawOval(new RectF(currentY, firstY, firstX, currentY), paint) ;
}else if(firstX < currentX && firstY > currentY){ // ↗
canvas.drawOval(new RectF(firstX, currentY, currentX, firstY), paint) ;
}else if(firstX > currentX && firstY > currentY){ //↖
canvas.drawOval(new RectF(currentX, currentY, firstX, firstY), paint);
}
}
}
通过 继承ShapeDrawer进行话曲线
public class LineDrawer extends ShapeDrawer {
private int preX,preY ;
private float currentX ;
private float currentY ;
private Path path ;
public LineDrawer(View view) {
super(view);
path = new Path() ;
}
@Override
public void draw(Canvas viewCanvas) {
super.draw(viewCanvas);
drawShape(viewCanvas, preX, preY, currentX, currentY);
}
private void drawShape(Canvas canvas, int preX2, int preY2,
float currentX2, float currentY2) {
Paint paint = AttributesTool.getInstance().getPaint() ;
canvas.drawPath(path, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX() ;
int y = (int) event.getY() ;
int controlX = 0 ;
int controlY = 0;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN: //按下
path.reset() ;
preX = x ;
preY = y ;
path.moveTo(x, y) ;
break ;
case MotionEvent.ACTION_MOVE:
//手指移动过程中显示绘制过程
//使用贝赛尔曲线进行绘制,需要一个起点(preX,preY)
//一个终点(x,y),一个控制点((x1+x2)/2,(y1+y2)/2)
controlX = (int) ((x+preX) * 0.5f);
controlY = (int) ((y+preY) * 0.5f) ;
path.quadTo(controlX, controlY, x, y) ;
//手指松开后将最终的绘图结果绘制在 bitmapBuffer中,同时绘制到 View上
getView().invalidate() ;
preX = x ;
preY = y ;
break;
case MotionEvent.ACTION_UP:
Canvas canvas1 = BitmapBuffer.getInstance().getCanvas() ;
drawShape(canvas1,controlX, controlY, x, y) ;
getView().invalidate() ;
//保存到撤销栈中
BitmapBuffer.getInstance().pushBitmap() ;
break;
}
return true;
}
@Override
public void logic() {
}
}
4.4.6 绘图区
绘图区 ImageView类是 View的子类,是绘图的核心类。ImageView类接受用户的回去请求,响应用户手势,在 onDraw()方法中,需要判断当前时否撤下操作还是 绘图操作。如果是绘图操作,调用 ShapeDrawer 的 draw()方法。根据 OOP的多态特性根据 ShapeDrawer 的子类对象来绘制对应的图形 对 ImageView 来说,手势处理就完全依赖与 ShapeDrawer 对象的 onTouchEvent()方法了,不同的图形或会根据手势实现不同的绘制功能。与 ImageView本身来说已经没有什么关心了public class ImageView extends View {
private ShapeDrawer shapeDrawer ;
public void setShapeDrawer(ShapeDrawer shapeDrawer) {
this.shapeDrawer = shapeDrawer;
}
public ImageView(Context context, AttributeSet attrs) {
super(context, attrs);
shapeDrawer = new RectDrawer(this) ;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
SystemParams.areaWidth = this.getMeasuredWidth();
SystemParams.areaHeight = this.getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(SystemParams.isRedo){ //撤销
canvas.drawBitmap(BitmapBuffer.getInstance().getBitmap(),
0, 0, null);
SystemParams.isRedo = false ;
}else{
shapeDrawer.draw(canvas);
}
//逻辑
shapeDrawer.logic();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
return shapeDrawer.onTouchEvent(event);
}
}
4.4.7 主机面
主界面有绘图区和功能区组成。功能区包含一个菜单。菜单包含要绘制的图形、参数设置等菜单项。主界面 SecondActivity类构建了 ImageView、ShapeDrawer和布局文件。public class SecondActivity extends Activity {
private ImageView draw;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
draw = (ImageView) findViewById(R.id.drawer);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.second, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
ShapeDrawer shapeDrawer = null ;
AttributesTool at = AttributesTool.getInstance() ;
switch(item.getItemId()){
case R.id.redo:
BitmapBuffer.getInstance().reDo() ;
SystemParams.isRedo = true ;
draw.invalidate() ;
break ;
case R.id.line:
shapeDrawer = new LineDrawer(draw);
break;
case R.id.rect:
shapeDrawer = new RectDrawer(draw);
break;
case R.id.oval:
shapeDrawer = new OvalDrawer(draw);
break;
case R.id.black:
at.setColor(Color.BLACK);
break;
case R.id.red:
at.setColor(Color.RED);
break;
case R.id.yellow:
at.setColor(Color.YELLOW);
break;
case R.id.fill:
at.setFill(true);
break;
case R.id.stroke:
at.setFill(false);
break;
}
if(shapeDrawer != null){
draw.setShapeDrawer(shapeDrawer);
}
return super.onOptionsItemSelected(item);
}
}
运行如下: