Android中的绘图
视频课:https://edu.csdn.net/course/play/7621
学习内容
Ø Android中基本图形的绘制
Ø Android文本的绘制
Ø 双缓冲技术
Ø 图像的绘制及效果处理
能力目标
Ø 能使用View类搭建绘图框架
Ø 能在Android中绘制基本图形
Ø 掌握双缓冲技术在Android中的实现
Ø 能在Canvas上绘制图片并实现各种效果
本章简介
界面是软件与用户交互的最直接的层,界面的好坏决定用户对软件的第一印象。Android系统能够在诸多的移动平台中脱颖而出,漂亮的界面带来的良好的用户体验无疑是其中一个很重要的因素。在我们平时的软件开发中,仅靠系统提供的那些组件来实现界面是远远不够的,在很多情况下我们都需要自己来绘制软件界面。在本章中我们就将学习Android中和绘制图形及位图显示和效果有关的知识。
核心技能部分
1.1 简单绘图
1.1.1 绘制基本图形
玩过愤怒的小鸟的同学一定会为它里面漂亮的界面所吸引,如下图1.1.1所示。这些漂亮的界面是如何显示出来呢,这些界面可以通过绘图的形式实现。
本节中所谓的绘图指的就是在屏幕上绘制一系列基本的图形,比如直线、圆、弧等。这些基本的图形虽然简单,但通过组合以及色彩渲染,它们就可以构成我们所看到的漂亮的程序界面。Android SDK提供了对基本图形以及位图的绘制,所有的绘图操作通常都是在View类的onDraw()方法中进行的。
在Android中绘图只需要继承View类,并重写它的onDraw()方法就可以了。在具体的绘图过程中可能会涉及Paint类、Color类、Canvas类等。其中,Paint类表示画笔,通过它可以设置画笔的精细、样式等,只有先得到画笔才能进行图形的绘制;Color类主要定义了一些颜色常量,利用它可以画出各种彩色的图形; Canvas类相当于画布,除了可以在它上绘制之外,还可以设置它的属性,比如,画布的颜色、尺寸等。
一般情况下,应用程序的组件都是在相同的GUI线程中绘制的,这个主应用程序线程同时也用来处理所有的用户交互(例如,按钮单击或者文本输入)操作。在Android中,任何一个View的子类只需要重写onDraw()方法,就可以实现界面的定制显示。对于一个应用来说除了图形的显示之外还需要有交互功能,比如图形的移动、变形等,但由于Android UI不是线程安全的,而界面刷新操作又必须得在UI线程中执行。为了解决这个问题,我们一般是利用Handler来实现UI线程的更新(通过调用View对象的invalidate()方法)。
Android中的View类提供了onKeyDown、onKeyUP、onTouchEvent、onTrackballEvent等方法来处理用户界面和用户交互所发生的事件。故我们的View类只要重写了这些的方法,当有按键按下或弹起等事件发生时,与之对应的事件处理方法就会被调用。
下面我们通过一个示例程序给大家演示Android中基本图形的绘制。在绘制基本图形之前,我们先搭建一个在Android中编写绘图程序的框架,以后我们的程序都在这个框架的基础之上进行编写。
示例1.1
使用View类搭建绘图框架。
(1)定制一个用来绘制界面的View类
public class GameView extends View {
public GameView(Context context) {
super(context);
}
protected void onDraw(Canvas canvas) {
// 用户自己的绘图代码
}
}
(2)编写一个用来控制整个应用(界面元素动作)的类:
public class ViewFrameActivity extends Activity {
private static final int REFRESH = 0x000001;
private GameView mGameView = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGameView = new GameView(this);
setContentView(mGameView); // 设置显示为我们自定义的视图GameView
new Thread(new GameThread()).start();// 开启线程
}
class GameThread implements Runnable {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
Message message = Message.obtain(); //返回一个Message实例
message.what = ViewFrameActivity.REFRESH;
// 发送消息
myHandler.sendMessage(message);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
Handler myHandler = new Handler() {
// 接收到消息后处理
public void handleMessage(Message msg) {
if(msg.what == ViewFrameActivity.REFRESH){
mGameView.invalidate(); //更新整个屏幕区域
}
super.handleMessage(msg);
}
};
// 这些事件也可以写在GameView类中,当不需要事件处理时,这些方法可以不写。
public boolean onTouchEvent(MotionEvent event) {// 触笔事件
return true;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {// 按键按下事件
return true;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {// 按键弹起事件
return false;
}
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
return true;
}
}
本示例采用在onDraw方法之后调用invalidate()方法实现屏幕的刷新。这种方法会通知UI线程重绘,使View重新调用onDraw()方法,实现刷新屏幕。这样写看起来代码非常简洁漂亮,但是同时也存在一个很大的问题。invalidate()线程和程序主线程是分开的,它违背了单线程模式,这样绘制的话是很不安全的。举个例子,比如程序先进入Activity1中,使用invalidate()方法来重绘;然后我跳到了Activity2,这时候Activity1已经finash()掉,可是Activity1中的invalidate()线程还在程序中,Android的虚拟机不可能主动杀死正在运行中的线程,所以这样操作是非常危险的。总结起来说即,重绘操作在UI线程中是被动调用的,所以不安全。
解决方案,在调用postInvalidate()方法后通知UI线程重绘屏幕。以new Thread(this).start()开启一个绘图主线程,,然后在主线程中通过调用postInvalidate()方法来刷新屏幕。postInvalidate()方法调用后,系统会帮我们调用onDraw方法,它是在我们自己的线程中调用,通过调用它可以通知UI线程刷新屏幕。由此可见它是主动调用UI线程的。所以建议按这种方式使用postInvalidate()方法通知UI线程来刷新整个屏幕。
修改后的代码如下:
public class ViewFrameActivity extends Activity {
private GameView2 mGameView = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGameView = new GameView2(this);
setContentView(mGameView);// 设置显示为我们自定义的视图GameView
new Thread(new GameThread()).start();//开启一个游戏的主线程
}
class GameThread implements Runnable {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
mGameView.postInvalidate();//使用这个方法可以直接在线程中更新界面。
}
}
}
// 这些事件也可以写在GameView中
public boolean onTouchEvent(MotionEvent event) {// 触笔事件
return true;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {// 按键按下事件
return true;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {// 按键弹起事件
return true;
}
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
return true;
}
}
示例1.2
绘制基本图形,包括像素点、直线、圆、弧、多边形、矩形等,最终显示效果如下图1.1.2所示。
有了示例1.1绘图框架的搭建,本示例的实现就变得非常简单了,我们只需要修改GameView类的代码在其中加入相应的代码即可,修改后的代码如下:
public class GameView extends View {
private float[] pts;
public GameView(Context context) {
super(context);
pts = new float[20];
for (int i = 0; i < 20; i++)
pts[i] = (float) (i + 10 + Math.random() * 40);// 产生10到40之间的随机数
}
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GRAY);
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);// 设置取消锯齿
mPaint.setStyle(Paint.Style.STROKE);
// 把mPaint.setStyle();的参数改为Paint.Style.FILL,会画出实心图形
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.BLUE);
// 绘制像素
canvas.drawPoint(5, 7, mPaint);
canvas.drawPoints(pts, mPaint);// 绘制坐标在(10,40)之间的像素20个。
canvas.drawPoints(pts, 2, 14, mPaint);
mPaint.setColor(Color.RED);
// 画直线
canvas.drawLine(27, 43, 99, 43, mPaint);
canvas.drawLines(pts, mPaint);
canvas.drawLines(pts, 3, 8, mPaint);
// 画圆
canvas.drawCircle(50, 80, 20, mPaint);
// 画弧
canvas.drawArc(new RectF(20, 100, 70, 150), 34.0f, 99.0f, false, mPaint);
canvas.drawArc(new RectF(20, 100, 70, 150), 34.0f, 99.0f, true, mPaint);
canvas.drawArc(new RectF(20, 100, 70, 150), 34.0f, 400.0f, false,
mPaint);
// 画多边形
Path path1 = new Path();
path1.moveTo(150 + 5, 130 + 80 - 50); // 设置多边形的点
path1.lineTo(150 + 45, 130 + 80 - 50);
path1.lineTo(150 + 30, 130 + 120 - 50);
path1.lineTo(150 + 20, 130 + 120 - 50);
path1.close();// 使这些点构成封闭的多边形
canvas.drawPath(path1, mPaint); // 绘制这个多边形
// 绘制矩形
mPaint.setColor(Color.YELLOW);
canvas.drawRect(130, 30, 170, 70, mPaint);
}
}
本示例中用到了产生随机数的知识,其中产生M到N之间的随机数(M <N) 的公式为:
M+Math.random()*(N-M)。
在上述代码中主要用到了Paint类和Canvas类的一些常用方法,下面分别加以说明。Paint类的常用方法有:
Ø public void setAntiAlias(boolean aa)
设置画笔的锯齿效果,true表示无锯齿。
Ø public void setColor(int color)
设置画笔的颜色。
Ø public void setStyle(Paint.Style style)
设置画笔风格,取值有:Paint.Style.FILL、Paint.Style.FILL_AND_STROKE及Paint.Style.STROKE。
Ø public void setStrokeWidth(float width)
设置空心的边框宽度。
使用Canvas类的方法可以绘制基本的图形,它的常用方法包含了绘制像素点、直线、圆和弧线的功能。
1、绘制像素点:
Ø public void drawPoint(float x, float y, Paint paint)
Ø public void drawPoints(float[] pts, int offset, int count, Paint paint)
Ø public void drawPoints(float[] pts, Paint paint)
x,y:指像素点的坐标。
float[] pts:一次绘制的多个像素点的坐标,该数组必须得是偶数个,两个一组为一个像素点的坐标。
offset:偏移量,用来指定取得数组中连续元素的第一个元素的位置。因为drawPoints()方法可以取pts数组中的一部分连续元素作为像素点的坐标。
count:要获得的数组元素的个数,必须为偶数。
2、画直线
Ø public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
Ø public void drawLines(float[] pts, Paint paint)
Ø public void drawLines(float[] pts, int offset, int count, Paint paint)
startX、startY:直线开始点的坐标。
stopX、stopY:直线结束点的坐标。
pts:画多条直线时端点坐标的集合,其中每四个数组元素为一组,表示一条直线
count:要获得的数组元素的个数,这个数必须得是4的整数倍。
3、画圆
Ø public void drawCircle(float cx, float cy, float radius, Paint paint)
cx、cy:圆心坐标。
radius:半径。
4、绘制弧
Ø public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
oval:弧的外切矩形坐标。
startAngle:弧的起始角度。
sweepAngle:弧的结束角度,如果sweepAngle - startAngle >360,则画出来的圆或椭圆。
useCenter:指定是否把起始的半径给画上,若为false则只画弧而不画起始的半径。
示例1.3
在屏幕上显示一个矩形,当我们按键盘方向键时控制其中的正方形进行移动。
(1)以示例1.1为基础,修改GameView的代码如下:
class GameView extends View {
int y = 10;
int x = 10;
public GameView2(Context context) {
super(context);
}
public void onDraw(Canvas canvas) {
// 绘图
Paint mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.YELLOW);
// 画圆,用来作为参照
canvas.drawCircle(50, 80, 20, mPaint);
// 绘制矩形--后面我们将详细讲解
canvas.drawRect(x, y, x + 80, y + 40, mPaint);
}
}
(2)修改DrawActivity类的onKeyDown方法,代码如下:
public boolean onKeyDown(int keyCode, KeyEvent event) {// 按键按下事件
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:// 上方向键
mGameView.y -= 3;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:// 下方向键
mGameView.y += 3;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
mGameView.x -= 3;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
mGameView.x += 3;
break;
}
return true;
}
运行程序,当我们按方向键时,会发现矩形发生移动,而圆形位置保持不变,如图1.1.3所示。
1.1.2 绘制文本
除了可以在屏幕上绘制简单的形状图形外,我们还可以在图形中绘制文本。在Android中我们不仅可以中规中矩地绘制文本,还可以按照指定的路径绘制文本。这在开发中是很有用的,比如游戏中人物上面的提示文字等。
示例1.4
演示Android中文本的绘制,包括文本的简单绘制及沿着指定路径绘制。程序最终运行效果如下图1.1.4所示:
本案例需要绘制的图形可以分为两种:在某个位置绘制文本以及沿某个路径绘制文本。
绘制文本的基本方法是drawText(),语法
canvas.drawText(String s,Point);
沿路径绘制需要两个步骤,首先要绘制路径,其次绘制文本,并在文本和路径之间建立关系。
canvas.drawTextOnPath(String ,Path );
Path参数
Path对象及其构建。
修改GameView类的代码如下:
class GameView extends View {
private final String showString = "好好学习,天天向上!";
private Path[] paths = new Path[3];
private Paint paint;
public GameView3(Context context) {
super(context);
paths[0] = new Path();
paths[0].moveTo(0, 0);
for (int i = 1; i <= 7; i++) {
// 生成7个点,随机生成它们的Y座标。并将它们连成一条Path
paths[0].lineTo(i * 33, (float) Math.random() * 33);
}
paths[1] = new Path();
RectF rectF = new RectF(0, 0, 200, 120);
paths[1].addOval(rectF, Path.Direction.CCW);
paths[2] = new Path();
paths[2].addArc(rectF, 60, 180);
// 初始化画笔
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStrokeWidth(2);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
canvas.translate(40, 40);
// 设置从右边开始绘制(右对齐)
paint.setTextAlign(Paint.Align.RIGHT);
paint.setTextSize(20);
// 绘制路径
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(paths[0], paint);
// 沿着路径绘制一段文本。
paint.setStyle(Paint.Style.FILL);
canvas.drawTextOnPath(showString, paths[0], -8, 20, paint);
// 画布下移120
canvas.translate(0, 60);
// 绘制路径
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(paths[1], paint);
// 沿着路径绘制一段文本。
paint.setStyle(Paint.Style.FILL);
canvas.drawTextOnPath(showString, paths[1], -20, 20, paint);
// 画布下移120
canvas.translate(0, 120);
// 绘制路径
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(paths[2], paint);
// 沿着路径绘制一段文本。
paint.setStyle(Paint.Style.FILL);
canvas.drawTextOnPath(showString, paths[2], -10, 30, paint);
canvas.drawText(showString, 174, 169, paint);
}
}
上面程序中用到了名为Path的类,这个类可以预先在View上将N个点连成一条路径,然后我们就可以调用Canvas类的drawPath()方法沿着指定的路径绘制图形了。上面代码中绘制文本应用到了下面方法:
public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
text:要绘制的文本。
path:绘制文本时要使用的路径对象。
hOffset:绘制文本时相对于路径水平方向的偏移量。
vOffset:绘制文本时相对于路径垂直方向的偏移量。
paint:绘制文本的画笔。
1.1.3 双缓冲技术
本节要模拟实现一个画图程序,即当用户在触摸屏上移动时,在屏幕上绘制任意的图形。具体的实现思路是:借助Android的Path类,使用Canvas的drawLine()方法画直线。其中每条直线都是从上一次拖动事件发生点画到本次拖动事件发生点。当用户在屏幕上移动时,两次拖动事件发后点的距离很小,多条极短的直线连接起来,肉眼看起来就是整条直线了。但如果程序每次都只是从上次拖动事件的发生点绘一条直线到本次拖动事件的发生点,那么用户前面绘制的就会丢失。为了保留用户之前绘制的内容,程序需要借助于下面讲到的“双缓冲”技术。
所谓的双缓冲技术其实很简单,就是当程序需要在指定的View上进行绘图时,程序并不直接绘制到该View组件上,而是先绘制到一个内存中的Bitmap上,等到内存中的Bitmap绘制好后,再一次性地将Bitmap绘制到View组件上。
示例1.5
采用双缓冲技术模拟实现画图程序,要求能够在屏幕上绘制任意图形。程序运行结果如下图1.1.5所示:
(1)以示例1.1为基础,修改GameView类的代码如下:
public class GameView4 extends View {
private float preX;
private float preY;
private Path path;
public Paint paint = null;
// 定义一个内存中的图片,该图片将作为缓冲区
Bitmap bitmap = null;
// 定义cacheBitmap上的Canvas对象
Canvas canvas = null;
public GameView4(Context context) {
super(context);
// 创建一个与该View相同大小的缓存区
bitmap = Bitmap.createBitmap(320, 480, Config.ARGB_8888);
canvas = new Canvas();
path = new Path();
// 设置cacheCanvas将会绘制到内存中的cacheBitmap上
canvas.setBitmap(bitmap);
// 设置画笔的颜色
paint = new Paint();
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:
path.moveTo(x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_MOVE:
path.quadTo(preX, preY, x, y);
preX = x;
preY = y;
break;
case MotionEvent.ACTION_UP:
canvas.drawPath(path, paint); -------------①
path.reset();
break;
}
invalidate();
return true;// 返回true表明处理方法已经处理该事件
}
@Override
public void onDraw(Canvas canvas) {
Paint bmpPaint = new Paint();
// 将cacheBitmap绘制到该View组件上
canvas.drawBitmap(bitmap, 0, 0, bmpPaint); ------------②
// 沿着path绘制
canvas.drawPath(path, paint);
}
}
在触摸事件中我们只是简单地修改了currentX、currentY两个属性的值 ,并通知组件重绘。注意①处的代码并不是调用该View的Canvas进行绘制,而是调用了缓存Bitmap的Canvas进行绘制,这是向缓冲绘图。②才是将缓冲中的Bitmap对象绘制到View组件上。
再来看一下,主布局文件的代码:
public class HandDraw extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new GameView4(this));
}
}
代码比较简单,仅仅是把我们自定义的View设置为Activity显示的内容。
1.2 图像的绘制及简单处理
Canvas不仅可以绘制简单图形,还可以将复杂的图像绘制到View上,比如图1.1.1中愤怒的小鸟中的小鸟、小草等都可以利用本节中讲解的绘制图片的方法进行实现。而且利用这种方法可以更容易地创造出来让人赏心悦目的软件界面,给用户带来愉悦的体验。
1.2.1 绘制图像
可以通过Canvas类的drawBirmap()来显示位图,也可以借助于BitmapDrawable将Bitmap绘制到Canvas中或者显示到View中。其中使用Bitmap方式绘制图像需要装载图像资源,并获得图像资源的InputStream对象,然后使用BitmapFactory.decodeStream()方法将InputStream解码成Bitmap对象,最后使用Canvas.drawBitmap()方法在View上绘制位图。下面我们通过一个综合性的示例进行演示。
示例1.6
演示在屏幕上绘制图像的方法及技巧。本示例程序中用于显示绘制图像的类的代码如下:
public class DrawImg extends View {
private Bitmap bitmap1 = null;
private Bitmap bitmap2 = null;
private Bitmap bitmap3 = null;
private Bitmap bitmap4 = null;
private Drawable drawable = null;
public DrawImg(Context context) {
super(context);
setBackgroundColor(Color.GRAY);
//装载图像资源,获得图像资源的InputStream形式的对象
InputStream is = getResources().openRawResource(R.drawable.wx);
Options options = new Options();
bitmap1 = BitmapFactory.decodeStream(is, null, options);//将InputStream解码为Bitmap对象
is = getResources().openRawResource(R.drawable.psu);
bitmap2 = BitmapFactory.decodeStream(is);
int w = bitmap2.getWidth();
int h = bitmap2.getHeight();
int[] pixels = new int[w * h];
bitmap2.getPixels(pixels, 0, w, 0, 0, w, h);// 复制bitmap2所有像素的颜色值
bitmap3 = Bitmap.createBitmap(pixels, 0, w, w, h, Bitmap.Config.ARGB_8888);
bitmap4 = Bitmap.createBitmap(pixels, 0, w, w, h, Bitmap.Config.ARGB_4444);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap1, 120, 10, null); //将图像绘制到屏幕上
canvas.drawBitmap(bitmap2, 18, 150, null);
canvas.drawBitmap(bitmap3, 118, 150, null);
canvas.drawBitmap(bitmap4, 218, 150, null);
//装载图像资源,并获得图像资源的Drawable对象
drawable = getResources().getDrawable(R.drawable.zwx);
drawable.setBounds(40, 273, 142,347);
drawable.draw(canvas); //使用Drawable的draw()方法绘制位图
Bitmap bitmap = ((BitmapDrawable) getResources().getDrawable(R.drawable.mm)).getBitmap();
canvas.drawBitmap(bitmap, 224,283, null);
}
}
代码中用到的方法如下:
Ø Resources View.getResources()
返回和当前视图所关联的资源。
Ø InputStream Resources.openRawResource(int id)
通过读取指定的未加工的资源打开一个数据流。
Ø Drawable Resources.getDrawable(int id)
返回一个和指定id相关联的Drawable对象。
Ø void Drawable.setBounds(int left, int top, int right, int bottom)
为Drawable对象设定边界(它会对图像进行缩放变形,但不会裁剪图像)。
Ø void Drawable.draw(Canvas canvas)
将Drawable对象画在canvas/屏幕上。
程序运行效果如下图1.1.6所示。
1.2.2 改变图像透明度
Android系统支持的颜色由RGB三原色(红、绿、蓝)再加上一个Alpha四个值组成。这四个值都在0~255之间,颜色值越小,表示该颜色越淡,颜色值越大,表示颜色越深。如果RGB都为0,就是黑色,如果RGB都为255,就是白色。Alpha值也在0~255之间变化,值越小,颜色越透明,Alpha值越大,颜色越不透明,当Alpha值为0时,颜色将因完全透明而从屏幕上消失。设置颜色的透明度可以通过Paint类的setAlpha()方法完成。
示例1.7
实现改变指定图片透明度的功能。本示例某一时刻程序最终运行效果如下图1.1.7所示。
本示例中用于显示绘制图像的类的代码如下:
public class DrawImg extends View {
private Bitmap bitmap = null;
private int alpha = 12;
public DrawImg(Context context ) {
super(context);
setBackgroundColor(Color.GRAY);
InputStream is = getResources().openRawResource(R.drawable.wx);
bitmap = BitmapFactory.decodeStream(is);
}
public void setAlpha(int alpha) {
this.alpha =alpha;
}
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setAlpha(alpha);
canvas.drawBitmap(bitmap, 120,10, paint);
}
}
Activity类的代码如下:
public class DrawActivity extends Activity {
private static final int REFRESH = 0x000001;
private DrawImg drawImg = null;
private int alpha = 15;
private SeekBar seekBar = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
drawImg = new DrawImg(this);
drawImg.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 120));
layout.addView(drawImg);
SeekBar seekBar = new SeekBar(this);
seekBar.setMax(255);
seekBar.setProgress(alpha);
layout.addView(seekBar);
setContentView(layout);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
alpha = progress;
drawImg.setAlpha(alpha);
drawImg.invalidate();
}
});
// new Thread(new GameThread()).start();// 开启线程
}
}
1.2.3 旋转图像
在愤怒的小鸟中,当小鸟和别的物体发生碰撞的时候,会出现图像旋转的动画效果。在实际的游戏开发中,图像旋转是一个经常用到的功能,本小节中我们就学习和图像旋转有关的操作。要实现图像的旋转需要用到矩阵Matrix类的相关知识,要实现图像的不断旋转,需要在onDraw()方法中调用invalidate()方法。每调用一次invalidate方法,onDraw()方法就会调用一次,当在onDraw()方法中调用invalidate()方法时,就意味着onDraw()方法会不断地被调用。
示例1.8:实现旋转指定图片的功能(让图片绕着某一方向不停地旋转),某一时刻程序的运行效果如图1.1.8所示。
本示例中用于显示绘制图像的类的代码如下:
public class DrawImg extends View {
private Bitmap bitmap = null;
private int degree = 0 ;
public DrawImg(Context context) {
super(context);
setBackgroundColor(Color.GRAY);
InputStream is = getResources().openRawResource(R.drawable.wx);
bitmap = BitmapFactory.decodeStream(is);
}
@Override
protected void onDraw(Canvas canvas) {
if(degree <0)
degree = 360;
if(degree > 360 )
degree = 0;
Matrix matrix = new Matrix();
matrix.setRotate(degree++ , 160, 144);
canvas.setMatrix(matrix);
canvas.drawBitmap(bitmap, 120,88, null);
invalidate();
}
}
另外对于图像的变形还有扭曲和拉伸等。
任务实训部分
1:在Android中绘制基本图形
训练技能点
Ø Android中基本图形的绘制
Ø 熟悉Paint类、Canvas类的常用方法
需求说明
练习Android中如何绘制基本图形,要求最终绘制结果如下图1.2.1所示
实现步骤
(1) 搭建程序框架
(2) 自定义View类,并在其中实现所要求图形的绘制
具体实现请参看1.1节的内容。
2:完善1.1.3节的画图程序
训练技能点
双缓冲技术
需求说明
为1.1.3节中我们模拟实现的画图程序添加菜单选择功能,要求至少添加两个菜单项,一个用来设置画笔的颜色,一个用来设置画笔的宽度,并实现这些功能。
巩固练习
一、选择题
1. 我们一般是在( )实现屏幕的刷新。
A. 在onDraw方法之后调用invalidate()方法
B. 在onDraw方法之前调用invalidate()方法
C. 调用postInvalidate()方法之后
D. 调用postInvalidate()方法之前
2. 下列有关双缓冲技术的说法正确的是( )
A. 双缓冲技术只能应用在Android中
B. 双缓冲技术的效率比较高
C. 双缓冲技术能够避免闪屏
D. 双缓冲技术其实就是把要显示的东西缓存起来绘制好之后才进行显示
二、上机练习
1、在手机屏幕上利用基本图形绘制出一个机器人。
2、为第一题中的机器人添加走动的功能(选做)。
扩展进阶
使用SurfaceView类搭建框架
对于View的onDraw()方法,不能把容易阻塞的处理移动到后台线程中,因为从后台线程修改一个GUI元素是被显式地禁止的。
当需要快速地更新View的UI,或者当渲染代码阻塞GUI线程的时间过长的时候,SurfaceView就派上用场了。SurfaceView封装了一个Surface对象(View及其子类都是画在Surface上的)。这一点很重要,因为Surface可以使用后台线程绘制图形。对于那些资源敏感的操作,或者那些要求快速更新或者高速帧率的地方,例如,使用3D图形、创建游戏或者实时预览摄像头,这一点特别有用。
独立于GUI线程进行绘图的代价是额外的内存消耗,所以,虽然它是创建定制的View的有效方式,有时甚至是必须的,但是使用SurfaceView的时候仍然要保持谨慎。SurfaceView的使用方式与任何View所派生的类是完全相同的。可以像其他View那样应用动画,并把它们放到布局中。SurfaceView类的事件处理和View一样。
SurfaceView封装的Surface支持使用所有标准Canvas方法进行绘图,同时也完全支持的OpenGL ES库。SurfaceView和View的明显不同在于Surface不需要通过线程来更新视图,但在绘制之前必须使用lockCanvas方法锁定画布,并得到画布,然后绘制,完成后用unlockCanvasAndPost方法解锁画布。
Ø Canvas android.view.SurfaceHolder.lockCanvas()
锁定画布,得到canvas。
Ø void android.view.SurfaceHolder.unlockCanvasAndPost(Canvas canvas)
绘制后解锁,绘制后必须解锁才能显示。
另外,值得强调的是对被绘制的画面进行裁剪、控制大小等需要使用SurfaceHolder来完成。
使用SurfaceView时,可以通过SurfaceHolder.Callback接口来对其进行创建、销毁,还有当情况改变时进行监视。SurfaceHolder.Callback接口提供了以下三个方法:
Ø public void surfaceCreated(SurfaceHolder holder)
在surface创建时触发
Ø public void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
在surface的大小发生改变时触发
Ø public void surfaceDestroyed(SurfaceHolder holder)
在surface销毁时触发
下面示例使用SurfaceView搭建绘图框架。在画布上绘制一个小球,然后为应用添加事件控制功能,要求可以通过方向键控制小球的移动。
首先,编写一个用来绘制界面的View类,代码如下:
class GameSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
boolean mbLoop = false;// 控制循环
SurfaceHolder mSurfaceHolder = null;// 定义SurfaceHolder对象
int miCount = 0;
int y = 50;
public GameSurfaceView(Context context) {
super(context);
mSurfaceHolder = this.getHolder();// 实例化SurfaceHolder
mSurfaceHolder.addCallback(this);// 添加回调
this.setFocusable(true);
mbLoop = true;
}
// 在surface的大小发生改变时触发
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{ }
// 在surface创建时触发
@Override
public void surfaceCreated(SurfaceHolder holder) {
new Thread(this).start();// 开启绘图线程
}
// 在surface销毁时触发
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mbLoop = false; // 停止循环
}
// 绘图循环
public void run() {
while (mbLoop) {
try {
Thread.sleep(200);
} catch (Exception e) {
}
synchronized (mSurfaceHolder) {
Draw();
}
}
}
// 绘图方法
public void Draw() {
Canvas canvas = mSurfaceHolder.lockCanvas();// 锁定画布,得到canvas
if (mSurfaceHolder == null || canvas == null) {
return;
}
if (miCount < 100) {
miCount++;
} else {
miCount = 0;
}
Paint mPaint = new Paint();// 绘图
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
canvas.drawRect(0, 0, 320, 480, mPaint); // 绘制矩形--清屏作用
switch (miCount % 4) {
case 0:
mPaint.setColor(Color.BLUE);
break;
case 1:
mPaint.setColor(Color.GREEN);
break;
case 2:
mPaint.setColor(Color.RED);
break;
case 3:
mPaint.setColor(Color.YELLOW);
break;
default:
mPaint.setColor(Color.WHITE);
break;
}
canvas.drawCircle((320 - 25) / 2, y, 50, mPaint);// 绘制矩形
mSurfaceHolder.unlockCanvasAndPost(canvas);// 绘制后解锁,绘制后必须解锁才能显示
}
}
然后编写一个用来控制整个应用(界面元素动作)的类:
public class SurfaceViewFrameActivity extends Activity {
GameSurfaceView mGameSurfaceView;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mGameSurfaceView = new GameSurfaceView(this);//创建GameSurfaceView对象
setContentView(mGameSurfaceView);//设置显示GameSurfaceView视图
}
//触笔事件
public boolean onTouchEvent(MotionEvent event)
{
return true;
}
//按键按下事件
public boolean onKeyDown(int keyCode, KeyEvent event)
{
return true;
}
//按键弹起事件
public boolean onKeyUp(int keyCode, KeyEvent event)
{
switch (keyCode)
{
case KeyEvent.KEYCODE_DPAD_UP://上方向键
mGameSurfaceView.y-=3;
break;
case KeyEvent.KEYCODE_DPAD_DOWN://下方向键
mGameSurfaceView.y+=3;
break;
}
return false;
}
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)
{
return true;
}
}
程序运行效果如下图1.3.1所示:
文章来源: aaaedu.blog.csdn.net,作者:tea_year,版权归原作者所有,如需转载,请联系作者。
原文链接:aaaedu.blog.csdn.net/article/details/71436449
- 点赞
- 收藏
- 关注作者
评论(0)