Android进阶-View系列(三)-Android绘图机制与处理技巧(下)
2017-12-02 11:06
513 查看
Android图像处理之色彩特效处理
Bitmap,它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、滤、蓝这四个通道分量,他们共同决定了每个像素点显示的颜色。
色彩矩阵分析
在色彩处理中,通常使用一下三个角度来描述一个图像。
色调:物体传播的颜色
饱和度:颜色的纯度,从0(灰)到100%(饱和)来进行描述
亮度:颜色的相对明暗程度
而在Android中,系统使用一个颜色矩阵——ColorMatrix,来处理图像的这些色彩效果。Android中的颜色矩阵是一个4*5的数字矩阵,它用来对图片的色彩进行处理。而对于每个像素点,都有一个颜色分量矩阵用来保存颜色的RGBA值。
根绝上面对矩阵MC的定义,通过矩阵乘法运算法则,可以得到:
矩阵运算的乘法计算过程如下所示:
看颜色矩阵M:
第一行的abcde值用来决定新的颜色值中的R——红色
第二行的fghij值用来决定新的颜色值中的G——绿色
第三行的kimno值用来决定新的颜色值中的B——蓝色
第四行的pqrst值用来决定新的颜色值中的A——透明度
矩阵A中的第五列——ejot值分别用来决定每个分量中的offset,即偏移量
接下来通过几个小例子来进一步讲解:
首先,重新看一下矩阵变换的计算公司,以R分量为例,计算过程如下。
如果令a=1,b、c、d、e都等于0,那么计算结果为R1=R,因此可以构造出一个矩阵。
如果把这个矩阵带入公式R1=AC,那么根据矩阵乘法运算法则,可以得到R1=R。因此,这个矩阵通常被用来作为初始的颜色矩阵来使用,它不会对原有颜色值进行任何变化。
那么当我们要变换颜色值的时候,通常有两种方法。一个是直接改变颜色的offset,即偏移量的值来修改颜色分量,另一个方法是直接改变对应RGBA值的系数来调整颜色分量的值。
从前面的分析中,可以知道要修改R1的值,只要将第五列的值进行修改即可。即改变颜色的偏移量,其他值保存初始矩阵的值。
在上面这个矩阵中,我们修改了R、G所对应的颜色偏移量,那么最后的处理结果就是图像的红色、绿色分量增加了100.而我们知道,红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图徐昂的色调偏黄色。
如果修改颜色分量中的某个系数值,而其他值依然保存初始矩阵的值
在上面这个矩阵中,改变了G分量所对应的系数g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿。
下面通过一个实例来看看如何通过矩阵改变图像的色调、饱和度和亮度。在Android中系统封装了一个类---ColorMatrix,通过这个类,可以很方便地通过改变矩阵值来处理颜色效果。
色调:
Android系统还提供了setRotate(int axis, float degree)方法来设置颜色的色调,第一个参数分别使用0、1、2代表Red、Green、Blue三种颜色,第二参数需要处理的值
饱和度:
Android系统提供了setSaturation(float sat)方法来设置颜色的饱和度,参数代表设置饱和度的值
亮度:
Android系统提供了setScale(float rscale,float gscale,float bscale,float ascale)方法来设置三原色和透明度的亮度,参数分别是红、绿、蓝、透明度的亮度大小值
除了单独使用上面三种方式来进行颜色效果处理之外,Android系统还封装了矩阵的乘法运算。还提供了postConcat()方法来将矩阵的作用效果混合,从而叠加处理效果
通过SeekBar调节色调、饱和度、亮度代码:
运行结果:
Android颜色矩阵---ColorMatrix
下面模拟一个4*5的颜色矩阵,不仅可以通过Android系统提供的API来进行ColorMatrix的修改,同样可以精确地修改矩阵的值来实现颜色效果的处理。
代码演示:
运行结果:
常用图像颜色矩阵处理效果
灰度效果
图像反转
怀旧效果
去色效果
高饱和度
像素点分析
在Android中,系统系统提供了Bitmap.getPixels()方法来帮我们提取整个Bitmap中的像素点,并保存在一个数组中:
这几个参数的具体含义如下:
pixels:接收位图颜色值的数组
offset:写入到pixels[]中的第一个像素索引值
stride:pixels[]中的行间距
x:从位图中读取的第一个像素的x坐标值
y:从位图中读取的第一个像素的y坐标值
width:从每一行中读取的像素宽度
height:读取的行数
通常使用如下代码:
接下来获取每个像素具体的ARGB值:
接下来就是修改像素值,产生新的像素值
最后使用我们的新像素值
底片效果、老照片效果、浮雕效果:
Android变形矩阵---Matrix
对于图像的色彩处理,Android系统提供了ColorMatrx颜色矩阵来帮助我们进行图像处理。而对于图像的图形变换,Android的图形变换矩阵是一个3*3的矩阵
当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样
与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵
图像的变形处理通常包含以下四种基本变换:
Translate:平移变换
Rotate:旋转变换
Scale:缩放变换
Skew:错切变换
平移变换:平移变换的坐标值变换过程如下图坐标系,即将每个像素点都进行平移变换,当从P(x0,y0)平移到P(x,y)时,坐标值发生了如下所示的变化。
根据左边中的公式计算可以发现如下等式:
旋转变换:即指一个点围绕一个中心旋转刀一个新的点,图中是以坐标的原点为旋转中心的旋转变化,如果以任意点O为选装中心来进行旋转变换,通常需要以下三个步骤:
将坐标原点平移到O点
使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
将坐标原点还原
缩放变换:一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果。缩放效果的计算公式如下:
写成矩阵形式:
错切变换:错切变换是一种比较特殊的线性变换。错切变换的效果就是让所有点的X坐标(或者Y坐标)保持不变,而对应的Y坐标(或者X坐标)按比例发生平移,且平移的大小和该点到X轴(或者Y轴)的垂直距离成正比。
错切变换通常包含两种---水平错切和垂直错切,如图
错切变换的计算公式:
了解了矩阵变换规律之后,可以通过setValues()方法将一个一维数组转换为图形变换矩阵:
Android中使用Matrix类来封装矩阵,并提供了一下几个操作方法来实现上面的四种变换方式:
matriX.setRotate():旋转变换
matriX.setTranslate():平移变换
matriX.setScale():缩放变换
matriX.setSkew():错切变换
pre()和post():提供矩阵的前乘和后乘运算
举个例子说明前乘和后乘的不同运算方式:
先平移到(300, 100)
再旋转45度
最后平移到(200, 200)
如果使用后乘运算,代码如下:
如果使用前乘运算,代码如下:
像素块分析
drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像,该方法代码如下:
参数分析:
bitmap:将要扭曲的图像
meshWidth:需要的横向网格数目
meshHeight:需要的纵向网格数目
verts:网格交叉点的坐标数组
vertOffset:verts数组中开始跳过的(x,y)坐标对的数目
其中最重要的参数是一个数组:verts
飘扬旗子的代码案例:
运行结果:
Android图像处理之画笔特效处理
PorterDuffXfermode
先看一张非常经典的图,出自API Demo,基本上所有讲PorterDuffXfermode的文章都会使用这张图
先用一个圆角的例子说明一下的使用。首先用一个普通画笔画一个Mask遮罩层,再用带PorterDuffXfermode的画笔将图像画在遮罩层上,代码如下:
圆角例子:
下面再来看一个稍微复杂点的效果-刮刮卡效果:
运行结果:
Shader:
Shader又被称为着色器、渲染器。它可以实现一系列的渲染,渐变等效果,Android中的Shader包括以下几种:
BitmapShader:位图Shader
LinearGradient:线性Shader
RadialGradient:光束Shader
SweepGradient:梯形Shader
ComposeShader:混合Shader
其中BitmapShader产生的是一个图像,它的作用是通过Paint对话不进行指定Bitmap的填充,填充有三种模式可以选择:
CLAMP拉伸:拉伸的是图片最后的那一个像素,不断重复
REPEAT重复:横向,纵向不断重复
MIRROR镜像:横向不断翻转重复,纵向不断翻转重复
圆形图片:
运行结果:
下面把TileMode改为REPEAT:
运行结果:
使用LinearGradient:
运行结果:
接下来使用LinearGradient和PorterDuffXfermode来创建一个具有倒影下过的图片:
运行结果:
PathEffect:
要理解PathEffect,先来看一张比较直观的图,来了解一下什么是PathEffect:
PathEffect就是指用各种笔触效果来绘制一个路径,上图中展示的集中绘制PathEffect的方式,从上到下依次是:
没效果
CornerPathEffect:拐弯角变得圆滑
DiscretePathEffect:线段上会产生许多杂点
DashPathEffect:绘制虚线,用一个数据来设置各个点之间的间隔
PathDashPathEffect:绘制虚线,可以使用方形点虚线和圆形点虚线
ComposePathEffect:可任意组合两种路径(PathEffect)的特性
代码演示:
View之孪生兄弟---SurfaceView
SurfaceView与View是有区别的:View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕的重绘,刷新的间隔时间为16ms。如果在16ms内View完成所有操作,那么用户视觉上就不会觉得卡顿;否则就会不断阻塞主线程,导致画面卡顿,在自定义View的Log中经常会出现下面的警告:
SurfaceView与View的主要区别:
View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新;
View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新;
View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中就已经实现了双缓冲机制。
总之,如果你的自定义View需要频繁的刷新,或者刷新时数据处理量比较大,那么就使用SurfaceView来代替View。
SurfaceView的使用
创建SurfaceView:创建自定义的SurfaceView继承自SurfaceView,并实现两个接口---SurfaceHolder.Callback和Runnable
初始化SurfaceView:在自定义SurfaceView的构造方法中,需要对SurfaceView进行初始化。在自定义的SurfaceView中,定义相关的成员变量,并注册SurfacHolder的回调方法。
使用SurfaceView:通过SurfacHolder对象lockCanvas()方法获得Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交
使用SurfaceView的模板代码:
以上代码基本上可以满足大部分的SurfaceView绘图需求,唯一需要注意的是在绘制方法中,将mHolder.unlockCanvasAndPost(mCanvas)方法放到代码快中,来保证每次都能将内容提交。
SurfaceView的实例:
正弦曲线:
运行结果:
绘画板:
运行结果:
源码下载
Bitmap,它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、滤、蓝这四个通道分量,他们共同决定了每个像素点显示的颜色。
色彩矩阵分析
在色彩处理中,通常使用一下三个角度来描述一个图像。
色调:物体传播的颜色
饱和度:颜色的纯度,从0(灰)到100%(饱和)来进行描述
亮度:颜色的相对明暗程度
而在Android中,系统使用一个颜色矩阵——ColorMatrix,来处理图像的这些色彩效果。Android中的颜色矩阵是一个4*5的数字矩阵,它用来对图片的色彩进行处理。而对于每个像素点,都有一个颜色分量矩阵用来保存颜色的RGBA值。
根绝上面对矩阵MC的定义,通过矩阵乘法运算法则,可以得到:
矩阵运算的乘法计算过程如下所示:
R1=a*R+b*G+c*B+d*A+e; G1=f*R+g*G+h*B+i*A+j; B1=k*R+l*G+m*B+n*A+o; A1=p*R+q*G+r*B+s*A+t;
看颜色矩阵M:
第一行的abcde值用来决定新的颜色值中的R——红色
第二行的fghij值用来决定新的颜色值中的G——绿色
第三行的kimno值用来决定新的颜色值中的B——蓝色
第四行的pqrst值用来决定新的颜色值中的A——透明度
矩阵A中的第五列——ejot值分别用来决定每个分量中的offset,即偏移量
接下来通过几个小例子来进一步讲解:
首先,重新看一下矩阵变换的计算公司,以R分量为例,计算过程如下。
R1=a*R+b*G+c*B+d*A+e;
如果令a=1,b、c、d、e都等于0,那么计算结果为R1=R,因此可以构造出一个矩阵。
如果把这个矩阵带入公式R1=AC,那么根据矩阵乘法运算法则,可以得到R1=R。因此,这个矩阵通常被用来作为初始的颜色矩阵来使用,它不会对原有颜色值进行任何变化。
那么当我们要变换颜色值的时候,通常有两种方法。一个是直接改变颜色的offset,即偏移量的值来修改颜色分量,另一个方法是直接改变对应RGBA值的系数来调整颜色分量的值。
从前面的分析中,可以知道要修改R1的值,只要将第五列的值进行修改即可。即改变颜色的偏移量,其他值保存初始矩阵的值。
在上面这个矩阵中,我们修改了R、G所对应的颜色偏移量,那么最后的处理结果就是图像的红色、绿色分量增加了100.而我们知道,红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图徐昂的色调偏黄色。
如果修改颜色分量中的某个系数值,而其他值依然保存初始矩阵的值
在上面这个矩阵中,改变了G分量所对应的系数g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿。
下面通过一个实例来看看如何通过矩阵改变图像的色调、饱和度和亮度。在Android中系统封装了一个类---ColorMatrix,通过这个类,可以很方便地通过改变矩阵值来处理颜色效果。
色调:
Android系统还提供了setRotate(int axis, float degree)方法来设置颜色的色调,第一个参数分别使用0、1、2代表Red、Green、Blue三种颜色,第二参数需要处理的值
ColorMatrix hueMatrix = new ColorMatrix(); hueMatrix.setRotate(0, hue0); hueMatrix.setRotate(1, hue1); hueMatrix.setRotate(2, hue2);
饱和度:
Android系统提供了setSaturation(float sat)方法来设置颜色的饱和度,参数代表设置饱和度的值
ColorMatrix saturationMatrix = new ColorMatrix(); saturationMatrix.setSaturation(saturation);
亮度:
Android系统提供了setScale(float rscale,float gscale,float bscale,float ascale)方法来设置三原色和透明度的亮度,参数分别是红、绿、蓝、透明度的亮度大小值
ColorMatrix lumMatrix = new ColorMatrix(); lumMatrix.setScale(lum, lum, lum, 1);
除了单独使用上面三种方式来进行颜色效果处理之外,Android系统还封装了矩阵的乘法运算。还提供了postConcat()方法来将矩阵的作用效果混合,从而叠加处理效果
ColorMatrix imageMatrix = new ColorMatrix(); imageMatrix.postConcat(hueMatrix); imageMatrix.postConcat(saturationMatrix); imageMatrix.postConcat(lumMatrix);
通过SeekBar调节色调、饱和度、亮度代码:
public class PrimaryColorActivity extends ActionBarActivity implements SeekBar.OnSeekBarChangeListener { private static int MAX_VALUE = 255; private static int MID_VALUE = 127; private ImageView mImageView; private SeekBar mSeekbarhue, mSeekbarSaturation, mSeekbarLum; private float mHue, mStauration, mLum; private Bitmap bitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_primary_color); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test3); mImageView = (ImageView) findViewById(R.id.imageview); mSeekbarhue = (SeekBar) findViewById(R.id.seekbarHue); mSeekbarSaturation = (SeekBar) findViewById(R.id.seekbarSaturation); mSeekbarLum = (SeekBar) findViewById(R.id.seekbatLum); mSeekbarhue.setOnSeekBarChangeListener(this); mSeekbarSaturation.setOnSeekBarChangeListener(this); mSeekbarLum.setOnSeekBarChangeListener(this); mSeekbarhue.setMax(MAX_VALUE); mSeekbarSaturation.setMax(MAX_VALUE); mSeekbarLum.setMax(MAX_VALUE); mSeekbarhue.setProgress(MID_VALUE); mSeekbarSaturation.setProgress(MID_VALUE); mSeekbarLum.setProgress(MID_VALUE); mImageView.setImageBitmap(bitmap); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { switch (seekBar.getId()) { case R.id.seekbarHue: mHue = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180; break; case R.id.seekbarSaturation: mStauration = progress * 1.0F / MID_VALUE; break; case R.id.seekbatLum: mLum = progress * 1.0F / MID_VALUE; break; } mImageView.setImageBitmap(ImageHelper.handleImageEffect(bitmap, mHue, mStauration, mLum)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }
运行结果:
Android颜色矩阵---ColorMatrix
下面模拟一个4*5的颜色矩阵,不仅可以通过Android系统提供的API来进行ColorMatrix的修改,同样可以精确地修改矩阵的值来实现颜色效果的处理。
代码演示:
public class ColorMatrixActivity extends ActionBarActivity { private ImageView mImageView; private GridLayout mGroup; private Bitmap bitmap; private int mEtWidth, mEtHeight; private EditText[] mEts = new EditText[20]; private float[] mColorMatrix = new float[20]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_color_matrix); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test4); mImageView = (ImageView) findViewById(R.id.imageview); mGroup = (GridLayout) findViewById(R.id.group); mImageView.setImageBitmap(bitmap); mGroup.post(new Runnable() { @Override public void run() { // 获取宽高信息 mEtWidth = mGroup.getWidth() / 5; mEtHeight = mGroup.getHeight() / 4; addEts(); initMatrix(); } }); } // 获取矩阵值 private void getMatrix() { for (int i = 0; i < 20; i++) { mColorMatrix[i] = Float.valueOf(mEts[i].getText().toString()); } } // 将矩阵值设置到图像 private void setImageMatrix() { Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); android.graphics.ColorMatrix colorMatrix = new android.graphics.ColorMatrix(); colorMatrix.set(mColorMatrix); Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); canvas.drawBitmap(bitmap, 0, 0, paint); mImageView.setImageBitmap(bmp); } // 作用矩阵效果 public void btnChange(View view) { getMatrix(); setImageMatrix(); } // 重置矩阵效果 public void btnReset(View view) { initMatrix(); getMatrix(); setImageMatrix(); } // 添加EditText private void addEts() { for (int i = 0; i < 20; i++) { EditText editText = new EditText(ColorMatrixActivity.this); mEts[i] = editText; mGroup.addView(editText, mEtWidth, mEtHeight); } } // 初始化颜色矩阵为初始状态 private void initMatrix() { for (int i = 0; i < 20; i++) { if (i % 6 == 0) { mEts[i].setText(String.valueOf(1)); } else { mEts[i].setText(String.valueOf(0)); } } } }
运行结果:
常用图像颜色矩阵处理效果
灰度效果
图像反转
怀旧效果
去色效果
高饱和度
像素点分析
在Android中,系统系统提供了Bitmap.getPixels()方法来帮我们提取整个Bitmap中的像素点,并保存在一个数组中:
bitmap.getPixels(pixels, offset, stride, x, y, width, height);
这几个参数的具体含义如下:
pixels:接收位图颜色值的数组
offset:写入到pixels[]中的第一个像素索引值
stride:pixels[]中的行间距
x:从位图中读取的第一个像素的x坐标值
y:从位图中读取的第一个像素的y坐标值
width:从每一行中读取的像素宽度
height:读取的行数
通常使用如下代码:
bitmap.getPixels(oldPx, 0, bitmap.getWidth(), 0, 0, width, height);
接下来获取每个像素具体的ARGB值:
color = oldPx[i]; r = Color.red(color); g = Color.green(color); b = Color.blue(color); a = Color.alpha(color);
接下来就是修改像素值,产生新的像素值
r1 = (int) (0.393 * r + 0.769 * g + 0.189 * b); g1 = (int) (0.349 * r + 0.686 * g + 0.168 * b); b1 = (int) (0.272 * r + 0.534 * g + 0.131 * b); newPx[i] = Color.argb(a, r1, b1, g1);
最后使用我们的新像素值
bmp.setPixels(newPx, 0, width, 0, 0, width, height);
底片效果、老照片效果、浮雕效果:
Android变形矩阵---Matrix
对于图像的色彩处理,Android系统提供了ColorMatrx颜色矩阵来帮助我们进行图像处理。而对于图像的图形变换,Android的图形变换矩阵是一个3*3的矩阵
当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样
X1 = a x X + b x Y + c; Y1 = d x X + e x Y + f; 1 = g x X + h x Y + i;
与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵
图像的变形处理通常包含以下四种基本变换:
Translate:平移变换
Rotate:旋转变换
Scale:缩放变换
Skew:错切变换
平移变换:平移变换的坐标值变换过程如下图坐标系,即将每个像素点都进行平移变换,当从P(x0,y0)平移到P(x,y)时,坐标值发生了如下所示的变化。
根据左边中的公式计算可以发现如下等式:
X = X0 + △X; Y = Y0 + △Y;
旋转变换:即指一个点围绕一个中心旋转刀一个新的点,图中是以坐标的原点为旋转中心的旋转变化,如果以任意点O为选装中心来进行旋转变换,通常需要以下三个步骤:
将坐标原点平移到O点
使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
将坐标原点还原
缩放变换:一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果。缩放效果的计算公式如下:
x = K1 * x0 y = K2 * y0
写成矩阵形式:
错切变换:错切变换是一种比较特殊的线性变换。错切变换的效果就是让所有点的X坐标(或者Y坐标)保持不变,而对应的Y坐标(或者X坐标)按比例发生平移,且平移的大小和该点到X轴(或者Y轴)的垂直距离成正比。
错切变换通常包含两种---水平错切和垂直错切,如图
错切变换的计算公式:
x = x0 + k1 * y0 y = k2 * x0 + y0
了解了矩阵变换规律之后,可以通过setValues()方法将一个一维数组转换为图形变换矩阵:
private float [] mImageMatrix = new float[9]; Matrix matrix = new Matrix(); matrix.setValues(mImageMatrix); canvas.drawBitmap(mBitmmap,matrix,null);
Android中使用Matrix类来封装矩阵,并提供了一下几个操作方法来实现上面的四种变换方式:
matriX.setRotate():旋转变换
matriX.setTranslate():平移变换
matriX.setScale():缩放变换
matriX.setSkew():错切变换
pre()和post():提供矩阵的前乘和后乘运算
举个例子说明前乘和后乘的不同运算方式:
先平移到(300, 100)
再旋转45度
最后平移到(200, 200)
如果使用后乘运算,代码如下:
matriX.setRotate(45); matriX.postTranslate(200, 200);
如果使用前乘运算,代码如下:
matriX.setTranslate(200, 200); matriX.perRotate(45);
像素块分析
drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图像分成了一个个的小块,然后通过改变每一个图像块来修改整个图像,该方法代码如下:
canvas.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)坐标对的数目
其中最重要的参数是一个数组:verts
飘扬旗子的代码案例:
/** * Created by Layne_Yao on 2017-11-30 下午5:04:49. * CSDN:http://blog.csdn.net/Jsagacity */ 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.test); 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; } } } }
运行结果:
Android图像处理之画笔特效处理
PorterDuffXfermode
先看一张非常经典的图,出自API Demo,基本上所有讲PorterDuffXfermode的文章都会使用这张图
先用一个圆角的例子说明一下的使用。首先用一个普通画笔画一个Mask遮罩层,再用带PorterDuffXfermode的画笔将图像画在遮罩层上,代码如下:
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1); mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mOut); mPaint = new Paint(); mPaint.setAntiAlias(true); canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint); mPaint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(mBitmap, 0, 0, mPaint);
圆角例子:
下面再来看一个稍微复杂点的效果-刮刮卡效果:
/** * Created by Layne_Yao on 2017-11-30 下午5:15:22. * CSDN:http://blog.csdn.net/Jsagacity */ public class XfermodeView extends View { private Bitmap mBgBitmap, mFgBitmap; private Paint mPaint; private Canvas mCanvas; private Path mPath; private Bitmap mBitmap,mOut; public XfermodeView(Context context) { super(context); init(); } public XfermodeView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public XfermodeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(); mPaint.setAlpha(0); mPaint.setXfermode( new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.ROUND); mPaint.setStrokeWidth(50); mPaint.setStrokeCap(Paint.Cap.ROUND); mPath = new Path(); mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mFgBitmap); mCanvas.drawColor(Color.GRAY); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPath.reset(); mPath.moveTo(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(event.getX(), event.getY()); break; } mCanvas.drawPath(mPath, mPaint); invalidate(); return true; } @Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(mBgBitmap, 0, 0, null); canvas.drawBitmap(mFgBitmap, 0, 0, null); } }
运行结果:
Shader:
Shader又被称为着色器、渲染器。它可以实现一系列的渲染,渐变等效果,Android中的Shader包括以下几种:
BitmapShader:位图Shader
LinearGradient:线性Shader
RadialGradient:光束Shader
SweepGradient:梯形Shader
ComposeShader:混合Shader
其中BitmapShader产生的是一个图像,它的作用是通过Paint对话不进行指定Bitmap的填充,填充有三种模式可以选择:
CLAMP拉伸:拉伸的是图片最后的那一个像素,不断重复
REPEAT重复:横向,纵向不断重复
MIRROR镜像:横向不断翻转重复,纵向不断翻转重复
圆形图片:
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mPaint = new Paint(); mPaint.setShader(mBitmapShader); canvas.drawCircle(500, 250, 200, mPaint);
运行结果:
下面把TileMode改为REPEAT:
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mPaint = new Paint(); mPaint.setShader(mBitmapShader); canvas.drawCircle(500, 250, 200, mPaint);
运行结果:
使用LinearGradient:
mPaint = new Paint(); mPaint.setShader(new LinearGradient(0,0,400,400, Color.BLUE,Color.YELLOW, Shader.TileMode.REPEAT)); canvas.drawRect(0,0,400,400,mPaint);
运行结果:
接下来使用LinearGradient和PorterDuffXfermode来创建一个具有倒影下过的图片:
/** * Created by Layne_Yao on 2017-11-30 下午5:12:59. * CSDN:http://blog.csdn.net/Jsagacity */ public class ReflectView extends View { private Bitmap mSrcBitmap, mRefBitmap; private Paint mPaint; private PorterDuffXfermode mXfermode; public ReflectView(Context context) { super(context); initRes(context); } public ReflectView(Context context, AttributeSet attrs) { super(context, attrs); initRes(context); } public ReflectView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initRes(context); } private void initRes(Context context) { mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); Matrix matrix = new Matrix(); matrix.setScale(1F, -1F); mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true); mPaint = new Paint(); mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(), 0, mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 4, 0XDD000000, 0X10000000, Shader.TileMode.CLAMP)); mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); canvas.drawBitmap(mSrcBitmap, 0, 0, null); canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null); mPaint.setXfermode(mXfermode); // 绘制渐变效果矩形 canvas.drawRect(0, mSrcBitmap.getHeight(), mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint); mPaint.setXfermode(null); } }
运行结果:
PathEffect:
要理解PathEffect,先来看一张比较直观的图,来了解一下什么是PathEffect:
PathEffect就是指用各种笔触效果来绘制一个路径,上图中展示的集中绘制PathEffect的方式,从上到下依次是:
没效果
CornerPathEffect:拐弯角变得圆滑
DiscretePathEffect:线段上会产生许多杂点
DashPathEffect:绘制虚线,用一个数据来设置各个点之间的间隔
PathDashPathEffect:绘制虚线,可以使用方形点虚线和圆形点虚线
ComposePathEffect:可任意组合两种路径(PathEffect)的特性
代码演示:
/** * Created by Layne_Yao on 2017-12-1 下午2:01:20. * CSDN:http://blog.csdn.net/Jsagacity */ public class PathEffectView extends View { private Paint mPaint; private Path mPath; private PathEffect[] mEffects; public PathEffectView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(5); mPaint.setColor(Color.DKGRAY); mPath = new Path(); mPath.moveTo(0, 0); for (int i = 0; i <= 30; i++) { mPath.lineTo(i * 35, (float) (Math.random() * 100)); } mEffects = new PathEffect[6]; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mEffects[0] = null; mEffects[1] = new CornerPathEffect(30); mEffects[2] = new DiscretePathEffect(3.0F, 5.0F); mEffects[3] = new DashPathEffect(new float[] { 20, 10, 5, 10 }, 0); Path path = new Path(); path.addRect(0, 0, 8, 8, Path.Direction.CCW); mEffects[4] = new PathDashPathEffect(path, 12, 0, PathDashPathEffect.Style.ROTATE); mEffects[5] = new ComposePathEffect(mEffects[3], mEffects[1]); for (int i = 0; i < mEffects.length; i++) { mPaint.setPathEffect(mEffects[i]); canvas.drawPath(mPath, mPaint); canvas.translate(0, 200); } } }
View之孪生兄弟---SurfaceView
SurfaceView与View是有区别的:View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕的重绘,刷新的间隔时间为16ms。如果在16ms内View完成所有操作,那么用户视觉上就不会觉得卡顿;否则就会不断阻塞主线程,导致画面卡顿,在自定义View的Log中经常会出现下面的警告:
Skipped 47 frames! The application may be doing too much work on its main thread
SurfaceView与View的主要区别:
View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新;
View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新;
View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中就已经实现了双缓冲机制。
总之,如果你的自定义View需要频繁的刷新,或者刷新时数据处理量比较大,那么就使用SurfaceView来代替View。
SurfaceView的使用
创建SurfaceView:创建自定义的SurfaceView继承自SurfaceView,并实现两个接口---SurfaceHolder.Callback和Runnable
初始化SurfaceView:在自定义SurfaceView的构造方法中,需要对SurfaceView进行初始化。在自定义的SurfaceView中,定义相关的成员变量,并注册SurfacHolder的回调方法。
使用SurfaceView:通过SurfacHolder对象lockCanvas()方法获得Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交
使用SurfaceView的模板代码:
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable { //SurfaceHolder private SurfaceHolder mHolder; //用于绘制的Canvas private Canvas mCanvas; //子线程标志位 private boolean mIsDrawing; /** * 构造方法 * * @param context * @param attrs */ public SurfaceViewTemplate(Context context, AttributeSet attrs) { super(context, attrs); mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); setKeepScreenOn(true); } @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { while (mIsDrawing) { draw(); } } private void draw() { try { mCanvas = mHolder.lockCanvas(); } catch (Exception e) { } finally { if (mCanvas != null) { //提交 mHolder.unlockCanvasAndPost(mCanvas); } } } }
以上代码基本上可以满足大部分的SurfaceView绘图需求,唯一需要注意的是在绘制方法中,将mHolder.unlockCanvasAndPost(mCanvas)方法放到代码快中,来保证每次都能将内容提交。
SurfaceView的实例:
正弦曲线:
/** * Created by Layne_Yao on 2017-12-1 下午3:13:32. * CSDN:http://blog.csdn.net/Jsagacity */ public class SinView extends SurfaceView implements SurfaceHolder.Callback, Runnable { private SurfaceHolder mHolder; private Canvas mCanvas; private boolean mIsDrawing; private int x = 0; private int y = 0; private Path mPath; private Paint mPaint; public SinView(Context context) { super(context); initView(); } public SinView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public SinView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } private void initView() { mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); mPath = new Path(); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); } @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; mPath.moveTo(0, 400); new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { while (mIsDrawing) { draw(); x += 1; y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400); mPath.lineTo(x, y); } } private void draw() { try { mCanvas = mHolder.lockCanvas(); // SurfaceView背景 mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath, mPaint); } catch (Exception e) { } finally { if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas); } } }
运行结果:
绘画板:
/** * Created by Layne_Yao on 2017-12-1 下午3:14:02. * CSDN:http://blog.csdn.net/Jsagacity */ public class SimpleDraw extends SurfaceView implements SurfaceHolder.Callback, Runnable { private SurfaceHolder mHolder; private Canvas mCanvas; private boolean mIsDrawing; private Path mPath; private Paint mPaint; public SimpleDraw(Context context) { super(context); initView(); } public SimpleDraw(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public SimpleDraw(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } private void initView() { mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); mPath = new Path(); mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(40); } @Override public void surfaceCreated(SurfaceHolder holder) { mIsDrawing = true; new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsDrawing = false; } @Override public void run() { long start = System.currentTimeMillis(); while (mIsDrawing) { draw(); } long end = System.currentTimeMillis(); // 50 - 100 if (end - start < 100) { try { Thread.sleep(100 - (end - start)); } catch (InterruptedException e) { e.printStackTrace(); } } } private void draw() { try { mCanvas = mHolder.lockCanvas(); mCanvas.drawColor(Color.WHITE); mCanvas.drawPath(mPath, mPaint); } catch (Exception e) { } finally { if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas); } } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPath.moveTo(x, y); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(x, y); break; case MotionEvent.ACTION_UP: break; } return true; } }
运行结果:
源码下载
相关文章推荐
- Android进阶-View系列(三)-Android绘图机制与处理技巧(上)
- Android绘图机制与处理技巧(五)——View的孪生兄弟SurfaceView
- Android群英传读书笔记第六章(Android绘图机制与处理技巧)
- 《Android群英传》读书笔记6.Android绘图机制与处理技巧
- Android绘图机制与处理技巧
- Android绘图机制与处理技巧(二)Android绘图技巧
- Android绘图机制与处理技巧(二)——Android图像处理之色彩特效处理
- Android绘图机制与处理技巧(六)Android图像处理之SurfaceView
- Android群英传学习——第六章、Android绘图机制与处理技巧
- Android群英传知识点回顾——第六章:Android绘图机制与处理技巧
- Android群英传》读书笔记 (3) 第六章 Android绘图机制与处理技巧 + 第七章 Android动画机制与使用技巧
- Android绘图机制与处理技巧(三)Android图像处理之色彩特效处理
- Android群英传笔记——第六章:Android绘图机制与处理技巧
- Android绘图机制与处理技巧(四)Android图像处理之图形特效处理
- Android 绘图机制与处理技巧-1
- 《Android群英传》读书笔记(6)第六章:Android绘图机制与处理技巧之二
- 第6章 Android绘图机制与处理技巧(Part 1)
- Android绘图机制与处理技巧(三)——Android图像处理之图形特效处理
- Android群英传之Android绘图机制与处理技巧
- Android绘图机制及处理技巧