Android群英传学习——第六章、Android绘图机制与处理技巧
来源:互联网 发布:淘宝图片分辨率 编辑:程序博客网 时间:2024/06/05 11:40
本章内容主要有:
Android屏幕相关知识 Android绘图技巧 Android图像处理技巧 SurfaceView的使用
一、屏幕的尺寸信息
1、屏幕参数
一个屏幕通常具有以下几个参数。
屏幕大小
指屏幕对角线的长度,通常使用“寸”来度量,例如4.7寸手机。
分辨率
分辨率是指手机屏幕的像素点个数,例如720*1280就是指屏幕的分辨率,指宽有720个像素点,而高有1280个像素点。
PPI
每英寸像素(Picels Per Inch)又被称为DPI(Dots Per Inch)。它是由对角线的像素点除以屏幕的大小得到的,同样的分辨率,屏幕越大,像素点之间的距离越大,屏幕就越粗糙。实践证明,PPI低于240的让人的视觉可以察觉明显颗粒感,高于300则无法察觉颗粒,通常达到400PPI就已经是非常高的屏幕密度了。
2、系统屏幕密度
每个厂商的Android手机具有不同的大小尺寸和像素密度的屏幕。系统定义了几个标准的DPI值,作为手机固定的DPI:
3、独立像素密度dp
由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同。Android系统使用mdpi即密度值为160的屏幕作为标准,在这个屏幕上1px=1dp。其它屏幕都可以据此进行换算。比如,同样100dp的长度,在mdpi中为100px,在hdpi中为150px。由此,我们可以得到各个分辨率直接的换算比例:
ldpi : mdpi : hdpi : xhdpi : xxhdpi = 3 : 4 : 6 : 8 : 12
4、单位转换
在程序中对单位进行转化,可以直接使用如下代码,当做工具类保存到项目中:
/** *dp、sp转换为px的工具类 */public class DisplayUtil { /** * 将px值转换为dip或dp值,,保证尺寸大小不变 * @param context * @param pxValue * DisplayMetrics类中属性density * @return */ public static int px2dip(Context context, float pxValue){ final float scale = context.getResources().getDisplayMetrics().density; return (int)(pxValue/scale + 0.5f); } /** * 将dip或dp值转换为px值,保证尺寸大小不变 * @param context * @param dipValue * DisplayMetrics类中属性density * @return */ public static int dip2px(Context context,float dipValue){ final float scale = context.getResources().getDisplayMetrics().density; return (int)(dipValue * scale + 0.5f); } /** * 将px值转换为sp值,保证文字大小不变 * @param context * @param pxValue * DisplayMetrics类中属性density * @return */ public static int px2sp(Context context,float pxValue){ final float fontScale = context.getResources().getDisplayMetrics().density; return (int)(pxValue/fontScale + 0.5f); } /** * 将sp值转换为px值,保证文字大小不变 * @param context * @param spValue * @return */ public static int sp2px(Context context,float spValue){ final float fontScale = context.getResources().getDisplayMetrics().density; return (int)(spValue/fontScale + 0.5f); }}
其中density就是前面所说的换算比例。这里使用的是公式换算方法进行转换。同时,系统也提供了TypedValue类帮助转换:
/** * dp2px * @param dp * @return */ protected int dp2px(int dp){ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics()); } /** * sp2px * @param sp * @return */ protected int sp2px(int sp){ return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics()); }
二、2D绘图基础
系统通过提供的Canvas对象来提供绘图方法。
它提供了各种绘制图像的API,如drawPoint(点)、drawLine(线)、drawRect(矩形)、drawVertices(多边形)、drawArc(弧)、drawCircle(圆)等等。
要绘制图形,首先要定义我们的画笔Paint,下面列举了它的一些属性和对应的功能:
* 1.图形绘制 * setARGB(int a,int r,int g,int b); * 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。 * * setAlpha(int a); * 设置绘制图形的透明度。 * * setColor(int color); * 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。 * * setAntiAlias(boolean aa); * 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。 * * setDither(boolean dither); * 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰 * * setFilterBitmap(boolean filter); * 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示 * 速度,本设置项依赖于dither和xfermode的设置 * * setMaskFilter(MaskFilter maskfilter); * 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等 * * setColorFilter(ColorFilter colorfilter); * 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果 * * setPathEffect(PathEffect effect); * 设置绘制路径的效果,如点画线等 * * setShader(Shader shader); * 设置图像效果,使用Shader可以绘制出各种渐变效果 * * setShadowLayer(float radius ,float dx,float dy,int color); * 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色 * * setStyle(Paint.Style style); * 设置画笔的样式,为FILL(实心),FILL_OR_STROKE,或STROKE (空心) * * setStrokeCap(Paint.Cap cap); * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式 * Cap.ROUND,或方形样式Cap.SQUARE * * setSrokeJoin(Paint.Join join); * 设置绘制时各图形的结合方式,如平滑效果等 * * setStrokeWidth(float width); * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度 * * setXfermode(Xfermode xfermode); * 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果 * *2.文本绘制 * setFakeBoldText(boolean fakeBoldText); * 模拟实现粗体文字,设置在小字体上效果会非常差 * * setSubpixelText(boolean subpixelText); * 设置该项为true,将有助于文本在LCD屏幕上的显示效果 * * setTextAlign(Paint.Align align); * 设置绘制文字的对齐方向 * * setTextScaleX(float scaleX); * 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果 * * setTextSize(float textSize); * 设置绘制文字的字号大小 * * setTextSkewX(float skewX); * 设置斜体文字,skewX为倾斜弧度 * * setTypeface(Typeface typeface); * 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等 * * setUnderlineText(boolean underlineText); * 设置带有下划线的文字效果 * * setStrikeThruText(boolean strikeThruText); * 设置带有删除线的效果 * **/
下面重点来看一下Canvas家族的各个成员们:
1)DrawPoint,绘制点
canvas.drawPoint(x,y,mPaint);
2)DrawLine,绘制直线
canvas.drawLine(starX,starY,endX,endY,mPaint);
3)DrawLines,绘制多条直线
float[] pts = {startX1,startY1,endX1,endY1,...startXn,startYn,endXn,endY};canvas.drawLines(pts,mPaint);
4)DrawRect,绘制矩形
canvas.drawRect(left,top,right,bottom,mPaint);
5)DrawRoundRect,绘制圆角矩形
canvas.drawRoundRect(left,top,right,bottom,radiusX,radiusY,mPaint);
6)DrawCircle,绘制圆
canvas.drawCircle(circleX,circleY,radius,mPaint);
7)DrawArc,绘制弧形、扇形
mPaint.setStyle(Paint.Style.STROKE);canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,useCenter,mPaint);
这里注意,弧形与扇形的区分就是倒数第二个参数useCenter的区别,useCenter设为true绘制的是扇形,设为false绘制的是弧形。
8)DrawOval,绘制椭圆
//通过椭圆的外接矩形来绘制椭圆canvas.drawOval(left,top,right,bottom,mPaint);
9)DrawText,绘制文本
canvas.drawText(text,startX,startY,mPaint);
10)DrawPosText,在指定位置绘制文本
canvas,drawPosText(text,new float[]{X1,Y1,X2,Y2,...Xn,Yn},mPaint);
11)DrawPath,绘制路径
Path path = new Path();path.moveTo(startX,startY);path.lineTo(point1X,point1Y);path.lineTo(point2X,point2Y);path.lintTo(point3X,point3Y);canvas.drawPath(path,mPaint);
三、Android XML绘图
XML在Android系统中不仅仅是一个布局文件、配置列表。它甚至可以变成一张画、一副图。
1、Bitmap
在XML中使用Bitmap十分简单,代码如下:
<?xml version="1.0" encoding="utf-8"?><bitmap xmlns:android="http://schemas.android.com/apk/res/android" android:src = "@drawable/ic_launcher"/>
通过这样引用图片,就可以将图片之间转成了Bitmap让我们在程序中使用。
2、Shape
<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android" <!--rectangle:矩形,填满整个包裹的控件,默认值--> <!--oval:椭圆,会根据控件的尺寸自适应--> <!--line:贯穿控件的横线,需要<stroke>标签来定义横线的宽度--> <!--ring:环形--> android:shape=["rectangle" | "oval" | "line" | "ring"] > <corners<!-- 只有在shape为rectangle时使用,以下参数取值必须大于1--> android:radius="统一四个圆角设置,这个可以被以下任何一个覆盖对应的角落做单独角落处理" android:topLeftRadius="integer" android:topRightRadius="integer" android:bottomLeftRadius="integer" android:bottomRightRadius="integer" /> <gradient <!--渐变 --> android:angle="渐变方向,0为从左至右,90为从下至上,逆时针方向旋转," android:centerX="渐变色中心的X相对位置(0-1.0)" android:centerY="渐变色中心的Y相对位置(0-1.0),还不是很理解,当渐变方向为竖直方向时,该值设定渐变中心的位置" android:centerColor="integer" android:endColor="color" android:gradientRadius="渐变色的半径 当type为radial时使用,调大些明显" android:startColor="color" android:type=["linear线性渐变,默认值" | "radial放射渐变,start color is the center color" | "sweep 扫线"] android:usesLevel=["true" | "false"] /> <padding <!--控件内距 四周留出来的空白 --> android:left="integer" android:top="integer" android:right="integer" android:bottom="integer" /> <size android:width="integer" android:height="integer" /> <solid <!--填充--> android:color="color" /> <stroke <!-- shape 为line时是贯穿控件的线条,非line时用来描边--> android:width="线条厚度" android:color="color" android:dashWidth="实线宽度" android:dashGap="虚线宽度" /></shape>
3、Layer
在Android中可以通过Layer来实现类似Photoshop中图层的概念。
下面我们通过使用layer、layer-list是想图片叠加效果:
在res-drawable目录下新建xml文件:
<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/ic_launcher"/> <item android:drawable="@drawable/ic_launcher" android:left="20.0dp" android:top="20.0dp" android:right="20.0dp" android:bottom="10.0dp"/></layer-list>
效果如图
4、Selector
Selector的作用在于帮助开发者实现静态绘图中的事件反馈,通过给不同的事件设置不同的图像,从而在程序中根据用户活动,返回不同的结果。
这一方法可以帮助开发者迅速制作View的触摸反馈。
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <!--默认时的背景图片--> <item android:drawable="@drawable/D1"/> <!--没有焦点时的背景图片--> <item android:state_window_focused="false" android:drawable="@drawable/D2"/> <!--非触摸模式下获得焦点并单击时的背景图片--> <item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/D3"/> <!--触摸模式下单击时的背景图片--> <item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/D4"/> <!--选中时的图片背景--> <item android:state_selected="true" android:drawable="@drawable/D5"/> <!--获得焦点时的背景图片--> <item android:state_focused="true" android:drawable="@drawable/D6"/></selector>
下面实现一个圆角矩形点击后换背景颜色的效果:
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape android:shape = "rectangle"> <!--填充的颜色--> <solid android:color = "#000"/> <!--设置按钮的四个角为弧形--> <!--android:radius 弧形的半径--> <corners android:radius = "5dip"/> <!--Button里面的文字与Button边界的间隔--> <padding android:bottom = "10dp" android:left = "10dp" android:right = "10dp" android:top = "10dp"/> </shape> </item> <item> <shape android:shape = "rectangle"> <!--填充的颜色--> <solid android:color = "#6769"/> <!--设置按钮的四个角为弧形--> <!--android:radius 弧形的半径--> <corners android:radius = "5dip"/> <!--Button里面的文字与Button边界的间隔--> <padding android:bottom = "10dp" android:left = "10dp" android:right = "10dp" android:top = "10dp"/> </shape> </item></selector>
四、Android绘图技巧
1、Layer图层
Android中绘图API,很大程度上都来自于现实生活中的绘图工具,特别是Photoshop中的概念,比如图层。一张画可以有很多图层叠加起来,形成一个复杂的图像。
在Android中,使用setLayer()方法来创建一个图层。
图层同样是基于栈的结构进行管理的。
Android通过调用saveLayer()方法、saveLayerAlpha()方法将一个图层入栈。 使用restore()方法、restoreToCount()方法将一个图层出栈。
入栈的时候,后面所有的操作都发生在这个图层上,出栈的时候,会把图像绘制到上层Canvas上。
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE); mPaint.setColor(Color.BLUE); canvas.drawCircle(150,150,100,mPaint); canvas.saveLayerAlpha(0,0,400,400.127,LAYER_FLAGS); mPaint.setColor(Color.RED); canvas.drawCircle(200,200,100,mPaint); canvas.restore(); }
本例中绘制了两个相交的圆,这两个圆位于两个图层上。
将后面的图层透明度设置0-255不同的数值:当透明度Wie0时,即完全透明;当透明度为127时,即半透明;当透明度为255时,即完全不透明。
2、Canvas
Canvas对象除了可以直接绘制图形外,也可以对图层进行操作,主要有以下几个方法:
●canvas.save();
●canvas.restore();
●canvas.translate();
●canvas.rotate();
1)canvas.save()方法,从字面上可以理解为保存画布。它的作用就是将之前的所有已绘制的图像保存起来,让后续的操作好像就在一个新的图层上操作一样。
2)canvas.restore()方法,可以理解为将我们在save()之后绘制的所有图像与save()之前的图像进行合并。
3)canvas.translate(x,y)方法,可以理解为画布平移,默认绘图坐标零点位于屏幕左上角,那么调用这个方法后,原件就从(0,0)移动到了(x,y)。
4)canvas.rotate()方法与translate()方法相似,它将坐标系旋转了一定的角度。
下面我们做一个仪表盘,来加深一下对于上述几个方法的印象。
先看效果图:
这样一个图形,我们可以将它分解成以下几个元素:
1)仪表盘——外面的大圆盘2)刻度线——包含四个长的刻度线和其他短的刻度线3)刻度值——包含长刻度线对应的大的刻度值和其他小的刻度值4)指针——中间的指针、一粗一细两根指针
那我们就一个一个来画,之间看代码吧:
public class YiBiao extends View { private Paint paintCircle,paintDegree,paintHour,paintMinute; private int mHeight,mWidth; public YiBiao(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { //获取屏幕高宽 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); mWidth = wm.getDefaultDisplay().getWidth(); mHeight = wm.getDefaultDisplay().getHeight(); //圆盘画笔 paintCircle = new Paint(); paintCircle.setColor(Color.BLACK); paintCircle.setStrokeWidth(5); paintCircle.setStyle(Paint.Style.STROKE); paintCircle.setAntiAlias(true); //刻度线画笔 paintDegree = new Paint(); paintDegree.setStrokeWidth(3); //指针画笔 paintHour = new Paint(); paintHour.setStrokeWidth(20); paintMinute = new Paint(); paintMinute.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { canvas.drawCircle(mWidth/2,mHeight/2,mWidth/2,paintCircle); for(int i = 0;i < 24; i++){ //区分整点与非整点 if(i == 0 || i == 6 || i == 12 || i == 18 ) { paintDegree.setStrokeWidth(5); paintDegree.setTextSize(30); canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2, mWidth / 2, mHeight / 2 - mWidth / 2 + 60, paintDegree); String degree = String.valueOf(i); canvas.drawText(degree, mWidth / 2 - paintDegree.measureText(degree) / 2, mHeight / 2 - mWidth / 2 + 90, paintDegree); }else{ paintDegree.setStrokeWidth(3); paintDegree.setTextSize(15); canvas.drawLine(mWidth/2,mHeight/2-mWidth/2,mWidth/2,mHeight/2-mWidth/2+30,paintDegree); String degree = String.valueOf(i); canvas.drawText(degree, mWidth / 2 - paintDegree.measureText(degree) / 2, mHeight / 2 - mWidth / 2 + 60, paintDegree); } //通过旋转画布简化坐标运算 canvas.rotate(15,mWidth/2,mHeight/2); } //将保存前的图层与保存后的图存合并 canvas.save(); //将坐标原点移动到圆心的位置 canvas.translate(mWidth/2,mHeight/2); canvas.drawLine(0,0,100,100,paintHour); canvas.drawLine(0,0,100,200,paintMinute); }}
啊~!困疯了,实在写不了啦。
五、Android图像处理之色彩特效处理
Android对于图片的处理,最常使用到的数据结构是位图——Bitmap,它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、绿、蓝这四个通道分量,他们共同决定了每个像素点显示的颜色。
色光三原色
1、色彩矩阵分析
在色彩处理中,通常使用以下三个角度来描述一个图像。
● 色调——物体传播的颜色
● 饱和度——颜色的纯度,从0(灰)到100%(饱和)来进行描述
● 亮度——颜色的相对明暗程度
在Android中,系统使用一个颜色矩阵——ColorMatrix,来处理图像的这些色彩效果。这个颜色矩阵是一个4*5的数字矩阵,它以一维数组的形式来存储,如图中矩阵A。而对于每个像素点,都有一个颜色分量矩阵用来保存颜色的RGBA值,如图中矩阵C。在处理图像时,使用矩阵乘法运算AC来处理颜色分量矩阵。
即:
从这个公式可以发现,矩阵A中的4*5颜色矩阵是按一下方式划分的:
● 第一行的abcde值用来决定新的颜色值中的R——红色● 第二行的fghij值用来决定新的颜色值中的G——绿色● 第三行的klmno值用来决定新的颜色值中的B——蓝色● 第四行的pqrst值用来决定新的颜色值中的A——透明度● 矩阵A中的第五列——ejot值分别用来决定每个分量中的offset,即偏移量
这样划分好各自的势力范围之后,这些值的作用就比较明确了。当我们要变换颜色值的时候,通常有两种办法,一个是直接改变颜色的offset,即偏移量的值来修改颜色分量,另一个方法是直接改变对应RGBA值的系数来调整颜色分量的值。
1)改变偏移量
从前面的分析中,可以知道要修改R1的值,只需要将第五列的值进行修改即可,即改变颜色的偏移量,其他值保存初始矩阵的值。
在这个矩阵中,我们修改了R、G对应的颜色偏移量,所以最后的处理结构就是图像中的红色、绿色分量增加了100。 红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图像的色调偏黄色。
2)改变颜色系数
如果修改颜色分量中的某个系数值,而其他值依然保存初始矩阵的值
这个矩阵改变了G分量对应的系数g,这样在矩阵运算后G分量会变为以前的两倍,最终效果就是图像的色调更加偏绿。
3)改变色光属性
图像的色调、饱和度、亮度这三个属性在图像处理中的使用非常多。因此,在颜色矩阵中,也封装了一些API来快速调整这些参数,而不用每次都去计算矩阵的值。
ColorMatrix即颜色矩阵,可以很方便的通过改变矩阵值来处理颜色效果。创建一个ColorMatrix对象非常简单,代码如下:
ColorMatrix colorMatrix = new 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);
通过这样的方法,可以为RGB三种颜色分量分别重新设置了不同的色调值。
● 饱和度
Android系统提供了setSaturation(float sat)方法来设置颜色的饱和度,参数即代表设置颜色饱和度的值:
ColorMatrix saturationMatrix = new ColorMatrix(); saturationMatrix.setSaturation(saturation);
● 亮度
当三原色以相同的比例进行混合的时候,就会显示出白色。系统也正是使用这个原理来改变一个图像的亮度的:
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来改变不同的数值,并将这些数值作用到对应的矩阵中。最后通过postConcat()方法来显示混合后的处理效果:
布局代码:
<?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/mImage" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="4" /> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="色调:" android:textColor="#000" android:textSize="23dp" /> <SeekBar android:id="@+id/seekbarHue" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="饱和度:" android:textColor="#000" android:textSize="23dp" /> <SeekBar android:id="@+id/seekbarSaturation" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="亮度:" android:textColor="#000" android:textSize="23dp" /> <SeekBar android:id="@+id/seekbarLum" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" /> </LinearLayout> <Button android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:padding="10dp" android:text="原图" android:textSize="25sp" /></LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener, View.OnClickListener { private ImageView mImage; private SeekBar seekbarHue, seekbarSaturation, seekbarLum; private Button btn; Bitmap bitmap; float mHue, mSaturation, mLum; //SeekBar的中间值 int MID_VALUE = 127; //SeekBar的最大值 int MAX_VALUE = 255; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { mImage = (ImageView) findViewById(R.id.mImage); mImage.setImageResource(R.drawable.image01); //调节色调 seekbarHue = (SeekBar) findViewById(R.id.seekbarHue); //调节饱和度 seekbarSaturation = (SeekBar) findViewById(R.id.seekbarSaturation); //调节亮度 seekbarLum = (SeekBar) findViewById(R.id.seekbarLum); seekbarHue.setOnSeekBarChangeListener(this); seekbarHue.setMax(MAX_VALUE); seekbarHue.setProgress(MID_VALUE); seekbarSaturation.setOnSeekBarChangeListener(this); seekbarSaturation.setMax(MAX_VALUE); seekbarSaturation.setProgress(MID_VALUE); seekbarLum.setOnSeekBarChangeListener(this); seekbarLum.setMax(MAX_VALUE); seekbarLum.setProgress(MID_VALUE); //恢复原图按钮 btn = (Button) findViewById(R.id.btn); btn.setOnClickListener(this); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { switch (seekBar.getId()) { case R.id.seekbarHue: mHue =progress * 1.0F / MID_VALUE; break; case R.id.seekbarSaturation: mSaturation = progress * 1.0F / MID_VALUE; break; case R.id.seekbarLum: mLum = progress * 1.0F / MID_VALUE; //很多人运用如下公式,但是我用了以后发现效果并不好呀! //mLum = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180; break; } mImage.setImageBitmap(handleImageEffect(mHue, mSaturation, mLum)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } public Bitmap handleImageEffect(float hue, float saturation, float lum) { //Android不允许直接修改原图 //必须通过原图创建一个同样大小的Bitmap,并将原图绘制到该Bitmap中,以一个副本的形式来修改图像 //代码中bitmap为原图 //bmp为创建的副本 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image01); // Bitmap bmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888); Bitmap bmp = bitmap.copy(Bitmap.Config.ARGB_8888,true); Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); /** * 设置色调 * int axis:RGB标志,0 代表 RED, 1 代表 GREEN, 2 代表BLUE * 如果需要分别设置RGB的话,就需要调用三次,传入不同的 axis值 * * float degree: 控制具体的颜色,这里取值范围并非0-255,系统用了角度计算颜色值, * 所以取值的时候是以0-360为颜色取值范围,超出此范围,呈周期性变化 */ ColorMatrix hueMatrix = new ColorMatrix(); hueMatrix.setRotate(0, hue); hueMatrix.setRotate(1, hue); hueMatrix.setRotate(2, hue); /** * 设置饱和度 * float set:取值范围未知, * 0 为灰度图,纯黑白, 1 为与原图一样,但是取值可以更大 */ ColorMatrix saturationMatrix = new ColorMatrix(); saturationMatrix.setSaturation(saturation); /** * 设置亮度 * 原理是光的三原色同比例混合最终效果为白色,因此在在亮度上将 * RGB的值等比例混合,值给到足够大时,就会变成纯白效果, * 同样,没有亮度的时候就是黑色 * float rScale:红 * float gScale:绿 * float bScale:蓝 * float aScale:透明度 * 取值范围未知,0时为纯黑,但是1时不一定纯白 */ ColorMatrix lumMatrix = new ColorMatrix(); lumMatrix.setScale(lum, lum, lum, 1); /** * postConcat(ColorMatrix colorMatrix) * 将多个ColorMatrix效果混合 * 之前试过将饱和度,亮度,色调设置到同一个ColorMatrix对象里面, * 从而可以不使用postConcat()方法混合多个ColorMatrix对象, * 但是色调和亮度设置会失效,原因还没研究 */ ColorMatrix imageMatrix = new ColorMatrix(); imageMatrix.postConcat(hueMatrix); imageMatrix.postConcat(saturationMatrix); imageMatrix.postConcat(lumMatrix); //这里需要注意的是,在设置号颜色矩阵 // 通过使用Paint类的setColorFilter()方法,将通过imageMatrix构造的ColorMatrixColorFilter //对象传递进去,并使用这个画笔来绘制原来的图像,从而将颜色矩阵作用到原图中。 paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix)); canvas.drawBitmap(bmp, 0, 0, paint); return bmp; } @Override public void onClick(View view) { //恢复中间值 seekbarHue.setProgress(MID_VALUE); seekbarSaturation.setProgress(MID_VALUE); seekbarLum.setProgress(MID_VALUE); }}
2、Android颜色矩阵——ColorMatrix
通过前面的分析,我们知道了调整颜色矩阵可以改变一幅图像的色彩效果,图像处理很大程度上就是在寻找图像的颜色矩阵。不仅仅可以通过Android系统提供的API来进行ColorMatrix的修改,同样可以精确地修改矩阵的值来实现颜色效果的处理。
下面我们模拟一个4*5的颜色矩阵。
改变颜色偏移量
改变颜色系数
布局代码
<?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="match_parent" android:layout_height="0dp" android:layout_weight="3" /> <GridLayout android:id="@+id/mGroup" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:columnCount="5" android:rowCount="4"> </GridLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1.5" android:orientation="vertical"> <Button android:id="@+id/btn_change" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:padding="10dp" android:text="Change" android:onClick="btnChange" android:textSize="20sp" /> <Button android:id="@+id/btn_reset" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:padding="10dp" android:onClick="btnReset" android:text="Reset" android:textSize="20sp" /> </LinearLayout></LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity { private Bitmap bitmap; private ImageView mImageView; private GridLayout mGroup; private EditText [] mEts = new EditText[20]; private int mEtWidth,mEtHeight; private float[] mColorMatrix = new float[20]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.image01); mImageView = (ImageView) findViewById(R.id.imageView); mGroup = (GridLayout) findViewById(R.id.mGroup); mImageView.setImageBitmap(bitmap); mGroup.post(new Runnable() { @Override public void run() { //获取宽高信息 mEtWidth = mGroup.getWidth()/5; mEtHeight = mGroup.getHeight()/4; addEts(); initMatrix(); } }); } //添加EditText private void addEts(){ for(int i = 0;i < 20;i++){ EditText editText = new EditText(MainActivity.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)); } } } //获取矩阵值 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(); }}
3、常用图像颜色矩阵处理效果
这一部分展现一些比较经典、常用的颜色处理效果对应的颜色矩阵。
1)灰度效果
2)图像反转
3)怀旧效果
4)去色效果
5)高饱和度
4、像素点分析
作为更加精确的图像处理方式,可以通过改变每个像素点的具体ARGB值,达到处理一张图片效果的目的,这里要注意的是,传递进来的原始图片是不能修改的,一般根据原始图片生成一张新的图片来修改
在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);
当获取到具体的颜色值后,就可以通过相应的算法去修改这个ARGB值了,从而重构一张图片,当然,这些算法是前辈们研究的,总结出来的图像处理方法,由于我们不是专业的图像处理人员,所以就直接拿来用了
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);
再通过如下代码将新的RGBA值合成像素点:
newPx[i] = Color.argb(a, r1, b1, g1);
最后将处理后的像素点重新设置成新的bitmap:
bmp.setPixels(newPx, 0, width, 0, 0, width, height);
5、常用图像像素点处理效果
1)底片效果
若存在A,B,C三个像素点,要求B点对应的底片效果算法:
B.r = 255 - B.r;B.g = 255 - B.g;B.b = 255 - B.b;
实现代码如下:
/** * 底片效果 * * @param bm * @return */ public Bitmap handleImageNegative(Bitmap bm) { int width = bm.getWidth(); int height = bm.getHeight(); int color; int r, g, b, a; Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); int[] oldPx = new int[width * height]; int[] newPx = new int[width * height]; bm.getPixels(oldPx, 0, width, 0, 0, width, height); for (int i = 0; i < width * height; i++) { color = oldPx[i]; r = Color.red(color); g = Color.green(color); b = Color.blue(color); a = Color.alpha(color); r = 255 - r; g = 255 - g; b = 255 - b; if (r > 255) { r = 255; } else if (r < 0) { r = 0; } if (g > 255) { g = 255; } else if (g < 0) { g = 0; } if (b > 255) { b = 255; } else if (b < 0) { b = 0; } newPx[i] = Color.argb(a, r, g, b); } bmp.setPixels(newPx, 0, width, 0, 0, width, height); return bmp; }
2)老照片效果
r = (int) (0.393 * r + 0.769 * g + 0.189 * b); g = (int) (0.349 * r + 0.686 * g + 0.168 * b); b = (int) (0.272 * r + 0.534 * g + 0.131 * b);
3)浮雕效果
要求某像素点的对应的浮雕效果算法:
B.r = C.r - B.r + 127;B.g = C.g - B.g + 127;B.b = C.b - B.b + 127;
六、Android图像处理之图形特效处理
前面我们了解了关于图像色彩处理的相关技巧,下面继续学习图形图像方面的处理技巧。
1、Android变形矩阵——Matrix
对于图像的图形变换,Android系统也是通过矩阵来进行处理的,每个像素点都表达了其坐标的X、Y信息。Android的图形变换矩阵是一个3*3的矩阵。如图:
当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样,计算公式如下所示:
X1 = a*X+b*Y+cY1 = d(X+e*Y+f1 = g*X+h*Y+i
通常情况下,会让g=h=0,i=1,这样使1 = g*X+h*Y+i恒成立。因此,只需要着重关注上面几个参数就可以了。与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵:
图像的变形处理通常包含以下四类基本变换:
● Translate——平移变换
● Rotate ——旋转变换
● Scale——缩放变换
● Skew——错切变换
1)平移变换
平移变换的坐标值变换过程如图,即将每个像素点都进行平移变换:
当从p(x0,y0)平移到p(x,y)时,坐标值发生了如下所示的变换:
X = X0 + △XY = Y0 + △Y
这也就是前面所说的实现平移过程的平移公式。
2)旋转变换
旋转变换即指一个点围绕一个中心旋转到一个新的点,如图:
当从P(x0,y0)点,以坐标原点为旋转中心旋转到P(x,y)点时,可以将点的坐标都表达成OP与X轴正方向夹角的函数表达式:
通过计算,可以还原以上等式,下图所示矩阵也就是旋转变换矩阵。
前面是以坐标原点为旋转中心的旋转变换,如果以任意一点O为旋转中心来进行旋转变换,通常需要以下三个步骤:
● 将坐标原点平移到O点。● 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换● 将左边原点还原
通过以上三个步骤,实现了以任意点为旋转中心的旋转变换。
3)缩放变换
一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果,缩放效果的计算公式如下:
x = K1 * x0;y = K2 * y0;
写成矩阵形式,如下图:
通过计算就可以还原到以上等式。
4)错切变换
错切变换(skew)在数学上又称为Shear mapping(剪切变换)或者Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的X坐标(或者Y坐标)保持不变,而对应的Y坐标(或者X坐标)则按比例发生平移,且平移的大小和该点到X轴(或Y轴)的垂直距离成正比。
错切变换通常包含两种——水平错切与垂直错切:
错切变换的计算公式如下:
x = x0 + K1 * y0;y = K2 * x0 + y0;
可以发现,矩阵中的a,b,c,d,e,f这六个矩阵元素分别对应以下变换:
● a和e控制Scale——缩放变换● b和d控制Skew——错切变换● c和f控制Trans——平移变换● a,b,d,e共同控制Rotate——旋转变换
了解了矩阵变换规律后,通过类似色彩矩阵中模拟矩阵的例子来模拟一下变形矩阵。同样通过一个一维数组来模拟矩阵,并通过setValues()方法将一个一维数组转换为图形变换矩阵:
private float [] mImageMatrix = new float[9];Matrix matrix = new Matrix();matrix.setValues(mImageMatrix)
得到了变换矩阵后,就可以通过以下代码将一个图像以这个变换矩阵的形式绘制出来。
canvas.drawBitmap(mBitmmap,matrix,null);
与色彩矩阵一样,Android系统同样提供了一些API来简化矩阵的运算,它使用Matrix类来封装矩阵,并提供了以下几个操作方法来实现上面的四种变换方式。
● matriX.setRoatate()——旋转变换● matriX.setTranslate()——平移变换● matriX.setScale()——缩放变换● matriX.setSkew()——错切变换● pre()和post()——提供矩阵的前乘和后乘运算
我们和上一篇色彩处理的例子一样,做一个图形矩阵,直观的看到图像变换的原理:
方便起见,只放一个平移的效果:
布局代码:
<?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="match_parent" android:layout_height="0dp" android:layout_weight="3" /> <GridLayout android:id="@+id/mGroup" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:columnCount="3" android:rowCount="3"> </GridLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <Button android:id="@+id/btn_change" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="10dp" android:text="Change" android:onClick="btnChange" android:textSize="20sp" /> <Button android:id="@+id/btn_reset" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:padding="10dp" android:onClick="btnReset" android:text="Reset" android:textSize="20sp" /> </LinearLayout></LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity { private Bitmap bitmap; private ImageView mImageView; private GridLayout mGroup; private EditText[] mEts = new EditText[9]; private int mEtWidth,mEtHeight; private float[] mImageMatrix = new float[9]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher); mImageView = (ImageView) findViewById(R.id.imageView); mGroup = (GridLayout) findViewById(R.id.mGroup); mImageView.setImageBitmap(bitmap); mGroup.post(new Runnable() { @Override public void run() { //获取宽高信息 mEtWidth = mGroup.getWidth()/3; mEtHeight = mGroup.getHeight()/3; addEts(); initMatrix(); } }); } private void initMatrix() { for(int i= 0;i<9;i++){ if(i % 4 ==0 ) { mEts[i].setText(String.valueOf(1)); }else{ mEts[i].setText(String.valueOf(0)); } } } private void addEts() { for(int i = 0;i < 9;i++){ EditText editText = new EditText(MainActivity.this); mEts[i] = editText; mGroup.addView(editText,mEtWidth,mEtHeight); } } //获取矩阵值 private void getMatrix(){ for(int i = 0 ;i < 9;i++){ mImageMatrix[i] = Float.valueOf(mEts[i].getText().toString()); } } //将矩阵值设置到图像 private void setImageMatrix(){ Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888); Matrix mMatrix = new Matrix(); mMatrix.setValues(mImageMatrix); Canvas canvas = new Canvas(bmp); canvas.drawBitmap(bitmap, mMatrix, null); mImageView.setImageBitmap(bmp); } /** * 作用点击事件 */ public void btnChange(View view) { getMatrix(); setImageMatrix(); } /** * 重置矩阵效果 */ public void btnReset(View view) { initMatrix(); getMatrix(); setImageMatrix(); }}
2、像素块分析
在进行图像的特效处理时有两种方式,即前面讲的使用矩阵来进行图像变换和我们马上要学习的drawBitmapMesh()方法来进行处理。drawBitmapMesh()与操纵像素点来改变色彩的原理类似,只不过是把图形分成了一个个的小块,然后通过改变每一个图像块来修改整个图像。
该方法代码如下:
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。
在图像上横纵各画N-1条线,将图像分成N块,而这横纵各N条线交织成了N*N个点,而每个点的坐标则以x1,y1,x2,y2…….xn,yn的形式保存在verts数组中。而整个drawBitmapMesh()方法改变图像的方式,就是靠这些坐标值的改变来重新定位每一个图像块,从而达到图像效果处理的功能。
下面我们使用drawBitmapMesh()方法来实现一个随点击让画面呈现曲面的效果。想要达到这样的效果,只需要让图片中每个交织点的横坐标较之前坐标不发生变化,而纵坐标较之前呈现一个三角函数的周期性变化。
效果图:
ImageChange.java
public class ImageChange extends View { Bitmap bitmap; //定义两个常量,这两个常量指定该图片横向20格,纵向上都被划分为10格 private final int WIDTH = 20; private final int HEIGHT = 10; //记录该图像上包含的231个顶点 private final int COUNT = (WIDTH +1) * (HEIGHT + 1); //定义一个数组,记录Bitmap上的21*11个点的坐标 private final float[] verts = new float[COUNT * 2]; //定义一个数组,记录Bitmap上的21*11个点经过扭曲后的坐标 //对图片扭曲的关键就是修改该数组里元素的值 private final float[] orig = new float[COUNT * 2]; public ImageChange(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { bitmap = BitmapFactory.decodeResource(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; //这里人为将坐标+100是为了让图像下移,避免扭曲后被屏幕遮挡。 orig [index * 2 + 1] = verts [index * 2 + 1] = fy + 100; index += 1; } } } @Override protected void onDraw(Canvas canvas) { //对bitmap按verts数组进行扭曲 //从第一个点(由第5个参数0控制)开始扭曲 canvas.drawBitmapMesh(bitmap,WIDTH,HEIGHT,verts,0,null,0,null); } private void flagWave(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] + dx * pull; } } //通知View组件重绘 invalidate(); } public boolean onTouchEvent(MotionEvent event) { //调用warp方法根据触摸屏事件的坐标点来扭曲verts数组 flagWave(event.getX() , event.getY()); return true; }}
MainActivity.java
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}
七、Android图像处理之画笔特效处理
在前面的学习中,我们已经初步了解了一些常用 画笔属性,比如普通的画笔(Paint),带边框、填充的style,颜色(Color),宽度(StrokeWidth),抗锯齿(ANTI_ALIAS_FLAG)等,这些都是最基本的画笔属性。除此之外,还有各种各样专业的画笔工具,如记号笔、毛笔、蜡笔等,使用它们可以实现更加丰富的绘图效果。
1、PorterDuffXfermode
在开始学习之前,我们先看一张非常经典的图,出自API Demo,基本上所有讲PorterDuffDfermode的文章都会使用这张图作说明:
这里列举了16种PorterDuffXfermode,有点像数学中集合的交集、并集这样的概念。
16条Porter-Duff规则如下:
●1.PorterDuff.Mode.CLEAR——所绘制不会提交到画布上。●2.PorterDuff.Mode.SRC——显示上层绘制图片●3.PorterDuff.Mode.DST——显示下层绘制图片●4.PorterDuff.Mode.SRC_OVER——正常绘制显示,上下层绘制叠盖。●5.PorterDuff.Mode.DST_OVER——上下层都显示。下层居上显示。●6.PorterDuff.Mode.SRC_IN——取两层绘制交集。显示上层。●7.PorterDuff.Mode.DST_IN——取两层绘制交集。显示下层。●8.PorterDuff.Mode.SRC_OUT——取上层绘制非交集部分。●9.PorterDuff.Mode.DST_OUT——取下层绘制非交集部分。●10.PorterDuff.Mode.SRC_ATOP——取下层非交集部分与上层交集部分●11.PorterDuff.Mode.DST_ATOP——取上层非交集部分与下层交集部分●12.PorterDuff.Mode.XOR●13.PorterDuff.Mode.DARKEN●14.PorterDuff.Mode.LIGHTEN●15.PorterDuff.Mode.MULTIPLY●16.PorterDuff.Mode.SCREEN
下面做一个刮刮卡效果:
效果图:
CardImage.java
public class CardImage extends View { Bitmap mBgBitmap,mFgBitmap; Canvas mCanvas; Paint mPaint; Path mPath; public CardImage(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initCardImage(); } private void initCardImage() { mPaint = new Paint(); mPaint.setAlpha(0); //将画笔的透明度设为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.image01); 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); }}
在使用PorterDuffXfermode时还有一点需要注意,那就是最好在绘图时,将硬件加速关闭,因为有些模式并不支持硬件加速。
这个例子和下面的例子我都做在一个Demo里了,这里再放上布局文件和MainActivity:
布局文件
<?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"> <Button android:id="@+id/btn_card" android:layout_width="match_parent" android:layout_height="80dp" android:text="刮刮卡效果:"/> <com.example.administrator.xfermodedemo1.CardImage android:id="@+id/image_card" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone"/> <Button android:id="@+id/btn_shader" android:layout_width="match_parent" android:layout_height="80dp" android:text="圆形图片效果"/> <com.example.administrator.xfermodedemo1.ShaderImage android:id="@+id/image_shader" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone"/></LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener { Button btn_shader,btn_card; ShaderImage image_shader; CardImage image_card; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { btn_shader = (Button) findViewById(R.id.btn_shader); btn_shader.setOnClickListener(this); btn_card = (Button) findViewById(R.id.btn_card); btn_card.setOnClickListener(this); image_shader = (ShaderImage) findViewById(R.id.image_shader); image_card = (CardImage) findViewById(R.id.image_card); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_shader: image_shader.setVisibility(View.VISIBLE); break; case R.id.btn_card: image_card.setVisibility(view.VISIBLE); break; } }}
2、Shader
Shader又被称之为着色器、渲染器,它用来实现一系列的渐变、渲染效果。Android中的Shader包括以下几种。
●BitmapShader——位图Shader●LinearGradient——线性Shader●RadialGradient——光束Shader●SweepGradient——梯度Shader●ComposeShader——混合Shader
除了第一个Shader以外,其他的Shader都比较正常,实现了名副其实的渐变、渲染效果。而与其他的Shader所产生的渐变不同,BitmapShader产生的是一个图像,这有点像Photoshop中的图像填充渐变,它的作用就是通过Paint对画布进行指定Bitmap的填充,填充时有以下几种模式可以选择。
●CLAMP拉伸——拉伸的是图片最后的那一个像素、不断重复●REPEAT重复——横向、纵向不断重复●MIRROR镜像——横向不断翻转重复,纵向不断翻转重复
1)下面实现一个圆形图片的效果:
思路就是用一张图片创建一支具有图像填充功能的画笔,并使用这个画笔绘制一个圆形:
效果图:
ShaderImage.java
public class ShaderImage extends View { Bitmap mBitmap; BitmapShader mBitmapShader; Paint mPaint; public ShaderImage(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initShaderImage(); } private void initShaderImage() { mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.image01); mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,Shader.TileMode.CLAMP); mPaint = new Paint(); mPaint.setShader(mBitmapShader); } @Override protected void onDraw(Canvas canvas) { canvas.drawCircle(mBitmap.getWidth()/2,mBitmap.getHeight()/2,mBitmap.getHeight()/2,mPaint); }}
看一下REPEAT效果
mBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);mPaint = new Paint();mPaint.setShader(mBitmapShader);canvas.drawCircle(350,200,200,mPaint);
2)LinearGradient
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);
3)实现图片倒影效果
效果图:
ReflectView.java
public class ReflectView extends View { private Bitmap mSrcBitmap,mRefBitmap; private Paint mPaint; private PorterDuffXfermode mXfermode; public ReflectView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initRes(context); } private void initRes(Context context) {//将原图复制一份并进行翻转 mSrcBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.image01); 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); }}
3、PathEffect
首先来看一张比较直观的图,来了解一下什么是PathEffect.
PathEffect就是指,用各种笔触效果来绘制一个路径。图中展开的几种绘制PathEffect的方式,从上到下依次是:●没效果●CornerPathEffect——将拐角处变得圆滑,圆滑程度由参数决定。●DiscretePathEffect——这个效果使线段上产生许多杂点。●DashPathEffect——绘制虚线,用一个数组来设置各个点之间的间隔。●PathDashPathEffect——效果与DiscretePathEffect类似,不过它可以设置显示点的图形。●ComposePathEffect——组合PathEffect,将任意两种路径特性组合起来形成一个新的效果。
下面们来实现一下上图:
效果图
我这里的截图上没有显示全,因为我和上面的例子放到一起了,布局里有点放不下了。
ShowPath.java
public class ShowPath extends View { Paint mPaint; Path mPath; PathEffect []mEffects; public ShowPath(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initPath(); } private void initPath() { //用随机数来生成一些随机的点,并形成一条路径 mPath = new Path(); mPath.moveTo(0,0); for(int i = 0;i<= 30;i++){ mPath.lineTo(i * 35,(float)(Math.random() * 100)); } mPaint = new Paint(); } @Override protected void onDraw(Canvas canvas) { //通过不同的路径效果来绘制path mEffects = new PathEffect[6]; 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); //每绘制一个path,就将画布平移,从而将PathEffect依次绘制出来 canvas.translate(0,200); } }}
八、View之孪生兄弟——SurfaceView
1、SurfaceView与View的区别
Android系统提供了View进行绘图处理,View可以满足大部分的绘图需求,但在某些时候也心有余而力不足。我们知道,View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕的重绘,刷新的间隔时间为16ms,如果在16ms内完成View完成了你所需要执行的所有操作,那么用户在视觉上,就不会产生卡顿的感觉;而如果执行的操作逻辑太多,特别是需要频繁刷新的界面上,例如游戏界面,就会不断阻塞主线程,从而导致画面卡顿。
很多时候,在自定义View的Log中经常会看见如下所示警告。
“Skipped 47 frames!The application may be doing too much work on its main thread”
这些警告的产生,很多情况下就是因为在绘制过程中,处理逻辑太多造成的。
为了避免这一问题的产生,Andorid系统提供了SurfaceView组件来解决这个问题。SurfaceView可以说是View的孪生兄弟但它与View还是有所不同的,他们的主要区别体现在:
●View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新。●View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新。●View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中就已经实现了双缓冲机制。
总结一句话:如果你的自定义View需要频繁刷新,或者刷新时数据处理量比较大,那么就可以考虑使用SurfaceView来取代View了。
2、SurfaceView的使用
SurfaceView的使用虽然比View复杂,但是SurfaceView在使用时,有一套使用的模板代码,大部分的SurfaceView绘图操作都可以套用这样的模板代码来进行编写。
1)创建SurfaceView
创建自定义的SurfaceView继承自SurfaceView,并实现两个接口——SurfaceHolder.Callback和Runnable.
public class SurfaView extends SurfaceView implements SurfaceHolder.Callback,Runnable
通过实现这两个接口,就需要在自定义的SurfaceView中实现接口方法,对于SurfaceHolder.Callback方法,需实现如下方法:
//分别对应SurfaceView的创建、改变和销毁过程@Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { }
对于Runnable接口,需要实现run()方法,代码如下:
@Override public void run() { }
2)初始化SurfaceView
在自定义的SurfaceView的构造方法中,需要对SuifaceView进行初始化。通常需要定义以下三个成员变量:
//SurfaceHolder private SurfaceHolder mHolder; //用于绘制的Canvas private Canvas mCanvas; //子线程标志位 private boolean mIsDrawing;
初始化方法就是对SurfaceHolder进行初始化,通过以下代码来初始化一个SurfaceHolder对象,并注册SurfaceHolder的回调方法。
mHolder = getHolder();mHolder.addCallback(this);
另外两个成员变量——Canvas和标志位,Canvas用来绘图,而标志位则是用来控制子线程。
3)使用SurfaceView
通过SurfaceHolder对象的lockCanvas()方法,就可以获得当前的Canvas绘图对象。接下来,就可以与在View中进行的绘制操作一样进行绘制了。
这里需要注意,获取到的Canvas对象还是继续上次的Canvas对象,而不是一个新的对象。因此,之前的绘图操作都将被保留,若需擦除,可以在绘制钱,通过drawColor()方法进行清屏操作。
绘制时,充分利用SurfaceView的三个回调方法,在surfaveCreated()方法中开启子线程进行绘制,而子线程使用一个while(mIsDrawing)的循环来不停地进行绘制,而在绘制的具体逻辑中,通过lockCanvas()方法获得的Canvas对象进行绘制,并通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。整个SurfaceView的模板代码如下:
public class SurfaceViewTest extends SurfaceView implements SurfaceHolder.Callback,Runnable { //SurfaceHolder private SurfaceHolder mHolder; //用于绘图的Canvas private Canvas mCanvas; //子线程标志位 private boolean mIsDrawing; public SurfaceViewTest(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); //mHolder.setFormat(PixelFormat.OPAQUE); } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { mIsDrawing = true; new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { mIsDrawing = false; } @Override public void run() { while (mIsDrawing){ draw(); } } private void draw() { try { mCanvas = mHolder.lockCanvas(); //draw something }catch (Exception e){ }finally { if(mCanvas != null){ mHolder.unlockCanvasAndPost(mCanvas); } } }}
以上代码基本满足大部分的SurfaceView绘图需求,唯一需注意的是在绘制方法中,将mHolder.unlockCanvasAndPost(mCanvas)方法放到finally代码块中,来保证每次都能将内容提交。
3、SurfaceView实例
1)正弦曲线
首先看一个类似示波器的例子,在界面上不断绘制一个正弦曲线。我们只需要不断修改横纵坐标的值,并让它们满足正弦曲线即可。因此,使用一个Path对象来保存正弦函数上的坐标点,在子线程的while循环中,不断改变横纵坐标值:
效果图
SurfaceViewTest.java
public class SurfaceViewTest extends SurfaceView implements SurfaceHolder.Callback,Runnable { //SurfaceHolder private SurfaceHolder mHolder; //用于绘图的Canvas private Canvas mCanvas; //子线程标志位 private boolean mIsDrawing; Paint mPaint; Path mPath; int x = 100,y =0; public SurfaceViewTest(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { //初始化路径和画笔 mPath = new Path(); mPaint = new Paint(); mPaint.setStrokeWidth(20); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); //mHolder.setFormat(PixelFormat.OPAQUE); } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { mIsDrawing = true; new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 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); } } }}
2)绘图板
下面实现一个简单的绘图板,通过Path对象来记录手指滑动的路径来进行绘图。
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); } } }//在SurfaceView的onTouchEvent()中来记录Path路径。 @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; }
一直到这里,这个实例与之前的实例都没有太大区别,但是我们现在需要在子线程的循环中进行优化。我们在子线程中进行sleep操作,尽可能的节省系统资源。
完整代码入下:
SurfaceViewTest2.java
public class SurfaceViewTest2 extends SurfaceView implements SurfaceHolder.Callback,Runnable { //SurfaceHolder private SurfaceHolder mHolder; //用于绘图的Canvas private Canvas mCanvas; //子线程标志位 private boolean mIsDrawing; Paint mPaint; Path mPath; int x = 100,y =0; public SurfaceViewTest2(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { //初始化路径和画笔 mPath = new Path(); mPaint = new Paint(); mPaint.setStrokeWidth(20); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mHolder = getHolder(); mHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); //mHolder.setFormat(PixelFormat.OPAQUE); } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { mIsDrawing = true; new Thread(this).start(); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 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群英传》这本书。
- Android群英传学习——第六章、Android绘图机制与处理技巧
- Android群英传笔记——第六章:Android绘图机制与处理技巧
- Android群英传知识点回顾——第六章:Android绘图机制与处理技巧
- Android群英传学习-Android绘图机制与处理技巧
- 《Android群英传》读书笔记(5)第六章:Android绘图机制与处理技巧之一
- 《Android群英传》读书笔记(6)第六章:Android绘图机制与处理技巧之二
- Android群英传读书笔记第六章(Android绘图机制与处理技巧)
- 第六章Android绘图机制与处理技巧(Android群英传)
- Android群英传之Android绘图机制与处理技巧
- Android群英传之Android绘图机制与处理技巧
- 《Android群英传》读书笔记6.Android绘图机制与处理技巧
- 《Android群英传》读书笔记(7)第六章:Android绘图机制与技巧之三
- Android群英传--绘图机制与处理技巧(一)
- Android群英传--绘图机制与处理技巧(三)
- 绘图机制与图片处理-Android群英传
- Android群英传--绘图机制和处理技巧(二)
- Android群英传--绘图机制和处理技巧(四)
- Android群英传学习——第七章、Android动画机制与使用技巧
- javaGUI知识(二)
- JS原型链
- 一个类似Rxjava的响应式编程框架
- Lua和C++交互api学习
- git rebase -i
- Android群英传学习——第六章、Android绘图机制与处理技巧
- 如何在ABBYY PDF Transformer+中进行文本识别
- 三个问题
- JS-canvas 渐变 绘制圆
- 如何在ionic官网打包自己的App
- Android Studio下载/更新SDK
- 针对IE的CSS hack
- Laravel框架的体系结构
- JavaScript Event Loop 机制详解