上一篇博客说到了 Android 图像的色彩处理,使用的是 ColorMatrix 矩阵;本篇博客说 Android 图形的特效处理,使用的是 Matrix 这个类。
跟 Android 图像的色彩处理基本一样,只是将 ColorMatrix 换成了 Matrix,ColorMatrix 是 4*5 的矩阵,Matrix 是 3*3 的。每个像素点表达了其坐标的 X、Y 信息:
当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样,计算公式如下所示:
通常情况下,会让
与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵。就是对角线元素 a、e、i 为 1,其他元素为 0 的矩阵,如下图所示:
图像的变形处理通常包含以下四类基本变换:
平移变换的坐标值变换过程就是将每个像素点都进行平移变换,当从
旋转变换即指一个点围绕一个中心旋转到一个新的点。当从
x0=rcosα
y0=rsinα
x1=rcos(α+θ)=rcosαcosθ−rsinαsinθ=x0cosθ−y0sinθ
y1=rsin(α+θ)=rsinαcosθ+rcosαsinθ=y0cosθ+x0sinθ
矩阵形式如下图所示:
前面是以坐标原点为旋转中心的旋转变换,如果以任意点 O 为旋转中心来进行旋转变换,通常需要以下三个步骤:
一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果,缩放效果的公式如下
x1=K1x0
y1=K2y0
矩阵形式如下图所示:
错切变换 (skew) 在数学上又称为 Shear mapping(可译为" 剪切变换 ")或者 Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的 X 坐标 (或者 Y 坐标) 保持不变,而对应的 Y 坐标 (或者 X 坐标) 则按比例发生平移,且平移的大小和该点到 Y 轴 (或者 X 轴) 的距离成正比。错切变换通常包含两种——水平错切与垂直错切。
错切变换的计算公式如下:
x1=x0+K1y0
y1=y0
x1=x0
y1=K2x0+y0
矩阵形式如下图
由上面的分析可以发现,这个图形变换 3x3 的矩阵与色彩变换矩阵一样,每个位置的元素所表示的功能是有规律的,总结如下:
可以发现,a、b、c、d、e、f 这六个矩阵元素分别对应以下变换:
通过类似色彩矩阵中模拟矩阵的例子来模拟变形矩阵。在图形变换矩阵中,同样是通过一个一维数组来模拟矩阵,并通过 setValues() 方法将一个一维数组转换为图形变换矩阵,代码如下所示:
- private float[] mImageMatrix = new float[9];
- Matrix matrix = new Matrix();
- matrix.setValues(mImageMatrix);
当获得了变换矩阵后,就可以通过以下代码将一个图像以这个变换矩阵的形式绘制出来。
- canvas.drawBitmap(mBitmap, mMatrix, null);
示例代码:
activity:
- package com.example.androidmatrix;
- import android.app.Activity;
- import android.graphics.Bitmap;
- import android.graphics.Bitmap.Config;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Matrix;
- import android.os.Bundle;
- import android.view.View;
- import android.widget.EditText;
- import android.widget.GridLayout;
- import android.widget.ImageView;
- public class TestMatrixActivity extends Activity {
- //定义组件
- private ImageView imageView;
- private GridLayout group;
- private Bitmap bitmap;
- private EditText[] edits = new EditText[9];
- private float[] edittexts = new float[9];
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.testmatrix);
- imageView = (ImageView) findViewById(R.id.imageView);
- group = (GridLayout) findViewById(R.id.group);
- bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
- imageView.setImageBitmap(bitmap);
- group.post(new Runnable() {
- public void run() {
- setNineEdits();
- fillNineEdits();
- }
- });
- }
- //创建9个编辑框
- private void setNineEdits() {
- int width = group.getWidth();
- int height = group.getHeight();
- for (int i = 0; i < edits.length; i++) {
- edits[i] = new EditText(TestMatrixActivity.this);
- edits[i].setWidth(width / 3);
- edits[i].setHeight(height / 3);
- group.addView(edits[i]);
- }
- }
- //给九个编辑框赋值
- private void fillNineEdits() {
- for (int i = 0; i < edits.length; i++) {
- if (i % 4 == 0) {
- edits[i].setText(String.valueOf(1));
- } else {
- edits[i].setText(String.valueOf(0));
- }
- }
- }
- //重新获取九个编辑框的值
- private void getNineEdits() {
- for (int i = 0; i < edits.length; i++) {
- edittexts[i] = Float.valueOf(edits[i].getText().toString().trim());
- }
- }
- private void change() {
- Matrix matrix = new Matrix();
- Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
- matrix.setValues(edittexts);
- Canvas canvas = new Canvas(bmp);
- canvas.drawBitmap(bitmap, matrix, null);
- imageView.setImageBitmap(bmp);
- }
- public void onChange(View view) {
- getNineEdits();
- change();
- }
- public void onReset(View view) {
- fillNineEdits();
- change();
- }
- }
界面:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
- <ImageView
- android:id="@+id/imageView"
- android:layout_width="fill_parent"
- android:layout_height="0dp"
- android:layout_weight="2" />
- <GridLayout
- android:id="@+id/group"
- android:layout_width="match_parent"
- android:layout_height="200dp"
- android:layout_weight="3"
- android:columnCount="3"
- android:rowCount="3" >
- </GridLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal" >
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:onClick="onChange"
- android:text="生效" />
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:onClick="onReset"
- android:text="重置" />
- </LinearLayout>
- </LinearLayout>
效果图:
Android 系统同样提供了一些 API 来简化矩阵的运算,我们不必每次都去设置矩阵的每一个元素值。Android 中使用 Matrix 类来封装矩阵,并提供了以下几个操作方法来实现上面的四中变换方式:
Matrix 类的 set 方法会重置矩阵中的值,而 post 和 pre 方法不会,这两个方法常用来实现矩阵的混合作用。不过要注意的是,矩阵运算不满足乘法的交换律,所以矩阵乘法的前乘和后乘是两种不同的运算方式。举例说明,比如需要实现以下效果:
如果使用后乘运算,表示当前矩阵乘上参数代表的矩阵,代码如下所示:
- matrix.setRotate(45);
- matrix.postTranslate(200, 200);
如果使用前乘运算,表示参数代表的矩阵乘上当前矩阵,代码如下所示:
- matrix.setTranslate(200, 200);
- matrix.preRotate(45);
界面代码(就一个 ImageView)省略...
- private ImageView imageView;
- private Bitmap bitmap;
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.testmatrixmethod);
- imageView = (ImageView) findViewById(R.id.imageView);
- bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
- imageView.setImageBitmap(changeImage());
- }
- private Bitmap changeImage() {
- Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
- Matrix matrix = new Matrix();
- //旋转变换,参数是顺时针旋转角度
- matrix.setRotate(45);
- //平移变化,参数是要平移到的坐标
- //matrix.setTranslate(50, 50);
- //缩放变化
- //matrix.setScale(10, 10, 10, 10);
- //错切变换
- //matrix.setSkew(10, 10, 10, 10);
- Canvas canvas = new Canvas(bmp);
- canvas.drawBitmap(bitmap, matrix, null);
- return bmp;
- }
效果图:
图像的特效处理有两种方式,即使用矩阵来进行图像变换和使用 drawBitmapMesh() 方法来进行处理。drawBitmapMesh() 与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像。
drawBitmapMesh() 方法代码如下:
- public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
关键的参数如下:
bitmap:将要扭曲的图像
meshWidth:需要的横向网格数目
meshHeight :需要的纵向网格数目
verts:网格交叉点坐标数组
vertOffset:verts 数组中开始跳过的 (x, y) 坐标对的数目
要使用 drawBitmapMesh() 方法就需先将图片分割为若干个图像块。所以,在图像上横纵各画 N 条线,而这横纵各 N 条线就交织成了 NxN 个点,而每个点的坐标则以
drawBitmapMesh() 方法的功能非常强大,基本上可以实现所有的图像特效,但使用起来也非常复杂,其关键就是在于计算、确定新的交叉点的坐标。下面举例说明如何使用 drawBitmapMesh() 方法来实现一个旗帜飞扬的效果。
要想达到旗帜飞扬的效果,只需要让图片中每个交叉点的横坐标较之前不发生变化,而纵坐标较之前坐标呈现一个三角函数的周期性变化即可。
首先获取交叉点的坐标,并将坐标保存到 orig 数组中,其获取交叉点坐标的原理就是通过循环遍历所有的交叉线,并按比例获取其坐标,代码如下所示:
- mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.test);
- float bitmapWidth = mBitmap.getWidth();
- float bitmapHeight = mBitmap.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[index * 2] = verts[index * 2] = fx;
- //这里人为将坐标+100是为了让图像下移,避免扭曲后被屏幕遮挡
- orig[index * 2 + 1] = verts[index * 2 + 1] = fy + 100;
- index++;
- }
- }
接下来,在 onDraw() 方法中改变交叉点的纵坐标的值,为了实现旗帜飘扬的效果,使用一个正弦函数
- @Override protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- flagWave();
- K += 0.1f; //将K的值增加
- canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
- invalidate();
- }
- /**
- * 按当前点所在的横坐标的位置来确定纵坐标的偏移量,其中A代表正弦函数中的振幅大小
- */
- private void flagWave() {
- for (int j = 0; j <= HEIGHT; j++) {
- for (int i = 0; i <= WIDTH; i++) {
- //在获取纵坐标的偏移量时,利用正弦函数的周期性给函数增加一个周期K * Math.PI,就是为了让图像能够动起来
- float offsetY = (float) Math.sin(2 * Math.PI * i / WIDTH + K * Math.PI);
- verts[(j * (WIDTH + 1) + i) * 2 + 1] = orig[(j * (WIDTH + 1) + i) * 2 + 1] + offsetY * A;
- }
- }
- }
这样,每次在重绘时,通过改变相位来改变偏移量,从而造成一个动态的效果,就好象旗帜在风中飘扬一样,效果图如下(这里应该是动态的,似乎一个飘扬的旗帜)。
主要代码:
- package com.mfc.myview;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.util.AttributeSet;
- import android.view.View;
- import com.example.androidmatrix.R;
- public class FlagBitmapMeshView extends View {
- private final int WIDTH = 200;
- private final int HEIGHT = 200;
- private int COUNT = (WIDTH + 1) * (HEIGHT + 1);
- private float[] verts = new float[COUNT * 2];
- private float[] orig = new float[COUNT * 2];
- private Bitmap bitmap;
- private float A;
- private float k = 1;
- public FlagBitmapMeshView(Context context) {
- super(context);
- initView(context);
- }
- public FlagBitmapMeshView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initView(context);
- }
- public FlagBitmapMeshView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- initView(context);
- }
- private void initView(Context context) {
- setFocusable(true);
- bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.we);
- 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[index * 2 + 0] = verts[index * 2 + 0] = fx;
- orig[index * 2 + 1] = verts[index * 2 + 1] = fy + 100;
- index += 1;
- }
- }
- A = 50;
- }
- @Override protected void onDraw(Canvas canvas) {
- flagWave();
- k += 0.1F;
- canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
- invalidate();
- }
- private void flagWave() {
- for (int j = 0; j <= HEIGHT; j++) {
- for (int i = 0; i <= WIDTH; i++) {
- verts[(j * (WIDTH + 1) + i) * 2 + 0] += 0;
- float offsetY = (float) Math.sin((float) i / WIDTH * 2 * Math.PI + Math.PI * k);
- verts[(j * (WIDTH + 1) + i) * 2 + 1] = orig[(j * WIDTH + i) * 2 + 1] + offsetY * A;
- }
- }
- }
- }
使用 drawBitmapMesh() 方法可以创建很多复杂的图像效果,但是对它的使用也相对复杂,需要我们对图像处理有很深厚的功底。同时,对算法的要求也比较高,需要计算各种特效下不同的坐标点变化规律,从而设计出不同的特效。
源码下载:http://download.csdn.net/detail/fancheng614/9922173
来源: http://blog.csdn.net/fancheng614/article/details/76722381