本实例要实现一个画图板,当用户在触屏上移动时,即可在屏幕上绘制任意的形状。实现手绘功能其实是一种假象:表面上看起来可以随用户在触屏上自由地划线,实际上依然利用的是Canvas的drawLine()方法画直线,每条线都是从上一次拖动时间的发生点画到本次拖动事件的发生点。当用户在触屏上移动时,两次拖动事件发生的发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是直线了。借助于Android提供的Path类,可以非常方便地实现这种效果。:
需要指出的是,如果程序每次都只是从上次拖动事件的发生点绘制一条直线到本次拖动事件的发生点,那么用户前面绘制的就会丢失。为了保留用户之前绘制的内容,程序要借助于“双缓冲”技术。
所谓双缓冲技术其实很简单:当程序需要在指定View上进行绘制时,程序并不直接绘制到该View组件上,而是先绘制到内存中的一个Bitmap图片(这就是缓冲区)上,等到内存中的Bitmap绘制好之后,再一次性地将Bitmap绘制到View组件上。
实现思路: 1).定义一个内存中图片,将他作为缓冲区Bitmap cacheBitmap = null;
2).定义缓冲区Cache的Canvas对象 Canvas cacheCanvas = null;
3).设置cacheCanvas将会绘制到内存的bitmap上。 cacheCanvas.setBitmap(cacheBitmap);
4). 将cacheBitmap绘制到该View上.。canvas.drawBitmap(cacheBitmap,0,0,p);
该程序需要一个自定义View,该View的代码如下:
public class DrawView extends View {
float preX;
float preY;
public Paint paint = null;
private Bitmap cacheBitmap = null;
private Canvas cacheCanvas = null;
private Path path;
public DrawView(Context context, int width, int height) {
super(context);
//创建一个与该View具有相同大小的缓冲区
cacheBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas();
path = new Path();
//设置cacheCanvas将会绘制到内存中的cacheBitmap上
cacheCanvas.setBitmap(cacheBitmap);
//设置画笔的颜色,Paint.DITHER_FLAG是使位图进行有利的抖动的位掩码标志
paint = new Paint(Paint.DITHER_FLAG);
paint.setColor(Color.RED);
//设置画笔风格
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1);
//设置抗锯齿
paint.setAntiAlias(true);
paint.setDither(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float X = event.getX();
float Y = event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//从前一个点绘制到当前点之后,把当前点定义成下次绘制的前一个点
<strong> path.moveTo(X, Y);
preX = X;
preY = Y;</strong>
break;
case MotionEvent.ACTION_MOVE:
//从前一个点绘制到当前点之后,把当前点定义成下一次绘制的前一个点
<strong>path.quadTo(preX, preY, X, Y);
preX = X;
preY = Y;</strong>
break;
case MotionEvent.ACTION_UP:
<strong>cacheCanvas.drawPath(path, paint); </strong> //1
//清除path设置的所有属性值
path.reset();
break;
}
//通知View组件重写调用onDraw()方法重绘该组件
invalidate();
//返回true表明处理方法已经处理该事件
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint bmpPaint = new Paint();
//将cacheBitmap绘制到该View上
canvas.drawBitmap(cacheBitmap, 0, 0, bmpPaint); //2
canvas.drawPath(path, paint);
}
}
上面程序中的粗体字代码为触摸屏的拖动事件提供了响应---只是简单地修改了preX,preY两个属性,并通知该组件重绘。
在这个自定义的View组件中,程序重写了View的onDraw(Canvas canvas)方法,,注意该方法中的 / /2 号代码,这行代码并不是调用该View的Canvas进行绘制,而是调用了缓存Bitmap的Canvas进行绘图,这表明是向缓冲区绘图。程序的 / /2 号粗体字代码将缓冲区中的Bitmap对象绘制到View组件上-------这就是所谓的“双缓冲”技术。
提供了上面的DrawView之后,接下来把该组件添加到主界面中,本程序无需使用界面布局文件,本程序直接在Activity中使用代码创建程序界面。
本程序还提供了菜单来设置画笔的颜色和笔触大小,改程序的菜单资源文件如下:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/color">
<menu>
<!-- 定义一组单选菜单项-->
<group android:checkableBehavior="single">
<!-- 定义多个菜单项-->
<item android:id="@+id/red"
android:title="@string/color_red"/>
<item android:id="@+id/green"
android:title="@string/color_green"/>
<item android:id="@+id/blue"
android:title="@string/color_blue"/>
</group>
</menu>
</item>
<item android:title="@string/width">
<menu>
<group>
<!-- 定义三个菜单项-->
<item android:id="@+id/width_1"
android:title="@string/width_1"/>
<item android:id="@+id/width_2"
android:title="@string/width_3"/>
<item android:id="@+id/width_3"
android:title="@string/width_5"/>
</group>
</menu>
</item>
<item android:id="@+id/blur" android:title="@string/blur"/>
<item android:id="@+id/emboss" android:title="@string/emboss"/>
</menu>
主程序负责加载、显示界面布局,加载、显示上面的菜单资源。出此之外,程序还要为各菜单编写事件响应,程序代码如下:
import android.graphics.BlurMaskFilter;
import android.graphics.Color;
import android.graphics.EmbossMaskFilter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.LinearLayout;
public class MainActivity extends AppCompatActivity {
EmbossMaskFilter emboss;
BlurMaskFilter blur;
DrawView drawView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout linearLayout = new LinearLayout(this);
DisplayMetrics displayMetrics = new DisplayMetrics();
//获取创建的高度和宽度
getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
//创建一个DrawView,该DrawView的宽度与高度与该Activity保持相同
drawView = new DrawView(this, displayMetrics.widthPixels, displayMetrics.heightPixels);
linearLayout.addView(drawView);
setContentView(linearLayout);
emboss = new EmbossMaskFilter(new float[]{1.5f,1.5f,1.5f,1.5f}, 0.6f, 6, 4.2f);
blur = new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL);
}
@Override
//负责创建菜单项
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = new MenuInflater(this);
//加载R.menu.my_menu对应的菜单,并添加到menu中
inflater.inflate(R.menu.menu_main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
//菜单被单击后的回调方法
public boolean onOptionsItemSelected(MenuItem item) {
//判断单击的是哪个菜单项,并有针对性地做出响应
switch(item.getItemId()){
case R.id.red:
drawView.paint.setColor(Color.RED);
item.setChecked(true);
break;
case R.id.blue:
drawView.paint.setColor(Color.BLUE);
item.setChecked(true);
break;
case R.id.green:
drawView.paint.setColor(Color.GREEN);
item.setChecked(true);
break;
case R.id.width_1:
drawView.paint.setStrokeWidth(1);
break;
case R.id.width_2:
drawView.paint.setStrokeWidth(3);
break;
case R.id.width_3:
drawView.paint.setStrokeWidth(5);
break;
case R.id.blur:
drawView.paint.setMaskFilter(blur);
break;
case R.id.emboss:
drawView.paint.setMaskFilter(emboss);
break;
}
return true;
}
}
上面的程序代码比较简单,当用户单击不同的菜单项之后,程序只要简单地修改DrawView组件内的Paint对象的颜色和笔触粗细即可。运行上面程序,将看到如图所示界面:
附加:贝塞尔曲线初探