下面我们就通过一个Demo来验证一下我们的观点。
package com.mikyou.matrix; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.ImageView; public class MainActivity extends Activity { private ImageView iv; private Canvas canvas; private Paint paint; private Bitmap baseBitmap; private Bitmap copyBitmap; private Matrix matrix; private EditText e1,e2,e3,e4,e5,e6,e7,e8,e9; private float t1,t2,t3,t4,t5,t6,t7,t8,t9; private List<Float> valueList; public void ok(View view){ valueList=new ArrayList<Float>(); valueList.clear(); iv.setImageBitmap(null); t1=Float.valueOf(e1.getText().toString()); valueList.add(t1); t2=Float.valueOf(e2.getText().toString()); valueList.add(t2); t3=Float.valueOf(e3.getText().toString()); valueList.add(t3); t4=Float.valueOf(e4.getText().toString()); valueList.add(t4); t5=Float.valueOf(e5.getText().toString()); valueList.add(t5); t6=Float.valueOf(e6.getText().toString()); valueList.add(t6); t7=Float.valueOf(e7.getText().toString()); valueList.add(t7); t8=Float.valueOf(e8.getText().toString()); valueList.add(t8); t9=Float.valueOf(e9.getText().toString()); valueList.add(t9); float[] imageMatrix=new float[9]; for (int i = 0; i <valueList.size(); i++) { imageMatrix[i]=valueList.get(i); } matrix=new Matrix(); matrix.setValues(imageMatrix);//类似ColorMatrix中的setColorFilter方法 canvas.drawBitmap(baseBitmap, matrix, paint); iv.setImageBitmap(copyBitmap); } public void reset(View view){ iv.setImageBitmap(null); e1.setText("1"); e2.setText("0"); e3.setText("0"); e4.setText("0"); e5.setText("1"); e6.setText("0"); e7.setText("0"); e8.setText("0"); e9.setText("1"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initData(); } private void initView() { registerAllViewId(); registerAllViewEvent(); } private void registerAllViewEvent() { } private void registerAllViewId() { iv=(ImageView) findViewById(R.id.iv); e1=(EditText) findViewById(R.id.e1); e2=(EditText) findViewById(R.id.e2); e3=(EditText) findViewById(R.id.e3); e4=(EditText) findViewById(R.id.e4); e5=(EditText) findViewById(R.id.e5); e6=(EditText) findViewById(R.id.e6); e7=(EditText) findViewById(R.id.e7); e8=(EditText) findViewById(R.id.e8); e9=(EditText) findViewById(R.id.e9); } private void initData() { baseBitmap=BitmapFactory.decodeResource(getResources(), R.drawable.pre); copyBitmap=Bitmap.createBitmap(baseBitmap.getWidth(), baseBitmap.getHeight(), baseBitmap.getConfig()); canvas=new Canvas(copyBitmap); paint=new Paint(); } }
实际上,除了我们通过底层的方法,直接修改他们的变换矩阵的值来达到某种变换的效果,实际上android已经封装一些变换的API接口
例如:
旋转变换:matrix.setRotate() 平移变换 matrix.setTranslate() 缩放变换 matrix.setScale() 错切变换 matrix.setSkew()
不过android中还提供两个非常重要的方法一个是pre(),一个是post() 提供矩阵的前乘和后乘运算。
注意: 以上的几个set方法都会重新清空重置矩阵中的值,而有时候我需要实现叠加的效果,就单单用set方法是无法完成,因为第二次叠加的变换,会把上一次的
第一次变换的矩阵值给清空,只会保存最近一次的矩阵中的值。所以android 就给我们提供pre和post方法,这两个方法可实现矩阵混合效果,从而实现图形变换的
叠加。例如:
先移动到点(400,400),在旋转45度,最后平移到(200,200)
用以上例子来说明pre(先乘)和post(后乘)运算的区别,因为在矩阵乘法中不支持交换律,所以我们需要用这两个方法来区别实现
pre运算实现:
matrix.setTranslate(200,200) matrix.preRotate(45)
post运算实现:
matrix.setRotate(45) matrix.postTranslate(200,200)
最后我们通过一个案例来实现一个组合叠加变换的效果
package com.mikyou.dealImage; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.os.Bundle; import android.view.View; import android.widget.ImageView; public class MainActivity extends Activity { /** *@author zhongqihong *图片的绘制的个人总结: *如何去仿照一张原图去重新绘制一张图片呢?? *需要如下材料: *1、需要参考的原图 *2、画纸(实际上就是一张没有任何内容的图片,空白图片) *3、画布(就是用于固定画纸的) *4、画笔(就是用于绘制图片的工具) *其实一般分为如下几步: *1、首先必须拿到原图构造出一张空画纸(即空白图片),知道原图的大小,分辨率等信息,从而就可以确定我的画纸的大小,及所确定的分辨率(默认使用原图的分辨率)。 * baseBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.img_small_1);//拿到原图对象 * copyBitmap=Bitmap.createBitmap(baseBitmap.getWidth(),baseBitmap.getHeight(), baseBitmap.getConfig());//根据原图对象的相关信息,得到同大小的画纸,包括原图分辨率 *2、然后,确定了一个空白的画纸后,就需要将我们的画纸固定在我们的画布上 * Canvas canvas=new Canvas(copyBitmap); *3、接着就是构造一个画笔对象 * Paint paint=new Paint(); * 4、接着对图片进行一系列的操作,诸如:缩放、平移、旋转等操作 * Matrix matrix =new Matrix(); * 缩放:(缩放中心点:默认是画纸左上角点的坐标) * matrix.setScale(1.5f,1.5f);//第一个参数为X轴上的缩放比例为1.5(>1表示放大,<1表示缩小)此处就表示在XY轴上分别放大为原图1.5倍 * matrix.setScale(-1f,1f);//表示在X轴反向缩放,Y轴不变,即相当于把原图往X轴负方向反向翻转了一下,即相当于处理后的图片与原图位置(即现在的画纸位置)关于Y负半轴对称 * matrix.setScale(1f,-1f);//表示在X轴不变,Y轴的,即相当于把原图往Y轴负方向反向翻转了一下,即相当于处理后的图片与原图位置(即现在的画纸位置)关于X正半轴对称 * 5、处理完后,就需要把我们处理好的图片绘制在画布上形成最后的图片 * canvas.drawBitmap(baseBitmap,matrix,paint);//第一个参数表示原图的Bitmap对象,表示按照原图的样式内容在原来相同规格空白的纸上画出图片内容 iv.setImageBitmap(copyBitmap);//最后将图片的显示在ImageView控件上 * 个人理解:感觉整个过程很像PS中置入一张图片到一张空白画纸上,首先我们 * 需要去设置一张空白的画纸,然后将我们的原图置入,在置入之前我们可以做些对图片的操作 * 包括缩放,平移,旋转,确定一些操作后,点击确定就相当于调用绘制方法,从而就形成一张处理后的图片 * * */ private ImageView iv; private Bitmap baseBitmap;//原图 private Bitmap copyBitmap;//画纸 private Canvas canvas;//画布 private Paint paint;//画笔 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv= (ImageView) findViewById(R.id.iv2); } public void btn(View view){ //拿到原图:得到原图大小 baseBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.img_small_1); //1、拿到一张与原图一样大小的纸,并没有内容 copyBitmap=Bitmap.createBitmap(1200,1300, baseBitmap.getConfig());//拿到原图的分辨率和原图的Config //2、将这张纸固定在画布上 Canvas canvas=new Canvas(copyBitmap); //3、画笔 paint=new Paint(); paint.setAlpha(100); //4、加入一些处理的规则 Matrix matrix=new Matrix(); //1、缩放规则 //matrix.setScale(1.5f,1.5f); //2、位移, // matrix.setTranslate(50f,50f);//分别在x轴和Y轴上位移50,针对图片的左上角为原点来移动 //3、旋转 //matrix.setRotate(45f);//代表顺时针旋转45度,默认以左上角为原点 // matrix.setRotate(45, baseBitmap.getWidth()/2, baseBitmap.getHeight()/2);//第二个参数和第三个参数表示旋转中心的点的X,Y坐标 //4、翻转镜面效果 // matrix.setScale(-1f, 1f); // matrix.setTranslate(baseBitmap.getWidth(), 0); // matrix.postTranslate(baseBitmap.getWidth(), 0);//如果要对图片进行多次操作,就要用post的方法来操作 //5、倒影效果 matrix.setScale(1f, -1f);//先把图片在y轴方向反向缩放 matrix.postTranslate(0, baseBitmap.getHeight());//然后再把反向缩放后的图片移到canvas上显示即可 matrix.postTranslate(baseBitmap.getWidth()+160, 0); matrix.postSkew(-1, 0); //5、将处理过的图片画出来 canvas.drawBitmap(baseBitmap,matrix,paint);//第一个参数表示原图的Bitmap对象,表示按照原图的样式内容在原来相同规格空白的纸上画出图片内容 iv.setImageBitmap(copyBitmap);//最后将图片的显示在ImageView控件上 } }
与ColorMatrix颜色矩阵类似,在图形变换中也有一个针对每一个像素块做处理,而在颜色变换是针对每个像素点。针对像素快的处理我们使用
drawBitmapMesh()方法来处理。
drawBitmapMesh()和处理每像素点类似,只不是将图像分成一个一个的小块然后针对每个小块来改变整个图像
drawBitmapMesh()方法很是重要利用可以实现很多的图片的特效。下面我们就重点来介绍一下这个方法:
drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint)
官方源码是这样介绍这个方法的:
/**
* Draw the bitmap through the mesh, where mesh vertices are evenly
* distributed across the bitmap. There are meshWidth+1 vertices across, and
* meshHeight+1 vertices down. The verts array is accessed in row-major
* order, so that the first meshWidth+1 vertices are distributed across the
* top of the bitmap from left to right. A more general version of this
* method is drawVertices().
*
* @param bitmap The bitmap to draw using the mesh
* @param meshWidth The number of columns in the mesh. Nothing is drawn if
* this is 0
* @param meshHeight The number of rows in the mesh. Nothing is drawn if
* this is 0
* @param verts Array of x,y pairs, specifying where the mesh should be
* drawn. There must be at least
* (meshWidth+1) * (meshHeight+1) * 2 + vertOffset values
* in the array
* @param vertOffset Number of verts elements to skip before drawing
* @param colors May be null. Specifies a color at each vertex, which is
* interpolated across the cell, and whose values are
* multiplied by the corresponding bitmap colors. If not null,
* there must be at least (meshWidth+1) * (meshHeight+1) +
* colorOffset values in the array.
* @param colorOffset Number of color elements to skip before drawing
* @param paint May be null. The paint used to draw the bitmap
*/
它大概的意思的是这样的,通过网格来绘制图片,那么这张图片将会被meshWidth画出横向格子,meshHeight画出纵向的格子
那么在横向上将会产生meshWidth+1个交叉点,在纵向上会产生meshHeight+1个交叉点,最后总的交叉点个数为(meshWidth+1)*(meshHeight+1)
bitmap: 需要绘制在网格上的图像。
meshWidth: 网格的宽度方向的数目(列数),为0时不绘制图像。
meshHeight:网格的高度方向的数目(含数),为0时不绘制图像。
verts: (x,y)对的数组,表示网格顶点的坐标,至少需要有(meshWidth+1) * (meshHeight+1) * 2 + meshOffset 个(x,y)坐标。
vertOffset: verts数组中开始跳过的(x,y)对的数目。
Colors: 可以为空,不为空为没个顶点定义对应的颜色值,至少需要有(meshWidth+1) * (meshHeight+1) * 2 + meshOffset 个(x,y)坐标。
colorOffset: colors数组中开始跳过的(x,y)对的数目。
paint: 可以为空。
下面将通过一个自定义View方法来实现,旗帜飘扬的图片控件。并且添加自定义属性,下次使用可以方便的在XMl中更换图片
修改划分方格数,振幅大小,频率大小
实现整体思路如下:
针对像素块来实现图形扭曲的原理:
它的原理就是通过修改划分后的小方格产生交叉点的坐标,来实现图片的扭曲
本案例实现一个旗帜飘动形状的图片
大致的思路如下:
实现思路整体分两部分,第一部分取得所有扭曲前图片的所有交叉点的坐标
并把这些交叉点坐标保存在orig数组中;第二部分修改原图中方格每个交叉点的坐标,遍历这个数组,然后通过某种算法
使得这些交叉点的坐标,呈某种规律函数曲线变化。这里就以三角函数中的正弦函数
来改变这些交叉点坐标,从而产生每个交叉点新的坐标,然后再将这些新的坐标保存在verts数组中,最后通过drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint)
实现图片的绘制。
自定义属性:attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="Src" format="reference"></attr> <attr name="Amplitude" format="integer"></attr> <attr name="RowNum" format="integer"></attr> <attr name="ColumnNum" format="integer"></attr> <attr name="Frequency" format="float"></attr> <declare-styleable name="MikyouBannerView"> <attr name="Src"></attr> <attr name="Amplitude"></attr> <attr name="Frequency"></attr> <attr name="RowNum"></attr> <attr name="ColumnNum"></attr> </declare-styleable> </resources>
package com.mikyou.myview; import com.mikyou.piexkuai.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.widget.ImageView; /** * @author mikyou * 针对像素块来实现图形扭曲的原理: * 它的原理就是通过修改划分后的小方格产生交叉点的坐标,来实现图片的扭曲 * 本案例实现一个旗帜飘动形状的图片 * 大致的思路如下: * 实现思路整体分两部分,第一部分取得所有扭曲前图片的所有交叉点的坐标 * 并把这些交叉点坐标保存在orig数组中;第二部分修改原图中方格每个交叉点的坐标,遍历这个数组,然后通过某种算法 * 使得这些交叉点的坐标,呈某种规律函数曲线变化。这里就以三角函数中的正弦函数 * 来改变这些交叉点坐标,从而产生每个交叉点新的坐标,然后再将这些新的坐标保存在 * verts数组中,最后通过drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint) * 实现图片的绘制。 * */ public class MyBannerImageView extends View{ //定义两个常量表示需要将这张图片划分成20*20=400个小方格,//定义两个常量,这两个常量指定该图片横向,纵向上都被划分为20格 private int WIDTH=40;//横向划分的方格数目 private int HEIGHT=40;//纵向划分的方格数目 private float FREQUENCY=0.1f;//三角函数的频率大小 private int AMPLITUDE=60;//三角函数的振幅大小 //那么将会产生21*21=421个交叉点 private int POINT_COUNT=(WIDTH+1)*(HEIGHT+1); //由于,我要储存一个坐标信息,一个坐标包括x,y两个值的信息,相邻2个值储存为一个坐标点 //其实大家应该都认为这样不好吧,还不如直接写一个类来直接保存一个点的信息,但是没办法 // 但是在drawBitmapMesh方法中传入的是一个verts数组,该数组就是保存所有点的x,y坐标全都放在一起 //所以,我就只能这样去控制定义orig和verts数组了, private Bitmap baseBitmap; private float[] orig=new float[POINT_COUNT*2];//乘以2是因为x,y值是一对的。 private float[] verts=new float[POINT_COUNT*2]; private float k; public MyBannerImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //接收自定义属性值 TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MikyouBannerView); for (int i = 0; i < array.getIndexCount(); i++) { int attr=array.getIndex(i); switch (attr) { case R.styleable.MikyouBannerView_Src: baseBitmap=BitmapFactory.decodeResource(getResources(), array.getResourceId(attr, R.drawable.ic_launcher)); break; case R.styleable.MikyouBannerView_ColumnNum: HEIGHT=array.getInt(attr, 40); break; case R.styleable.MikyouBannerView_RowNum: WIDTH=array.getInt(attr, 40); break; case R.styleable.MikyouBannerView_Amplitude: AMPLITUDE=array.getInt(attr, 60); break; case R.styleable.MikyouBannerView_Frequency: FREQUENCY=array.getFloat(attr, 0.1f); break; default: break; } } array.recycle(); initData(); } public MyBannerImageView(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyBannerImageView(Context context) { this(context,null); } //set,gfanfg public int getWIDTH() { return WIDTH; } public void setWIDTH(int wIDTH) { WIDTH = wIDTH; } public int getHEIGHT() { return HEIGHT; } public void setHEIGHT(int hEIGHT) { HEIGHT = hEIGHT; } public float getFREQUENCY() { return FREQUENCY; } public void setFREQUENCY(float fREQUENCY) { FREQUENCY = fREQUENCY; } public int getAMPLITUDE() { return AMPLITUDE; } public void setAMPLITUDE(int aMPLITUDE) { AMPLITUDE = aMPLITUDE; } public Bitmap getBaseBitmap() { return baseBitmap; } public void setBaseBitmap(Bitmap baseBitmap) { this.baseBitmap = baseBitmap; } @Override protected void onDraw(Canvas canvas) { flagWave(); k+=FREQUENCY; canvas.drawBitmapMesh(baseBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null); invalidate(); } private void initData() { float baseBitmapWidth=baseBitmap.getWidth(); float baseBitmapHeight=baseBitmap.getHeight(); int index=0; //通过遍历所有的划分后得到的像素块,得到原图中每个交叉点的坐标,并把它们保存在orig数组中 for (int i = 0; i <= HEIGHT; i++) {//因为这个数组是采取行优先原则储存点的坐标,所以最外层为纵向的格子数,然后一行一行的遍历 float fy=baseBitmapHeight*i/HEIGHT;//得到每行中每个交叉点的y坐标,同一行的y坐标一样 for (int j = 0; j <= WIDTH; j++) { float fx=baseBitmapHeight*j/WIDTH;//得到每行中的每个交叉点的x坐标,同一列的x坐标一样 orig[index*2+0]=verts[index*2+0]=fx;//存储每行中每个交叉点的x坐标,为什么是index*2+0作为数组的序号呢?? //因为我们之前也说过这个数组既存储x坐标也存储y坐标,所以每个点就占有2个单位数组空间 orig[index*2+1]=verts[index*2+1]=fy+200;//存储每行中每个交叉点的y坐标.为什么需要+1呢?正好取x坐标相邻的下标的元素的值 //+200是为了避免等下在正弦函数扭曲下,会把上部分给挡住所以下移200 index++; } } } /** * @author mikyou * 加入三角函数正弦函数Sinx的算法,来修改原图数组保存的交叉点的坐标 * 从而得到旗帜飘扬的效果,这里我们只修改y坐标,x坐标保持不变 * */ public void flagWave(){ for (int i = 0; i <=HEIGHT ; i++) { for (int j = 0; j <WIDTH; j++) { verts[(i*(WIDTH+1)+j)*2+0]+=0; float offSetY=(float)Math.sin((float)j/WIDTH*2*Math.PI+Math.PI*k); verts[(i*(WIDTH+1)+j)*2+1]=orig[(i*(WIDTH+1)+j)*2+1]+offSetY*AMPLITUDE; } } } }
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:mikyou="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.mikyou.myview.MyBannerImageView android:id="@+id/iv" android:layout_width="wrap_content" android:layout_height="wrap_content" mikyou:Amplitude="100" mikyou:ColumnNum="20" mikyou:Frequency="0.1" mikyou:RowNum="20" mikyou:Src="@drawable/pre" /> </LinearLayout>运行的结果;
注意:不好意思,因为没有录制GIF所以看不出动态效果,大家可以下载Demo看看.
Demo下载链接