本章内容主要有:
Android屏幕相关知识
Android绘图技巧
Android图像处理技巧
SurfaceView的使用
一个屏幕通常具有以下几个参数。
屏幕大小
指屏幕对角线的长度,通常使用“寸”来度量,例如4.7寸手机。
分辨率
分辨率是指手机屏幕的像素点个数,例如720*1280就是指屏幕的分辨率,指宽有720个像素点,而高有1280个像素点。
PPI
每英寸像素(Picels Per Inch)又被称为DPI(Dots Per Inch)。它是由对角线的像素点除以屏幕的大小得到的,同样的分辨率,屏幕越大,像素点之间的距离越大,屏幕就越粗糙。实践证明,PPI低于240的让人的视觉可以察觉明显颗粒感,高于300则无法察觉颗粒,通常达到400PPI就已经是非常高的屏幕密度了。
每个厂商的Android手机具有不同的大小尺寸和像素密度的屏幕。系统定义了几个标准的DPI值,作为手机固定的DPI:
由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同。Android系统使用mdpi即密度值为160的屏幕作为标准,在这个屏幕上1px=1dp。其它屏幕都可以据此进行换算。比如,同样100dp的长度,在mdpi中为100px,在hdpi中为150px。由此,我们可以得到各个分辨率直接的换算比例:
ldpi : mdpi : hdpi : xhdpi : xxhdpi = 3 : 4 : 6 : 8 : 12
在程序中对单位进行转化,可以直接使用如下代码,当做工具类保存到项目中:
/**
*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());
}
系统通过提供的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);
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);
XML在Android系统中不仅仅是一个布局文件、配置列表。它甚至可以变成一张画、一副图。
在XML中使用Bitmap十分简单,代码如下:
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src = "@drawable/ic_launcher"/>
通过这样引用图片,就可以将图片之间转成了Bitmap让我们在程序中使用。
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>
在Android中可以通过Layer来实现类似Photoshop中图层的概念。
下面我们通过使用layer、layer-list是想图片叠加效果:
在res-drawable目录下新建xml文件:
<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>
Selector的作用在于帮助开发者实现静态绘图中的事件反馈,通过给不同的事件设置不同的图像,从而在程序中根据用户活动,返回不同的结果。
这一方法可以帮助开发者迅速制作View的触摸反馈。
<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>
下面实现一个圆角矩形点击后换背景颜色的效果:
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape = "rectangle">
<solid android:color = "#000"/>
<corners
android:radius = "5dip"/>
<padding
android:bottom = "10dp"
android:left = "10dp"
android:right = "10dp"
android:top = "10dp"/>
shape>
item>
<item>
<shape android:shape = "rectangle">
<solid android:color = "#6769"/>
<corners
android:radius = "5dip"/>
<padding
android:bottom = "10dp"
android:left = "10dp"
android:right = "10dp"
android:top = "10dp"/>
shape>
item>
selector>
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时,即完全不透明。
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对于图片的处理,最常使用到的数据结构是位图——Bitmap,它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应透明度、红、绿、蓝这四个通道分量,他们共同决定了每个像素点显示的颜色。
色光三原色
在色彩处理中,通常使用以下三个角度来描述一个图像。
● 色调——物体传播的颜色
● 饱和度——颜色的纯度,从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值的系数来调整颜色分量的值。
从前面的分析中,可以知道要修改R1的值,只需要将第五列的值进行修改即可,即改变颜色的偏移量,其他值保存初始矩阵的值。
在这个矩阵中,我们修改了R、G对应的颜色偏移量,所以最后的处理结构就是图像中的红色、绿色分量增加了100。
红色混合绿色会得到黄色,所以最终的颜色处理结果就是让整个图像的色调偏黄色。
如果修改颜色分量中的某个系数值,而其他值依然保存初始矩阵的值
这个矩阵改变了G分量对应的系数g,这样在矩阵运算后G分量会变为以前的两倍,最终效果就是图像的色调更加偏绿。
图像的色调、饱和度、亮度这三个属性在图像处理中的使用非常多。因此,在颜色矩阵中,也封装了一些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()方法来显示混合后的处理效果:
<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>
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);
}
}
通过前面的分析,我们知道了调整颜色矩阵可以改变一幅图像的色彩效果,图像处理很大程度上就是在寻找图像的颜色矩阵。不仅仅可以通过Android系统提供的API来进行ColorMatrix的修改,同样可以精确地修改矩阵的值来实现颜色效果的处理。
下面我们模拟一个4*5的颜色矩阵。
<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>
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();
}
}
这一部分展现一些比较经典、常用的颜色处理效果对应的颜色矩阵。
作为更加精确的图像处理方式,可以通过改变每个像素点的具体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);
若存在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;
}
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);
要求某像素点的对应的浮雕效果算法:
B.r = C.r - B.r + 127;
B.g = C.g - B.g + 127;
B.b = C.b - B.b + 127;
前面我们了解了关于图像色彩处理的相关技巧,下面继续学习图形图像方面的处理技巧。
对于图像的图形变换,Android系统也是通过矩阵来进行处理的,每个像素点都表达了其坐标的X、Y信息。Android的图形变换矩阵是一个3*3的矩阵。如图:
当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的矩阵乘法一样,计算公式如下所示:
X1 = a*X+b*Y+c
Y1 = d(X+e*Y+f
1 = g*X+h*Y+i
通常情况下,会让g=h=0,i=1,这样使1 = g*X+h*Y+i恒成立。因此,只需要着重关注上面几个参数就可以了。
与色彩变换矩阵的初始矩阵一样,图形变换矩阵也有一个初始矩阵:
图像的变形处理通常包含以下四类基本变换:
● Translate——平移变换
● Rotate ——旋转变换
● Scale——缩放变换
● Skew——错切变换
平移变换的坐标值变换过程如图,即将每个像素点都进行平移变换:
当从p(x0,y0)平移到p(x,y)时,坐标值发生了如下所示的变换:
X = X0 + △X
Y = Y0 + △Y
这也就是前面所说的实现平移过程的平移公式。
旋转变换即指一个点围绕一个中心旋转到一个新的点,如图:
当从P(x0,y0)点,以坐标原点为旋转中心旋转到P(x,y)点时,可以将点的坐标都表达成OP与X轴正方向夹角的函数表达式:
通过计算,可以还原以上等式,下图所示矩阵也就是旋转变换矩阵。
前面是以坐标原点为旋转中心的旋转变换,如果以任意一点O为旋转中心来进行旋转变换,通常需要以下三个步骤:
● 将坐标原点平移到O点。
● 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
● 将左边原点还原
通过以上三个步骤,实现了以任意点为旋转中心的旋转变换。
一个像素点是不存在缩放的概念的,但是由于图像是由很多个像素点组成的,如果将每个点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果,缩放效果的计算公式如下:
x = K1 * x0;
y = K2 * y0;
写成矩阵形式,如下图:
错切变换(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()——提供矩阵的前乘和后乘运算
我们和上一篇色彩处理的例子一样,做一个图形矩阵,直观的看到图像变换的原理:
方便起见,只放一个平移的效果:
<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>
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();
}
}
在进行图像的特效处理时有两种方式,即前面讲的使用矩阵来进行图像变换和我们马上要学习的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()方法来实现一个随点击让画面呈现曲面的效果。
想要达到这样的效果,只需要让图片中每个交织点的横坐标较之前坐标不发生变化,而纵坐标较之前呈现一个三角函数的周期性变化。
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;
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
在前面的学习中,我们已经初步了解了一些常用 画笔属性,比如普通的画笔(Paint),带边框、填充的style,颜色(Color),宽度(StrokeWidth),抗锯齿(ANTI_ALIAS_FLAG)等,这些都是最基本的画笔属性。除此之外,还有各种各样专业的画笔工具,如记号笔、毛笔、蜡笔等,使用它们可以实现更加丰富的绘图效果。
在开始学习之前,我们先看一张非常经典的图,出自API Demo,基本上所有讲PorterDuffDfermode的文章都会使用这张图作说明:
这里列举了16种PorterDuffXfermode,有点像数学中集合的交集、并集这样的概念。
●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
下面做一个刮刮卡效果:
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:
<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>
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;
}
}
}
Shader又被称之为着色器、渲染器,它用来实现一系列的渐变、渲染效果。Android中的Shader包括以下几种。
●BitmapShader——位图Shader
●LinearGradient——线性Shader
●RadialGradient——光束Shader
●SweepGradient——梯度Shader
●ComposeShader——混合Shader
除了第一个Shader以外,其他的Shader都比较正常,实现了名副其实的渐变、渲染效果。而与其他的Shader所产生的渐变不同,BitmapShader产生的是一个图像,这有点像Photoshop中的图像填充渐变,它的作用就是通过Paint对画布进行指定Bitmap的填充,填充时有以下几种模式可以选择。
●CLAMP拉伸——拉伸的是图片最后的那一个像素、不断重复
●REPEAT重复——横向、纵向不断重复
●MIRROR镜像——横向不断翻转重复,纵向不断翻转重复
思路就是用一张图片创建一支具有图像填充功能的画笔,并使用这个画笔绘制一个圆形:
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);
}
}
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);
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);
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);
}
}
首先来看一张比较直观的图,来了解一下什么是PathEffect.
PathEffect就是指,用各种笔触效果来绘制一个路径。图中展开的几种绘制PathEffect的方式,从上到下依次是:
●没效果
●CornerPathEffect——将拐角处变得圆滑,圆滑程度由参数决定。
●DiscretePathEffect——这个效果使线段上产生许多杂点。
●DashPathEffect——绘制虚线,用一个数组来设置各个点之间的间隔。
●PathDashPathEffect——效果与DiscretePathEffect类似,不过它可以设置显示点的图形。
●ComposePathEffect——组合PathEffect,将任意两种路径特性组合起来形成一个新的效果。
下面们来实现一下上图:
我这里的截图上没有显示全,因为我和上面的例子放到一起了,布局里有点放不下了。
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);
}
}
}
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了。
SurfaceView的使用虽然比View复杂,但是SurfaceView在使用时,有一套使用的模板代码,大部分的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() {
}
在自定义的SurfaceView的构造方法中,需要对SuifaceView进行初始化。通常需要定义以下三个成员变量:
//SurfaceHolder
private SurfaceHolder mHolder;
//用于绘制的Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
初始化方法就是对SurfaceHolder进行初始化,通过以下代码来初始化一个SurfaceHolder对象,并注册SurfaceHolder的回调方法。
mHolder = getHolder();
mHolder.addCallback(this);
另外两个成员变量——Canvas和标志位,Canvas用来绘图,而标志位则是用来控制子线程。
通过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代码块中,来保证每次都能将内容提交。
首先看一个类似示波器的例子,在界面上不断绘制一个正弦曲线。我们只需要不断修改横纵坐标的值,并让它们满足正弦曲线即可。因此,使用一个Path对象来保存正弦函数上的坐标点,在子线程的while循环中,不断改变横纵坐标值:
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);
}
}
}
}
下面实现一个简单的绘图板,通过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操作,尽可能的节省系统资源。
完整代码入下:
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群英传》这本书。