1 使用简单图片
2 绘图
3 图形特效处理
4 逐帧(Frame)动画
5 补间(Tween)动画
6 属性动画
7 使用SurfaceView实现动画
1、Bitmap和BitmapFactory
Bitmap代表一张位图,BitmapDrawable里封装的图片就是一个Bitmap对象。开发者为了把一个Bitmap对象包装成
BitmapDrawable对象。可以调用BitmapDrawable的构造器:
// 把一个Bitmap对象包装成BitmapDrawable对象 BitmapDrawable drawable = new BitmapDrawable(bitmap);
如果需要获取BitmapDrawable所包装的Bitmap对象,则可调用BitmapDrawable的getBitmap(),如下代码所示:
// 获取一个BitmapDrawable所包装的Bitmap对象。 Bitmap bitmap = drawable.getBitmap();
除此之外,Bitmap还提供了一些静态方法来创建新的Bitmap对象,例如如下常用方法。
● createBitmap(Bitmap source, int x, int y, int width, int height):
从源位图source的指定坐标点(给定x、y)开始,从中"挖取"宽width、高height的一块出来,创建新的Bitmap对象。
Android为Bitmap提供了两个方法来判断它是否已回收,以及强制Bitmap回收自己。
● boolean isRecycled():返回该Bitmap对象是否已被回收。
● void recycle():强制一个Bitmap对象立即回收自己。
除此之外,如果Android应用需要访问其他缓存路径(比如SD卡中)里的图片,都需要借助于BitmapFactory来解析、创建Bitmap对象。
范例:查看/assets/目录下图片的图片查看器。
import java.io.IOException; import java.io.InputStream; import android.app.Activity; import android.content.res.AssetManager; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; public class BitmapTest extends Activity { String[] images = null; AssetManager assets = null; int currentImg = 0; ImageView image; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); image = (ImageView) findViewById(R.id.image); try { assets = getAssets(); // 获取/assets/目录下所有文件 images = assets.list(""); System.out.println(images.length); } catch (IOException e) { e.printStackTrace(); } // 获取bn按钮 final Button next = (Button) findViewById(R.id.next); // 为bn按钮绑定事件监听器,该监听器将会查看下一张图片 next.setOnClickListener(new OnClickListener() { @Override public void onClick(View sources) { // 如果发生数组越界 if (currentImg >= images.length) { currentImg = 0; } // 找到下一个图片文件 while (!images[currentImg].endsWith(".png") && !images[currentImg].endsWith(".jpg") && !images[currentImg].endsWith(".gif")) { currentImg++; // 如果已发生数组越界 if (currentImg >= images.length) { currentImg = 0; } } InputStream assetFile = null; try { // 打开指定资源对应的输入流 assetFile = assets.open(images[currentImg++]); } catch (IOException e) { e.printStackTrace(); } BitmapDrawable bitmapDrawable = (BitmapDrawable) image .getDrawable(); // 如果图片还未回收,先强制回收该图片 // 用于判断当前ImageView所显示的图片是否已被回收。如果图片还未回收。系统强制回收该图片。 if (bitmapDrawable != null && !bitmapDrawable.getBitmap().isRecycled()) { bitmapDrawable.getBitmap().recycle(); } // 改变ImageView显示的图片 // BitmapFactory指定输入流解析、并创建Bitmap对象。 image.setImageBitmap(BitmapFactory.decodeStream(assetFile)); } }); } }
2、Android绘图基础:Canvas、Paint等
除了使用已有的图片之外,Android应用常常需要在运行时动态地生成图片,比如一个手机游戏。游戏界面
看上去丰富多彩,而且可以随着用户动作而动态改变,这就需要借助于Android的绘图支持了。
Android绘图继承View组件,并重写它的onDraw(Canvas canvas)即可。
除了上表所定义的各种方法之外,Canvas还提供了如下方法进行坐标变换。
● rotate(float degrees, float px, float py):对Canvas执行旋转变换。
● scale(float sx, float sy, float px, float py):对Canvas执行缩放变换。
● skew(float sx, float sy):对Canvas执行倾斜变换。
● translate(float dx, float dy):
移动Canvas。向右移动dx距离(dx为负数即向左移动);向下移动dx距离(dy为负数即向上移动)。
Paint类主要用于设置绘制风格,包括画笔颜色、画笔笔触粗细、填充风格等。
在Canvas提供的绘制方法中还用到了一个API:Path,Path代表任意多条直线连接而成的任意图形,当Canvas根据Path绘制时,
它可以绘制出任意的形状。
范例:在Android应用中绘制基本的集合图形。
public class MyView extends View { public MyView(Context context, AttributeSet set) { super(context, set); } @Override // 重写该方法,进行绘图 protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 把整张画布绘制成白色 canvas.drawColor(Color.WHITE); Paint paint = new Paint(); // 去锯齿 paint.setAntiAlias(true); paint.setColor(Color.BLUE); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(3); // 绘制圆形 canvas.drawCircle(40, 40, 30, paint); // 绘制正方形 canvas.drawRect(10, 80, 70, 140, paint); // 绘制矩形 canvas.drawRect(10, 150, 70, 190, paint); RectF re1 = new RectF(10, 200, 70, 230); // 绘制圆角矩形 canvas.drawRoundRect(re1, 15, 15, paint); RectF re11 = new RectF(10, 240, 70, 270); // 绘制椭圆 canvas.drawOval(re11, paint); // 定义一个Path对象,封闭成一个三角形。 Path path1 = new Path(); path1.moveTo(10, 340); path1.lineTo(70, 340); path1.lineTo(40, 290); path1.close(); // 根据Path进行绘制,绘制三角形 canvas.drawPath(path1, paint); // 定义一个Path对象,封闭成一个五角形。 Path path2 = new Path(); path2.moveTo(26, 360); path2.lineTo(54, 360); path2.lineTo(70, 392); path2.lineTo(40, 420); path2.lineTo(10, 392); path2.close(); // 根据Path进行绘制,绘制五角形 canvas.drawPath(path2, paint); // ----------设置填充风格后绘制---------- paint.setStyle(Paint.Style.FILL); paint.setColor(Color.RED); canvas.drawCircle(120, 40, 30, paint); // 绘制正方形 canvas.drawRect(90, 80, 150, 140, paint); // 绘制矩形 canvas.drawRect(90, 150, 150, 190, paint); RectF re2 = new RectF(90, 200, 150, 230); // 绘制圆角矩形 canvas.drawRoundRect(re2, 15, 15, paint); RectF re21 = new RectF(90, 240, 150, 270); // 绘制椭圆 canvas.drawOval(re21, paint); Path path3 = new Path(); path3.moveTo(90, 340); path3.lineTo(150, 340); path3.lineTo(120, 290); path3.close(); // 绘制三角形 canvas.drawPath(path3, paint); Path path4 = new Path(); path4.moveTo(106, 360); path4.lineTo(134, 360); path4.lineTo(150, 392); path4.lineTo(120, 420); path4.lineTo(90, 392); path4.close(); // 绘制五角形 canvas.drawPath(path4, paint); // ----------设置渐变器后绘制---------- // 为Paint设置渐变器 Shader mShader = new LinearGradient(0, 0, 40, 60, new int[] { Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW }, null, Shader.TileMode.REPEAT); paint.setShader(mShader); // 设置阴影 paint.setShadowLayer(45, 10, 10, Color.GRAY); // 绘制圆形 canvas.drawCircle(200, 40, 30, paint); // 绘制正方形 canvas.drawRect(170, 80, 230, 140, paint); // 绘制矩形 canvas.drawRect(170, 150, 230, 190, paint); RectF re3 = new RectF(170, 200, 230, 230); // 绘制圆角矩形 canvas.drawRoundRect(re3, 15, 15, paint); RectF re31 = new RectF(170, 240, 230, 270); // 绘制椭圆 canvas.drawOval(re31, paint); Path path5 = new Path(); path5.moveTo(170, 340); path5.lineTo(230, 340); path5.lineTo(200, 290); path5.close(); // 根据Path进行绘制,绘制三角形 canvas.drawPath(path5, paint); Path path6 = new Path(); path6.moveTo(186, 360); path6.lineTo(214, 360); path6.lineTo(230, 392); path6.lineTo(200, 420); path6.lineTo(170, 392); path6.close(); // 根据Path进行绘制,绘制五角形 canvas.drawPath(path6, paint); // ----------设置字符大小后绘制---------- paint.setTextSize(24); paint.setShader(null); // 绘制7个字符串 // getResources().getString(R.string.circle) canvas.drawText("圆形", 240, 50, paint); canvas.drawText("正方形", 240, 120, paint); canvas.drawText("长方形", 240, 175, paint); canvas.drawText("圆角矩形", 230, 220, paint); canvas.drawText("椭圆形", 240, 260, paint); canvas.drawText("三角形", 240, 325, paint); canvas.drawText("五角形", 240, 390, paint); } }
Android的Canvas不仅可以绘制这种简单的几何图形,还可以直接将一个Bitmap绘制到画布上,这样就给了开发者巨大的灵活性,
只要前期美工把应用程序所需的图片制作出来,后期开发时只要把这些图片绘制到Canvas上即可。
3、Path类
它可以预先在View上将N个点连成一条“路径”,然后调用Canvas的drawPath(path, paint)即可沿着路径绘制图形。
实际上Android还为路径绘制提供了PathEffect来定义绘制效果,PathEffect包含了如下子类(每个子类代表一种绘制效果):
● ComposePathEffect
● ComerPathEffect
● DashPathEffect
● DiscretePathEffect
● PathDashPathEffect
● SumPathEffect
范例:绘制7条曲线路径
public class PathTest extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new MyView(this)); } class MyView extends View { float phase; PathEffect[] effects = new PathEffect[7]; int[] colors; private Paint paint; Path path; public MyView(Context context) { super(context); paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(4); // 创建、并初始化Path path = new Path(); path.moveTo(0, 0); for (int i = 1; i <= 15; i++) { // 生成15个点,随机生成它们的Y座标。并将它们连成一条Path path.lineTo(i * 20, (float) Math.random() * 60); } // 初始化7个颜色 colors = new int[] { Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.RED, Color.YELLOW }; // -----------下面开始初始化7中路径效果---------- // 不使用路径效果。 effects[0] = null; // 使用CornerPathEffect路径效果 effects[1] = new CornerPathEffect(10); // 初始化DiscretePathEffect effects[2] = new DiscretePathEffect(3.0f, 5.0f); } @Override protected void onDraw(Canvas canvas) { // 将背景填充成白色 canvas.drawColor(Color.WHITE); // 初始化DashPathEffect,DashPathEffect有动画效果 effects[3] = new DashPathEffect(new float[] { 20, 10, 5, 10 }, phase); // 初始化PathDashPathEffect,PathDashPathEffect有动画效果 Path p = new Path(); p.addRect(0, 0, 8, 8, Path.Direction.CCW); effects[4] = new PathDashPathEffect(p, 12, phase, PathDashPathEffect.Style.ROTATE); // 初始化PathDashPathEffect effects[5] = new ComposePathEffect(effects[2], effects[4]); effects[6] = new SumPathEffect(effects[4], effects[3]); // 对Canvas执行坐标变换:将画布“整体位移”到8、8处开始绘制 canvas.translate(8, 8); // 依次使用7中不同路径效果、7种不同的颜色来绘制路径 for (int i = 0; i < effects.length; i++) { paint.setPathEffect(effects[i]); paint.setColor(colors[i]); canvas.drawPath(path, paint); canvas.translate(0, 60); } // 改变phase值,形成动画效果 phase += 1; invalidate(); } } }
当定义DashPathEffect、PathDashPath Effect时可指定一个phase参数,该参数用于指定路径效果的位置,
当该phase参数改变时,绘制效果也略有变化。上面的程序不停地改变phase参数,并不停地重绘该View组件,
这将看到产生动画效果。
除此之外,Android的Canvas还提供了一个drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint),
该方法可以沿着Path绘制文本。其中hOffset参数指定水平偏移、vOffset参数指定垂直偏移。
public class PathText extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new TextView(this)); } class TextView extends View { final String DRAW_STR = "疯狂Java讲义"; Path[] paths = new Path[3]; Paint paint; public TextView(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 * 30, (float) Math.random() * 30); } 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.CYAN); paint.setStrokeWidth(1); } @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(DRAW_STR, paths[0], -8, 20, paint); // 对Canvas进行坐标变换:画布下移120 canvas.translate(0, 60); // 绘制路径 paint.setStyle(Paint.Style.STROKE); canvas.drawPath(paths[1], paint); // 沿着路径绘制一段文本。 paint.setStyle(Paint.Style.FILL); canvas.drawTextOnPath(DRAW_STR, paths[1], -20, 20, paint); // 对Canvas进行坐标变换: 画布下移120 canvas.translate(0, 120); // 绘制路径 paint.setStyle(Paint.Style.STROKE); canvas.drawPath(paths[2], paint); // 沿着路径绘制一段文本。 paint.setStyle(Paint.Style.FILL); canvas.drawTextOnPath(DRAW_STR, paths[2], -10, 20, paint); } } }
上面的程序三次调用了drawTextOnPath在View组件上绘制文本,此时绘制的文本并不是简单地水平排列,而是沿着指定路径绘制的。
4、绘制游戏动画
范例:采用双缓冲实现画图板
当用户在触摸屏幕上移动时,即可在屏幕上绘制任意的图形。实现手绘功能其实是一种假象;表面上看起来可以随用户在触摸屏上
自由地画曲线。实际上依然利用的是Canvas的drawLine方法画直线,每条直线都是从上一次拖动事件发生点画到本次拖动事件发生点。
当用户在触摸屏上移动时,两次拖动事件发生点的距离很小,多条极短的直线连接起来,肉看看起来就是直线了。借助与Android提供
的Path类,可以非常方便地实现这种效果。
需要指出的是,如果程序每次都是从上次拖动事件的发生点绘一条直线到本次拖动事件的发生点,那么用户前面绘制的就会丢失。
为了保留用户之前绘制的内容,程序要借助于“双缓冲”技术。
所谓双缓冲技术其实很简单,当程序需要在指定View上进行绘制时,程序并不直接绘制到该View组件上,而是先绘制到一个内存中
的Bitmap图片(这就是缓冲)上,等到内存中的Bitmap绘制好之后,再一次性地将Bitmap绘制到View组件上。
// 自定义一个View类
public class DrawView extends View { float preX; float preY; private Path path; public Paint paint = null; final int VIEW_WIDTH = 320; final int VIEW_HEIGHT = 480; // 定义一个内存中的图片,该图片将作为缓冲区 Bitmap cacheBitmap = null; // 定义cacheBitmap上的Canvas对象 Canvas cacheCanvas = null; public DrawView(Context context, AttributeSet set) { super(context, set); // 创建一个与该View相同大小的缓存区 cacheBitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Config.ARGB_8888); cacheCanvas = new Canvas(); path = new Path(); // 设置cacheCanvas将会绘制到内存中的cacheBitmap上 cacheCanvas.setBitmap(cacheBitmap); // 设置画笔的颜色 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: 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: cacheCanvas.drawPath(path, paint); // ① path.reset(); break; } invalidate(); // 返回true表明处理方法已经处理该事件 return true; } @Override public void onDraw(Canvas canvas) { Paint bmpPaint = new Paint(); // 将cacheBitmap绘制到该View组件上 canvas.drawBitmap(cacheBitmap, 0, 0, bmpPaint); // ② // 沿着path绘制 canvas.drawPath(path, paint); } }
在这个自定义View组件中,程序重写了该View的onDraw(Canvas canvas),注意该方法中①,这行代码并不是调用
该View的Canvas进行绘制,而是调用了缓存Bitmap的Canvas进行绘图,这表明是向缓冲绘图。程序②代码将缓冲中的
BitMap对象绘制到View组件上------这就是所谓的“双缓冲”技术。
加载到布局当中。 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/white" android:orientation="vertical" > <org.crazyit.image.DrawView android:id="@+id/draw" android:layout_width="480dp" android:layout_height="480dp" /> </LinearLayout>
程序还提供了菜单来设置画笔的颜色和笔触大小。 <?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> <!-- 定义3个菜单项 --> <item android:id="@+id/width_1" android:title="1像素"/> <item android:id="@+id/width_3" android:title="3像素"/> <item android:id="@+id/width_5" android:title="53像素"/> </group> </menu> </item> <item android:id="@+id/blur" android:title="模糊效果"/> <item android:id="@+id/emboss" android:title="浮雕效果"/> </menu>
主程序负责加载、显示界面布局,加载、显示上面的菜单资源,除此之外,程序还要为各菜单项编写事件响应。
public class HandDraw extends Activity { EmbossMaskFilter emboss; BlurMaskFilter blur; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); emboss = new EmbossMaskFilter(new float[] { 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 inflator = new MenuInflater(this); // 装载R.menu.my_menu对应的菜单,并添加到menu中 inflator.inflate(R.menu.my_menu, menu); return super.onCreateOptionsMenu(menu); } @Override // 菜单项被单击后的回调方法 public boolean onOptionsItemSelected(MenuItem mi) { DrawView dv = (DrawView) findViewById(R.id.draw); // 判断单击的是哪个菜单项,并针对性的作出响应。 switch (mi.getItemId()) { case R.id.red: dv.paint.setColor(Color.RED); mi.setChecked(true); break; case R.id.green: dv.paint.setColor(Color.GREEN); mi.setChecked(true); break; case R.id.blue: dv.paint.setColor(Color.BLUE); mi.setChecked(true); break; case R.id.width_1: dv.paint.setStrokeWidth(1); break; case R.id.width_3: dv.paint.setStrokeWidth(3); break; case R.id.width_5: dv.paint.setStrokeWidth(5); break; case R.id.blur: dv.paint.setMaskFilter(blur); break; case R.id.emboss: dv.paint.setMaskFilter(emboss); break; } return true; } }
当用户单击不同菜单项之后,程序只要简单地修改DrawView组件内的Paint对象的颜色和笔触粗细即可。
5、图形特效处理 --- 使用Matrix控制变换
Matrix是Android提供的一个矩阵工具类,它本身并不能对图像或组件进行变换,但它可与其他API结合来控制
图形、组件的变换。
使用Matrix控制图像或组件变换的步骤如下:
1)获取Matrix对象,该Matrix对象既可新创建,也可直接获取其他对象内封装的Matrix(例如Transformation对象
内部就封装了Matrix)。
2)调用Matrix的方法进行平移、旋转、缩放、倾斜等。
3)将程序对Matrix所做的变换应用到指定图像或组件。
【提示】Matrix不仅可用于控制图形的平移、旋转、缩放、倾斜变换,也可控制View组件进行平移、旋转和缩放等。
Matrix提供了如下方法来控制平移、旋转和缩放:
● setTranslate(float dx, float dy):控制Matrix进行平移。
● setSkew(float kx, float ky, float px, float py):
控制Matrix以px、py为轴心进行倾斜。kx、ky为X、Y方向上的倾斜距离。
● setSkew(float kx, float ky):控制Matrix进行倾斜。kx、ky为X、Y方向上的倾斜距离。
● setRotate(float degrees):控制Matrix进行旋转,degrees控制旋转的角度。
● setRotate(float degrees, float px, float py):设置以px、py为轴心进行旋转,degrees控制旋转的角度。
● setScale(float sx, float sy):设置Matrix进行缩放,sx、sy控制X、Y方向上的缩放比例。
● setScale(float sx, float sy, float px, float py):设置Matrix以px、py为轴心进行缩放,sx、sy控制X、Y方向上的缩放比例。
一旦对Matrix进行了变换,接下来就可以应用该Matrix对图形进行控制了。例如Canvas就提供了一个
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)方法,调用该方法就可以在绘制bitmap
时应用Matrix上的变换。
范例:该自定义View可以检测到用户的键盘事件,当用户单击手机的方向键时,该自定义View会用
Matrix对绘制的图形进行旋转、倾斜变换。
public class MyView extends View { // 初始的图片资源 private Bitmap bitmap; // Matrix 实例 private Matrix matrix = new Matrix(); // 设置倾斜度 private float sx = 0.0f; // 位图宽和高 private int width, height; // 缩放比例 private float scale = 1.0f; // 判断缩放还是旋转 private boolean isScale = false; public MyView(Context context, AttributeSet set) { super(context, set); // 获得位图 bitmap = ((BitmapDrawable) context.getResources().getDrawable( R.drawable.a)).getBitmap(); // 获得位图宽 width = bitmap.getWidth(); // 获得位图高 height = bitmap.getHeight(); // 使当前视图获得焦点 this.setFocusable(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 重置Matrix matrix.reset(); if (!isScale) { // 旋转Matrix matrix.setSkew(sx, 0); } else { // 缩放Matrix matrix.setScale(scale, scale); } // 根据原始位图和Matrix创建新图片 Bitmap bitmap2 = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); // 绘制新位图 canvas.drawBitmap(bitmap2, matrix, null); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { // 向左倾斜 case KeyEvent.KEYCODE_A: isScale = false; sx += 0.1; postInvalidate(); break; // 向右倾斜 case KeyEvent.KEYCODE_D: isScale = false; sx -= 0.1; postInvalidate(); break; // 放大 case KeyEvent.KEYCODE_W: isScale = true; if (scale < 2.0) scale += 0.1; postInvalidate(); break; // 缩小 case KeyEvent.KEYCODE_S: isScale = true; if (scale > 0.5) scale -= 0.1; postInvalidate(); break; } return super.onKeyDown(keyCode, event); } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <org.crazyit.image.MyView android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
当用户单击手机的方向键时,事件处理器负责修改程序中sx(控制水平倾斜度)和scale(控制缩放比)两个参数。
6、使用drawBitmapMesh扭曲图像
范例:可揉动的图片
通过drawBitmapMesh方法来控制图片的扭曲。当用户“触摸”图片的指定点时,该图片会在这个点被用户“按”下去
-----就像这张图片铺在“极软的床上”一样。
为了实现这个效果,程序要在用户触摸图片的指定点时,动态地改变verts数组里每个元素的位置(控制扭曲后每
个顶点的坐标)-----这种改变也简单:程序计算图片上每个顶点与触摸点的距离,顶点与触摸点的距离越小,该顶点
向触摸点移动的距离越大。
public class WarpTest extends Activity { private Bitmap bitmap; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new MyView(this, R.drawable.jinta)); } private class MyView extends View { // 定义两个常量,这两个常量指定该图片横向、纵向上都被划分为20格。 private final int WIDTH = 20; private final int HEIGHT = 20; // 记录该图片上包含441个顶点 private final int COUNT = (WIDTH + 1) * (HEIGHT + 1); // 定义一个数组,保存Bitmap上的21 * 21个点的座标 private final float[] verts = new float[COUNT * 2]; // 定义一个数组,记录Bitmap上的21 * 21个点经过扭曲后的座标 // 对图片进行扭曲的关键就是修改该数组里元素的值。 private final float[] orig = new float[COUNT * 2]; public MyView(Context context, int drawableId) { super(context); setFocusable(true); // 根据指定资源加载图片 bitmap = BitmapFactory.decodeResource(getResources(), drawableId); // 获取图片宽度、高度 float bitmapWidth = bitmap.getWidth(); float bitmapHeight = bitmap.getHeight(); int index = 0; for (int y = 0; y <= HEIGHT; y++) { float fy = bitmapHeight * y / HEIGHT; for (int x = 0; x <= WIDTH; x++) { float fx = bitmapWidth * x / WIDTH; // 初始化orig、verts数组。 初始化后,orig、verts // 两个数组均匀地保存了21 * 21个点的x,y座标 orig[index * 2 + 0] = verts[index * 2 + 0] = fx; orig[index * 2 + 1] = verts[index * 2 + 1] = fy; index += 1; } } // 设置背景色 setBackgroundColor(Color.WHITE); } @Override protected void onDraw(Canvas canvas) { // 对bitmap按verts数组进行扭曲 // 从第一个点(由第5个参数0控制)开始扭曲 canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0, null, 0, null); } // 工具方法,用于根据触摸事件的位置计算verts数组里各元素的值 private void warp(float cx, float cy) { for (int i = 0; i < COUNT * 2; i += 2) { float dx = cx - orig[i + 0]; float dy = cy - orig[i + 1]; float dd = dx * dx + dy * dy; // 计算每个座标点与当前点(cx、cy)之间的距离 float d = (float) Math.sqrt(dd); // 计算扭曲度,距离当前点(cx、cy)越远,扭曲度越小 float pull = 80000 / ((float) (dd * d)); // 对verts数组(保存bitmap上21 * 21个点经过扭曲后的座标)重新赋值 if (pull >= 1) { verts[i + 0] = cx; verts[i + 1] = cy; } else { // 控制各顶点向触摸事件发生点偏移 verts[i + 0] = orig[i + 0] + dx * pull; verts[i + 1] = orig[i + 1] + dy * pull; } } // 通知View组件重绘 invalidate(); } @Override public boolean onTouchEvent(MotionEvent event) { // 调用warp方法根据触摸屏事件的座标点来扭曲verts数组 warp(event.getX(), event.getY()); return true; } } }
7、使用Shader填充图形
Shader包含一个setShader(Shader s)方法,该方法控制“画笔”的渲染效果:Android不仅可以使用颜色来填充图形
(矩形、椭圆、圆形等各种几何图形),也可以使用Shader对象指定的渲染效果来填充图形。
Shader本身是一个抽象类,它提供了如下实现类。
● BitmapShader:使用位图平铺的渲染效果。
● LinearGradient:使用线性渐变来填充图形。
● RadialGradient:使用原形渐变来填充图形。
● SweepGradient:使用角度渐变来填充图形。
● ComposeShader:使用组合渲染效果来填充图形。
范例:单击不同按钮时系统将会设置Paint使用不同的Shader。
public class MyView extends View { // 声明画笔 public Paint paint; public MyView(Context context, AttributeSet set) { super(context, set); paint = new Paint(); paint.setColor(Color.RED); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 使用指定Paint对象画矩形 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > <Button android:id="@+id/bn1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="位图" /> <Button android:id="@+id/bn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="线性渐变" /> <Button android:id="@+id/bn3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="圆形渐变" /> <Button android:id="@+id/bn4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="角度渐变" /> <Button android:id="@+id/bn5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="混合" /> </LinearLayout> <org.crazyit.image.MyView android:id="@+id/my_view" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
public class ShaderTest extends Activity implements OnClickListener { // 声明位图渲染对象 private Shader[] shaders = new Shader[5]; // 声明颜色数组 private int[] colors; MyView myView; // 自定义视图类 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); myView = (MyView) findViewById(R.id.my_view); // 获得Bitmap实例 Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.water); // 设置渐变的颜色组,也就是按红、绿、蓝的方式渐变 colors = new int[] { Color.RED, Color.GREEN, Color.BLUE }; // 实例化BitmapShader,x坐标方向重复图形,y坐标方向镜像图形 shaders[0] = new BitmapShader(bm, TileMode.REPEAT, TileMode.MIRROR); // 实例化LinearGradient shaders[1] = new LinearGradient(0, 0, 100, 100, colors, null, TileMode.REPEAT); // 实例化RadialGradient shaders[2] = new RadialGradient(100, 100, 80, colors, null, TileMode.REPEAT); // 实例化SweepGradient shaders[3] = new SweepGradient(160, 160, colors, null); // 实例化ComposeShader shaders[4] = new ComposeShader(shaders[1], shaders[2], PorterDuff.Mode.DARKEN); Button bn1 = (Button) findViewById(R.id.bn1); Button bn2 = (Button) findViewById(R.id.bn2); Button bn3 = (Button) findViewById(R.id.bn3); Button bn4 = (Button) findViewById(R.id.bn4); Button bn5 = (Button) findViewById(R.id.bn5); bn1.setOnClickListener(this); bn2.setOnClickListener(this); bn3.setOnClickListener(this); bn4.setOnClickListener(this); bn5.setOnClickListener(this); } @Override public void onClick(View source) { switch (source.getId()) { case R.id.bn1: myView.paint.setShader(shaders[0]); break; case R.id.bn2: myView.paint.setShader(shaders[1]); break; case R.id.bn3: myView.paint.setShader(shaders[2]); break; case R.id.bn4: myView.paint.setShader(shaders[3]); break; case R.id.bn5: myView.paint.setShader(shaders[4]); break; } // 重绘界面 myView.invalidate(); } }