Android群英传学习——第六章、Android绘图机制与处理技巧

本章内容主要有:

    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<!-- 只有在shaperectangle时使用,以下参数取值必须大于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 <!-- shapeline时是贯穿控件的线条,非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+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——错切变换

1)平移变换

平移变换的坐标值变换过程如图,即将每个像素点都进行平移变换:

这里写图片描述

当从p(x0,y0)平移到p(x,y)时,坐标值发生了如下所示的变换:
X = X0 + △X
Y = 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群英传》这本书。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值