Android群英传笔记——第六章:Android绘图机制与处理技巧


一直在情调,时间都是可以自己调节的,不然世界上哪有这么多牛X的人

今天就开始读第六章了,算日子也刚好一个月了,一个月就读一半,这效率也确实有点低了,自己还要加把劲,争取四月底全部看完,第六章讲的是Android的绘图机制,应该算是比较核心的东西了,不管什么功能,最终都是以图形的方式呈现给用户的,因此,掌握Android的绘图技巧,可以在让你设计应用的时候更加的随心所欲,对Android的理解更高

基本的绘图方法,相信读者都已经很清楚了,我们这章就开始玩高级的东西了

  • Android屏幕相关知识
  • Android绘图技巧
  • Android图像处理技巧
  • SurfaceView的使用

一.屏幕的尺寸信息

屏幕的尺寸,我在画.9的时候已经提过Android UI技巧(一)——Android中伸缩自如的点9图片切法,没有美工自给自足

主要还是因为Android的屏幕确实五花八门,所以在一定程度上的适配问题也是很捉急的,所以我们要对这块屏幕充分的认识

1.屏幕参数

一块屏幕通常具备以下的几个参数

  • 屏幕大小

指屏幕对角线的长度,通常用寸来表示,例如4.7寸,5.5寸

  • 分辨率

分辨率是指实际屏幕的像素点个数,例如720X1280就是指屏幕的分辨率,宽有720个像素点,高有1280个像素点

  • PPI

每英寸像素又称为DPI,他是由对角线的的像素点数除以屏幕的大小所得,通常有400PPI就已经很6了

2.系统屏幕密度

每个厂商的安卓手机具有不同的大小尺寸和像素密度的屏幕,安卓系统如果要精确到每种DPI的屏幕,基本上是不可能的,因此系统定义了几个标准的DPI

3.独立像素密度dp

这是由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同,因此相同长度的屏幕,高密度的屏幕包含更多的像素点,在安卓系统中使用mdpi密度值为160的屏幕作为标准,在这个屏幕上,1px = 1dp,其他屏幕则可以通过比例进行换算,例如同样是100dp的长度,mdpi中为100px,而在hdpi中为150,我们也可以得出在各个密度值中的换算公式,在mdpi中 1dp = 1px, 在hdpi中, 1dp = 1.5px,在xhdpi中,1dp = 2px,在xxhdpi中1dp = 3px,由此可见,我们换算公司 l:m:h:xh:xxh = 3:4:6:8:12

4.单位换算

在程序中,我们可以非常方便地对一些单位的换算,下面的代码给出了一种换算的方法我们可以把这些代码作为工具类保存在项目中

package com.lgl.playview;

import android.content.Context;

/**
 * dp,sp转换成px的工具类
 * Created by lgl on 16/3/23.
 */
public class DisplayUtils {

    /**
     * 将px值转换成dpi或者dp值,保持尺寸不变
     *
     * @param content
     * @param pxValus
     * @return
     */
    public static int px2dip(Context content, float pxValus) {
        final float scale = content.getResources().getDisplayMetrics().density;
        return (int) (pxValus / scale + 0.5f);
    }

    /**
     * 将dip和dp转化成px,保证尺寸大小不变。
     *
     * @param content
     * @param pxValus
     * @return
     */
    public static int dip2px(Context content, float pxValus) {
        final float scale = content.getResources().getDisplayMetrics().density;
        return (int) (pxValus / scale + 0.5f);
    }

    /**
     * 将px转化成sp,保证文字大小不变。
     *
     * @param content
     * @param pxValus
     * @return
     */
    public static int px2sp(Context content, float pxValus) {
        final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValus / fontScale + 0.5f);
    }

    /**
     * 将sp转化成px,保证文字大小不变。
     *
     * @param content
     * @param pxValus
     * @return
     */
    public static int sp2px(Context content, float pxValus) {
        final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValus / 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 dp
     * @return
     */
    protected int sp2px(int sp){
        return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
    }

二.2D绘图基础

系统通过提供的Canvas对象来提供绘图方法,它提供了各种绘制图像的API,drawLine,deawPoint,drawRect,drawVertices,drawAce,drawCircle等等,我以前写过Android绘图机制(二)——自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解,通过他们的名字我们基本可以大致了解他们的功能,当然,Paint作为一个非常重要的元素功能也非常强大这里也简单地列举了一些他的属性Android绘图机制(一)——自定义View的基础属性和方法

   setAntiAlias();            //设置画笔的锯齿效果

  setColor();                //设置画笔的颜色

  setARGB();                 //设置画笔的A、R、G、B值

  setAlpha();                //设置画笔的Alpha值

  setTextSize();             //设置字体的尺寸

  setStyle();                //设置画笔的风格(空心或实心)

  setStrokeWidth();          //设置空心边框的宽度

  getColor();                //获取画笔的颜色

是由于画笔功能的不一样,再结合各种不同的绘图API,这样任意组合就可以实现不同的绘图效果,比如同样是矩形设置Paint就Style可以画出空心或者实心,我们来看看RectView

package com.lgl.playview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by lgl on 16/3/23.
 */
public class RectView extends View {

    private Paint paint1,paint2;

    public RectView(Context context, AttributeSet attrs) {
        super(context, attrs);

        initView();
    }

    private void initView() {

        paint1 = new Paint();
        paint1.setColor(Color.BLUE);
        paint1.setAntiAlias(true);
        //空心
        paint1.setStyle(Paint.Style.STROKE);

        paint2 = new Paint();
        paint2.setColor(Color.BLUE);
        //实心
        paint2.setStyle(Paint.Style.FILL);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawRect(50,100,300,300,paint1);
        canvas.drawRect(350,400,700,700,paint2);
    }
}

运行起来就是

如果有不明白的,可以仔细看下我Android绘图机制(二)——自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解,这里就不再详细描述了

三.Android XML 绘图

XML在安卓系统中可不仅仅是JAVA中的一个布局文件配置列表,在安卓开发者的手头上他甚至可以变成一张画,一幅画,Android开发者给XML提供了几个强大的属性

1.Bitmap

在XML中使用Bitmap很简单

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@mipmap/ic_launcher">

</bitmap>

通过这样引用图片就可以将图片直接转化成Bitmap让我们在程序中使用

2.Shape

通过Shape可以绘制各种图形,下面展示一下shape的参数

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"

    android:shape="rectangle">
    <!--默认是rectangle-->

    <!--当shape= rectangle的时候使用-->
    <corners
        android:bottomLeftRadius="1dp"
        android:bottomRightRadius="1dp"
        android:radius="1dp"
        android:topLeftRadius="1dp"
        android:topRightRadius="1dp" />
    <!--半径,会被后面的单个半径属性覆盖,默认是1dp-->

    <!--渐变-->
    <gradient
        android:angle="1dp"
        android:centerColor="@color/colorAccent"
        android:centerX="1dp"
        android:centerY="1dp"
        android:gradientRadius="1dp"
        android:startColor="@color/colorAccent"
        android:type="linear"
        android:useLevel="true" />

    <!--内间距-->
    <padding
        android:bottom="1dp"
        android:left="1dp"
        android:right="1dp"
        android:top="1dp" />

    <!--大小,主要用于imageview用于scaletype-->
    <size
        android:width="1dp"
        android:height="1dp" />

    <!--填充颜色-->
    <solid android:color="@color/colorAccent" />

    <!--指定边框-->
    <stroke
        android:width="1dp"
        android:color="@color/colorAccent" />
    <!--虚线宽度-->
    android:dashWidth= "1dp"

    <!--虚线间隔宽度-->
    android:dashGap= "1dp"

</shape>

shape可以说是xml绘图的精华所在,而且功能十分的强大,无论是扁平化,拟物化还是渐变,都是十分的OK,我们现在来做一个阴影的效果

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="45"
        android:endColor="#805FBBFF"
        android:startColor="#FF5DA2FF" />

    <padding
        android:bottom="7dp"
        android:left="7dp"
        android:right="7dp"
        android:top="7dp" />

    <corners android:radius="8dp" />

</shape>

看效果

3.Layer

Layer是在PhotoShop中是非常常用的功能,在Android中,我们同样可以实现图层的效果

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!--图片1-->
    <item android:drawable="@mipmap/ic_launcher"/>

    <!--图片2-->
    <item
        android:bottom="10dp"
        android:top="10dp"
        android:right="10dp"
        android:left="10dp"
        android:drawable="@mipmap/ic_launcher"
        />

</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="@mipmap/ic_launcher" />

    <!-- 没有焦点时候的背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_window_focused="false" />

    <!-- 非触摸模式下获得焦点并点击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" android:state_window_focused="true" />

    <!-- 触摸模式下获得焦点并点击时的背景图片-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="false" android:state_pressed="true" />

    <!--选中时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_selected="true" />

    <!--获得焦点时的图片背景-->
    <item android:drawable="@mipmap/ic_launcher" android:state_focused="true" />

</selector>

这一方法可以帮助开发者迅速制作View的反馈,通过配置不同的触发事件,selector会自动选中不同的图片,特别是自定义button的时候,而我们不再使用原生单调的背景,而是使用selector特别制作的背景,就能完美实现触摸反馈了

通常情况下,上面提到的这些方法都可以共同实现,下面这个例子就展示了在一个selector中使用shape作为他的item的例子,实现一个具体点击反馈效果的,圆角矩形的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="#33444444" />
            <!--设置按钮的四个角为弧形-->
            <corners android:radius="5dp" />
            <!--间距-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <!--填充颜色-->
            <solid android:color="#FFFFFF" />
            <!--设置按钮的四个角为弧形-->
            <corners android:radius="5dp" />
            <!--间距-->
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>
</selector>

我们来看一下效果图

四.Android绘图技巧

在学完Android的基本绘图之后我们来讲解一下常用的绘图技巧

1.Canvas

Canvas作为绘制图形的直接对象,提供了一下几个非常有用的方法

  • Canvas.save()
  • Canvas.restore()
  • Canvas.translate()
  • Canvas.roate()

首先,我们来看一下前面两个方法

在讲解这两个方法之前,首先来了解一下Android绘图的坐标体系,这个其实这个前面已经讲了,这里不赘述,而Canvas.save()这个方法,从字面上的意思可以理解为保存画布,他的作用就是讲之前的图像保存起来,让后续的操作能像在新的画布一样操作,这跟PS的图层基本差不多

而Canvas.restore()这个方法,则可以理解为合并图层,,就是讲之前保存下来的东西合并

而后面两个方法尼?从字母上理解画布平移或者旋转,但是把他理解为坐标旋转更加形象,前面说了,我们绘制的时候默认坐标点事左上角的起始点,那么我们调用translate(x,y)之后,则将原点(0,0)移动到(x,y)之后的所有绘图都是在这一点上执行的,这里可能说的不够详细,最典型的例子是画一个表盘了,那我们这里就演示一下画一个表盘

我们先来分析一下这个表盘有什么?我们可以将他分解

  • 1.仪表盘——外面的大圆盘
  • 2.刻度线——包含四个长的刻度线和其他短的刻度线
  • 3.刻度值——包含长刻度线对应的大的刻度尺和其他小的刻度尺
  • 4.指针——中间的指针,一粗一细两根

相信如果现实中叫你去画一个仪表盘的话,你应该也会这样的步骤去画,实际上Android上的绘图远比现实中的绘制十分的相似,与PS的绘图更家相似,当然,我们在绘制一个复杂的图形之后,不妨先把思路请清除了

这个示例中,我们第一步,先画表盘,现在画个圆应该很轻松了。关键在于确定圆心和半径,这里直接居中吧

// 画外圆
Paint paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(5);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);

下面,我们来画刻度尺,这个也很简单,一条线而已,只要确定两个端点的位置就可以,第一根线还是比较容易确定的,那后面的我们怎么去确定尼?那些斜着的我们可以用三角函数去实现计算,但是这其实就是一个很简单的画图,三角函数的运算也要这么多,我们不经要思考,该怎么去简化他尼?其实Google已经为我们想好了

我们治国与会觉得这个不好画,主要是这个角度,那么如果我们将画布以中心为原点旋转到需要的角度尼?每当画好一根线,我们就旋转多少级角度,但是下一次划线的时候依然是第一次的坐标,但是实际上我们把画布重新还原到旋转钱的坐标了,所有的刻度线就已经画好了,通过旋转画布——实际上是旋转了画图的坐标轴,这就避免了万恶的三角函数了,通过这样一种相对论式的变换,间接简化了绘图,这时再去绘制这些刻度,是不是要简单了,只需要区别整点和非整点的刻度了

    // 画刻度
        Paint paintDegree = new Paint();
        paintDegree.setStrokeWidth(3);
        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,
                        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,
                        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);
        }

我们看看效果

紧接着,我们就可以来绘制两根针了,我们可以这样来绘制

        // 画指针
        Paint paintHour = new Paint();
        paintHour.setStrokeWidth(20);
        Paint paintMinute = new Paint();
        paintMinute.setStrokeWidth(10);
        canvas.save();
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawLine(0, 0, 100, 100, paintHour);
        canvas.drawLine(0, 0, 100, 200, paintMinute);
        canvas.restore();

这样运行的效果就和最上面的效果图一样了,这里贴上完整的代码

package com.lgl.dial;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;

public class DialView extends View {

    // 宽高
    private int mWidth;
    private int mHeight;

    // 构造方法
    public DialView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 获取屏幕的宽高
        WindowManager wm = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        mWidth = wm.getDefaultDisplay().getWidth();
        mHeight = wm.getDefaultDisplay().getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);

        // 画外圆
        Paint paintCircle = new Paint();
        paintCircle.setAntiAlias(true);
        paintCircle.setStyle(Paint.Style.STROKE);
        paintCircle.setStrokeWidth(5);
        canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);

        // 画刻度
        Paint paintDegree = new Paint();
        paintDegree.setStrokeWidth(3);
        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);
        }

        // 画指针
        Paint paintHour = new Paint();
        paintHour.setStrokeWidth(20);
        Paint paintMinute = new Paint();
        paintMinute.setStrokeWidth(10);
        canvas.save();
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawLine(0, 0, 100, 100, paintHour);
        canvas.drawLine(0, 0, 100, 200, paintMinute);
        canvas.restore();
    }

}

2.Layer图层

Android中的绘图API,很大程度上都来自绘图的API,特别是借鉴了很多PS的原理,比如图层的概念,相信看过图层的也都知道是个什么样的,我们画个图来分析一下

Android通过saveLayer()方法,saveLayerAlpha()将一个图层入栈,使用restore()方法,restoreToCount()方法将一个图层出栈,入栈的时候,后面的所有才做都是发生在这个图层上的,而出栈的时候,则会把图层绘制在上层Canvas上,我们仿照API Demo来

@Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(150, 150, 100, mPaint);

        canvas.saveLayerAlpha(0, 0,400,400,127,LAYER_TYPE_NONE);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(200, 200, 100, mPaint);
        canvas.restore();
    }

当绘制两个相交的圆时,就是图层

接下来将图层后面的透明度设置成0-255不同值

我们分别演示 127 255 0三个

五.Android图像处理之色彩特效处理

Android对于图片的处理,最常用的数据结构是位图——Bitmap,他包含了一张图片的所有数据,整个图片都是由点阵和颜色值去组成的,所谓的点阵就是一个包含像素的矩形,每一个元素对应的图片就是一个像素,而颜色值——ARGB,分别对应透明度红,绿,蓝,这四个通用的分量,他们共同决定了每个像素点显示的颜色

1.色彩矩阵分析

在色彩处理中,我们通常用三个角度描述一张图片

  • 色调——物体传播的颜色
  • 饱和度——颜色的纯度,从0-100来进行描述
  • 亮度——颜色的相对,明暗程度

而在Android中,系统会使用一个颜色矩阵——ColorMatrix,来处理这些色彩的效果,Android中的颜色矩阵是4X5的数字矩阵,他用来对颜色色彩进行处理,而对于每一个像素点,都有一个颜色分量矩阵来保存ARGB值

图中矩阵A就是一个4X5的颜色矩阵,在Android中,他们会以一段一维数组的形式来存储[a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t],则C就是一个颜色矩形分量,在处理图像中,使用矩阵乘法运算AC处理颜色分量矩阵如下图

矩阵运算的乘法公式相信大家都在线性代数课程中学过,计算过程如下

从这个公式我们可以发现

这些是这样归类的

  • 第一行的abcde用来决定新的颜色值R——红色
  • 第二行的fghij用来决定新的颜色值G——绿色
  • 第三行的kimno用来决定新的颜色值B——蓝色
  • 第四行的pqrst用来决定新的颜色值A——透明

这样划分好各自的范围之后,这些值就比较慢明确了,不过只这样说可能会比较抽象,我们通过几个小李子不断的去分析

首先,我们来看一下矩阵变换的计算公式,以R分量为例,计算过程是

R1 = a * R + b* G + c*B+d *A + e

如果让a = 1,b,c,d,e都等于0,那么计算的结果为R1 = R,因此我们可以构建一个矩阵

如果把这个矩阵公式带入R1 = AC,那么根据矩阵的乘法运算法则,可以得到R1 = R;因此,这个矩阵通常是用来作为初始的颜色矩阵来使用,他不会对原有颜色进行任何改变

那么当我们要变换颜色值的时候通常有两种方法,一个是直接改变颜色的offset,即修改颜色的分量。另一种方法直接改变RGBA值的系数来改变颜色分量的值。

-1.改变偏移量

从前面的分析中,可以知道要修改R1的值,只要将第五列的值进行修改即可,即改变颜色的偏移量,其它值保存初始矩阵的值,如图所示的颜色矩阵实现了这样的效果。

在上面这个矩阵中,我们修改了R,G,所对应的颜色偏移量,那么最后的处理结果,就是图像的红色绿色分别增加了100,而我们知道,红色混合绿色的到了黄色,所以最终的颜色处理结果就是让整个图片的色调偏黄色

-2.改变颜色系数

如果修改颜色分量中的某个系数值而其他只依然保存初始矩阵的值。

在上面这个矩阵中改变了G分量所对应的系数据g,这样在矩形运算后G分量会变成以前的两倍,最终效果就是图像的色调更加偏绿。

-3.改变色光属性

图像的色调,饱和度,亮度这三个属性在图像处理中使用的非常多,因此在颜色矩阵中也封装了一些API来快速调用这些参数,而不用每次都去计算矩阵的值。下面通过一个实例来看看如何通过句正改变图像的色调,饱和度和亮度。

在Android中,系统封装了一个类——ColorMatrix,也就是前面所说的颜色矩阵。通过这个类,可以很方便地通过改变矩阵值来处理颜色的效果,创建一个ColorMatrix对象非常简单代码如下。

ColorMatrix colorMatrix = new ColorMatrix();

下面我们来处理不同的色光属性

  • 色调

安卓安卓系统提供了setRotate()帮助我们设置三个颜色的色调,第一个参数系统分别使用0,1,2来代表red green blue 三个颜色的处理而第二个参数就是需要处理的值了

        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.setRotate(0, 2);
        colorMatrix.setRotate(1, 4);
        colorMatrix.setRotate(2, 3);

通过这样的方法,可以为RGB三个颜色的分量重新设置不同的值了

  • 饱和度

    Android系统中提供了setSaturation()来设置颜色的饱和度,参数即代表饱和度的值,当饱和度为0的时候是灰色的

ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(10);
  • 亮度

    当三原色以相同的比例及及混合的时候,就会显示出白色,系统也正在使用这个原理来改变一个图像的亮度,代码如下,当亮度为零时图像会变成全黑。

ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setScale(10, 10, 10, 10);

除了单独使用上面的三种方法来经颜色效果的处理之外,Android系统还封装了矩阵的乘法运算,它提供了PostConcat方法来将矩阵的作用效果混合,从而叠加处理效果代码如下。

        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.postConcat(colorMatrix);
        colorMatrix.postConcat(colorMatrix);
        colorMatrix.postConcat(colorMatrix);

在了解了改变色光属性之后,我们回到现实中的例子,在本例子中,我们通过seekBar来改变不同的数值,并将这些数值作用到对应的矩阵中,最后通过PostConcat方法混合处理

滑动seekbar获得输入值的代码如下

**这个例子有机会做单独介绍,这就不提**

2.Android颜色矩阵——ColorMatrix

通过前面的分析,我们可以知道调整颜色矩阵可以改变一张图片的颜色属性,图像处理很大程度上就是寻找处理图像的颜色矩阵,不仅仅可以通过Android系统提供的API完成,我们自己也可以修改

下面我们可以模拟一个4X5的颜色矩阵,通过修改矩阵的值,一来验证前面所说的改变图像色彩效果的原理是否正确,二来让读者对矩阵产生的作用效果更加清晰和认识

![这里写图片描述](http://img.blog.csdn.net/20160329223612151)

看下怎么实现的

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    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="2" />

    <GridLayout
        android:id="@+id/group"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:columnCount="5"
        android:rowCount="4" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btnChange"
            android:text="Change" />

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="btnReset"
            android:text="Reset" />
    </LinearLayout>

</LinearLayout>

这里我们需要动态的去添加EditText

    private Bitmap bitmap;
    private GridLayout mGroup;
    private ImageView mImageView;
    // 高宽
    private int mEtWidth, mEtHeight;

    private EditText [] mEts = new EditText[20];

bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
        mImageView = (ImageView) findViewById(R.id.imageview);
        mGroup = (GridLayout) findViewById(R.id.group);
        mImageView.setImageBitmap(bitmap);

        mGroup.post(new Runnable() {

            @Override
            public void run() {
                // 获取高宽信息
                mEtWidth = mGroup.getWidth() / 5;
                mEtHeight = mGroup.getHeight() / 4;
                addEts();
                initMatrix();
            }

        });

    }

    /**
     * 添加输入框
     */
    private void addEts() {
        for (int i = 0; i < 20; i++) {
            EditText editText = new EditText(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));
            }
        }
    }

我们无法再onCreate里获得视图的宽高值,所以通过View的Post方法,,在视图创建完成后获得其宽高值

接下来,只需要获得修改后的edittext值,并将矩阵值设置给颜色矩阵即可,在Android中使用一个20的一位数组板寸,通过colormatrix用set方法,将一个数组转换成colormatrix


    /**
     * 获得矩阵值
     */
    private void getMatrix() {
        for (int i = 0; i < 20; i++) {
            mColorMatrix[i] = Float.valueOf(mEts[i].getText().toString());
        }
    }

    /**
     * 将矩阵值设置到图像
     */
    private void setImageMatrix() {
        Bitmap bitmap = Bitmap.createBitmap(mEtWidth, mEtHeight,
                Bitmap.Config.ARGB_8888);
        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.set(mColorMatrix);

        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        canvas.drawBitmap(bitmap, 0, 0, paint);
        mImageView.setImageBitmap(bitmap);
    }

最后设置两个按钮的点击事件

/**
     * 作用点击事件
     */
    public void btnChange(View view) {
        getMatrix();
        setImageMatrix();
    }

    /**
     * 重置矩阵效果
     */
    public void btnReset(View view) {
        initMatrix();
        getMatrix();
        setImageMatrix();
    }

3.常用图象颜色矩阵处理效果

这里主要是说一些矩阵达到一些效果,比如

  • 灰度效果
  • 反转效果
  • 怀旧效果
  • 去色效果
  • 高饱和度

4.像素点分析

作为更加精确的图像处理方式,可以通过改变每个像素点的具体ARGB值,达到处理一张图片效果的目的,这里要注意的是,传递进来的原始图片是不能修改的,一般根据原始图片生成一张新的图片来修改

在Android中,系统系统提供了Bitmap.getPixels()方法来帮我们提取整个Bitmap中的像素密度点,并保存在一个数组中,该方法如下

bitmap.getPixels(oldPx, 0, bitmap.getWidth(), 0, 0, width, height);

这几个参数的具体含义如下:

  • pixels ——接收位图颜色值的数组,
  • offset——写入到pixels[]第一个像素索引值,
  • stride——pixels[]中的行间距
  • x——从位图中读取的第一个像素的x坐标
  • y——从图中读取的第一个像素的的y坐标
  • 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.底片效果

若在ABC三个像素点上,要求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系统提供了ColorMatrix颜色矩阵来帮助我们进行图像处理,而对于图像的图形变换,Android系统也可以通过矩阵来帮忙,每个像素点都表达了其xy的信息,Android的变化就是一个3X3的矩阵

当使用变换矩阵去处理每一个像素点的时候,与颜色矩阵的算法公司其实是一样的

 X1 = a x X +b x Y +c;
 Y1 = d x X +e x Y +f;
 1 = g x X +hx Y + i;

通常情况下,会让g = h = 0,i = 1;这样使1 = g x X + h x Y + i恒成立,因此只需要关注这几个参数就行

与色彩变换矩阵的初始化矩阵一样,图形变换矩阵也需要一个初始矩阵,很明显就是对角线元素,其他元素为0的矩阵

图像的变形处理包含以下四类基本变换

  • Translate——平移变换
  • Rotate——旋转变换
  • Scale——缩放变换
  • Skew——错切变换

-1.平移变换

我们先看一张图

平移变换的坐标就如图,即将每个点的像素点进行平移变换,当p(x0,y0)平移到p(x,y)时,坐标发生了如下的变化

X = X0 + ^X;
Y = Y0 + ^Y;

通过计算我们可以发现如下等式

X = X0 + ^X;
Y = Y0 + ^Y;

这就是我们前面所说的实现平移的平移公式

-2.旋转变换

选择变换实际上就是一个点围绕一个中心旋转到一个新的点上

通过计算,额可以还原以上等式

前面是以坐标原点为旋转中心旋转变换,如果以任一点0为旋转中心进行旋转变换,通常需要以下三个步骤

  • 将坐标原点平移到0点
  • 使用前面讲的以坐标原点为中心的旋转方法进行旋转变换
  • 将坐标原点还原

通过上述三个步骤,就可以以任一点进行旋转了

-3.缩放变换

一个像素点是不存在缩放的概念的,但是由于图像是由很多人像素点组成,如果将每个像素点的坐标都进行相同比例的缩放,最终就会形成让整个图像缩放的效果,缩放效果的计算公式

x = K1 X x0;
y = K2 X y0;

如果写成矩阵的话

通过计算,就可以还原上述等式了

-4.错切变换

错切变换(skew)在数学上又称为Shear mapping(可译为“剪切变换”),或者Transvection(缩并),它是一种比较特殊的线性变换,错切变换的效果就是让所有的X坐标标尺不变,一般分水平和垂直

处置错切就不画了

在了解了矩阵变换规律之后,通过类似多彩矩阵中模拟矩阵的例子来模拟变形矩阵,整个代码与模块颜色矩阵所使用的代码基本一致,在图形变换,同样是通过了一个一位数组将一个一位数组转换为变换矩阵

private float [] mImageMatrix = new float[9];
Matrix matrix = new Matrix();
matrix.setValues(mImageMatrix);
canvas.drawBitmap(mBitmmap,matrix,null);

后面一小部分不写

2.像素块分析

与我们当初了解色彩的处理效果时类似,使用了颜色矩阵和像素点处理两种方式来进行图像处理,这里在进行图像的处理方式也有两种,即前面讲的使用矩阵来进行图像变换和马上要使用到的drawBitmapMesh()方法来处理,操作像素块的原理,该方法如下

canvas.drawBitmapMesh(Bitmap bitmap,int meshWidth,int meshHeight,float [] verts,int vertOffset,int [] color,int colorOffset,Paint paint);

这个方法的参数非常的多

  • bitmap:将要扭曲的图像
  • meshWidth:需要的横向网格数目
  • meshHeight:需要的纵向网格数目
  • verts:网格交叉点的坐标数组
  • vertOffset:verts数组中开始跳过的(X,Y)坐标对的数目

前面说了,要想使用drawBitmapMesh()的方法先要将突破分成若干份,所以在图像各画N-1条线,将图片分成N块,这里我来演示一下

mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.nice);

        float bitmapWidth = mBitmap.getWidth();
        float bitmapHeight = mBitmap.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] = fx+100;
                index +=1;
            }
        }

接下来,在onDraw()方法中改变交叉点的纵坐标的值,为了实现旗帜飘飘的效果,使用sin x,来改变交叉点的坐标值,使横坐标不变,将其变化后的值保存在数组中

private void flagWava(){
        for (int j = 0; j<=HEIGHT;j++){
            for (int i = 0; i<=WIDTH;i++){
                verts[(j*(WIDTH+1)+i)*2+0] +=0;
                float offsetY = (float)Math.sin((float)i/WIDTH*2*Math.PI);
                verts[(j*(WIDTH+1)+i)*2+1] =  orig[(j*(WIDTH+i)+i)*2+1]+offsetY*A;
            }
        }
    }

然后我们就可以使用了

canvas.drawBitmapMesh(mBitmap,WIDTH,HEIGHT,verts,0,null,0,null);

为了能够让图片动起来,可以充分利用正弦函数的周期性来实现,在获取纵坐标的偏移量的时候给函数增加一个周期

 float offsetY = (float)Math.sin((float)i/WIDTH*2*Math.PI+Math.PI*k);

而在重绘 d 时候,给K值增加

flagWava();
k+=0.1f;     canvas.drawBitmapMesh(mBitmap,WIDTH,HEIGHT,verts,0,null,0,null);
        invalidate();

这里,每次重绘的时候,通过改变相位来改变偏移量,从而造成一个动态的效果,就好像旗帜在空中飘扬一样

使用drawBitmapMesh()方法可以创建很多复杂的特效,但是对他的使用也相对来讲比较复杂,需要开发者对图像处理有很深的功底,同时对算法要求比较高,才能做出特效

这一章没有截图,原书乱七八糟的例子,也可能是自己渣渣了吧

七.Android图像处理之画笔特效处理

不管是在我们的世界里,还是在Android的世界里,要想画出好的图片,就必须账务画笔的特效,今天我们就来玩玩这个Paint的特殊Get

1.PorterDuffXfermode

在学习这个东西之前,我们先来看看一张非常经典的图片,来自API DEMO。基本上讲PorterDuffXfermode的都是根据这张图片来的

途中列举了16中PorterDuffXfermode,有点像数学中的集合,主要是一个混合显示模式

这里注意了,PorterDuffXfermode设置两个图层交集区域的显示方法des是先画的图像,src是后画的

当然,这些模式也不是经常的用到,用到最多的事,使用一张图片作为另一张图片的遮罩,通过控制遮罩层的图形,来控制下面被遮罩的显示效果,其中最常用的就是通过DST_IN.SRC_IN模式来实现将一个矩形变成圆角图片的效果,我们这里来实现一下

mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nice);
        mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mOut);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(mBitmap,0,0,mPaint);

下面我们来做一个比较类似于刮刮卡的效果,其实效果就是两张图片,上面那张一刮就显示下面的那张看,这个效果同样我们可以使用PorterDuffXfermode去实现,我们首先要做的就是初始化一些数据

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPaint.setAlpha(0);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        mPath = new Path();
        mBgBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.nice);
        mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mFgBitmap);
        mCanvas.drawColor(Color.GRAY);
    }

在这里我们给paint设置一些属性,让他更圆一点,然后我们再监听触摸时间

    /**
     * 触摸事件
     * @param event
     * @return
     */
    @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_UP:
                mPath.lineTo(event.getX(),event.getY());
                break;
        }
        mCanvas.drawPath(mPath,mPaint);
        invalidate();
        return true;
    }

最后只需要使用DST_IN来完善覆盖即可,这里我把源码放上来

package com.lgl.playview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * 涂鸦
 * Created by lgl on 16/4/2.
 */
public class PlayView extends View {

    private Bitmap mBgBitmap, mFgBitmap;
    private Paint mPaint;
    private Canvas mCanvas;
    private Path mPath;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public PlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPaint.setAlpha(0);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        mPath = new Path();
        mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nice);
        mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mFgBitmap);
        mCanvas.drawColor(Color.GRAY);
    }

    /**
     * 触摸事件
     *
     * @param event
     * @return
     */
    @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_UP:
                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的时候最好把硬件加速给关掉,因为有些不支持

2.Shader

Shader又被称为着色器。渲染器,它可以实现渲染,渐变等效果,Android中的Shader包括以下几种

  • BitmapShader:位图Shader
  • LinearGradient:线性Shader
  • RadialGradient:光束Shader
  • SweepGradient:梯形Shader
  • ComposeShader:混合Shader

除了第一个之外,其他的都是比较正常的,实现了名副其实的渐变,渲染效果,而与其他Shader的效果不同的是,BitmapShader所产生的是一个图像,有点类似PS里面的图像填充,他的作用是通过Paint对画布进行制定bitmap的填充,填充式有一下几个模式供选择。

  • CLAMP拉伸——拉伸的是图片最后的哪一个像素,不断重复
  • REPEAT重复——横向,纵向不断重复
  • MIRROR镜像——横向不断翻转重复,纵向不断翻转重复

这几种模式的含义都非常好理解的,与字面意识基本相同,这里最常用的是CLAMP拉伸模式,虽然他会拉伸最后一个像素,但是只要将突破设置成一个固定的像素,是可以避免的,下面我们来举个例子;

 mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.nice);
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
        mPaint = new Paint();
        mPaint.setShader(mBitmapShader);
        canvas.drawCircle(500,250,200,mPaint);

运行

这里模式是选取的CLAMP,我们再来看,我们这里更改为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(500,250,200,mPaint);

我们再运行一下

很好玩,我们接下来看LinearGradient,理解起来就是线性渐变的意思,我们也来写个例子

mPaint = new Paint();
        mPaint.setShader(new LinearGradient(0,0,400,400, Color.BLUE,Color.YELLOW, Shader.TileMode.REPEAT));
        canvas.drawRect(0,0,400,400,mPaint);

我们运行的结果

其实这几种模式都差不多相同,只是渐变显示的效果不一样罢了

其实,这些渐变效果通常会直接使用在程序里,我们再来用一个倒影的例子来解释这个原理吧

package com.lgl.playview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;

/**
 * 倒影
 * Created by lgl on 16/4/2.
 */
public class InvertedView extends View {

    private Bitmap mSrcBitmap, mRefBitmap;
    private Paint mPaint;
    private PorterDuffXfermode xfermode;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public InvertedView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initRes(context);
    }

    /**
     * 初始化
     *
     * @param context
     */
    private void initRes(Context context) {
        mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.nice);
        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));
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    }

    /**
     * 绘制
     *
     * @param canvas
     */
    @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(xfermode);
        //绘制渐变效果矩形
        canvas.drawRect(0, mSrcBitmap.getHeight(), mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
        mPaint.setXfermode(null);
    }
}

我们直接预览

3.PathEffect

要想了解PathEffect,还是直接看一张图比较直观吧

PathEffect就是指,用各种笔触效果绘制路径,Android系统提供的从上到下

  • CornerPathEffect:拐弯处变圆滑
  • DashPathEffect:线段上有杂点
  • PathDashPathEffect:杂点可以设置形状
  • ComposePathEffect:新效果

这样,我们来做一个简单的实例:

package com.lgl.playview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ComposePathEffect;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.DiscretePathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathDashPathEffect;
import android.graphics.PathEffect;
import android.util.AttributeSet;
import android.view.View;

/**
 * PathEffect
 * Created by lgl on 16/4/2.
 */
public class PathEffectView extends View{

    private Path mPath;
    private PathEffect [] mEffect = new PathEffect[6];
    private Paint mPaint;

    /**
     * 构造方法
     * @param context
     * @param attrs
     */
    public PathEffectView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mPaint = new Paint();
        mPath = new Path();
        mPath.moveTo(0,0);
        for (int i = 0; i<= 30;i++){
            mPath.lineTo(i*35,(float)(Math.random()*100));
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mEffect[0] = null;
        mEffect[1] = new CornerPathEffect(30);
        mEffect[2] = new DiscretePathEffect(3.0F,5.0F);
        mEffect[3] = new DashPathEffect(new float[]{20,10,5,10},0);
        Path path = new Path();
        path.addRect(0,0,8,8,Path.Direction.CCW);
        mEffect[4]= new PathDashPathEffect(path,12,0,PathDashPathEffect.Style.ROTATE);
        mEffect[5] = new ComposePathEffect(mEffect[3],mEffect[1]);
        for (int i = 0; i<mEffect.length;i++){
            mPaint.setPathEffect(mEffect[i]);
            canvas.drawPath(mPath,mPaint);
            canvas.translate(0,200);
        }
    }
}

我们再来运行一下

八.View之孪生兄弟——SurfaceView

1.SurfaceView和View的区别

Android系统提供了VieW进行绘图处理, vieW可以满足大部分的绘图需求,但在某些时却也有些心有余而力不足,特别是在进行一些开发的时候。 我们知道,VieW通过刷新来视图, Android系统通过发出VSYNC信号来进行屏幕的重绘, 刷新的间隔时间为I6ms。在16ms内View完成了你所需要执行的所有操作,那么用户在视觉上, 就不会产生卡顿的感觉:而如果执行的操作逻辑太多,特别是需要频繁刷新的界面上,例如游戏界面,那么就塞主线程,从而导致画面卡顿,很多时候,在自定义VieW的Log中经常会看见如下示的警告

Skipped 47 frames! The application may be doing too much work on its main thread

这些警告的产生,很多情况下就是因为在绘制过程中, 处理逻辑太多造成的为了避免这一问题的产生一 Android系统提供了surfacevicW组件来解决这个问题,可以说是VieW的孪生兄弟,但它与View还是有所不同的,它们的区别主要体现

  • 1.View主要用于自动更新的情况下,而surfaceVicw主要适用于被动更新,例如频繁刷新
  • 2.View在主线程中刷新,而surfaceView通常会通过一 个子线程来进行页面刷新。
  • 3.View在绘制的时候没有双缓冲机制,而surfaceVicw在底层实现机制中就已经实现了双缓冲机制;

总结起来,如果你的自定义View需要频繁刷新,或者刷新时数据处理量比较大,那么你就可以考虑使用surfaceVicw取代View了

2.surfaceView的使用

surfaceView的使用虽然比View复杂.但是surfaeView在使用时,

有一套使用的模板代码,大部分的surfaceView绘图操作都可以套用这样的模板代码来进行编写:因此,其实surfaceView的使用更加简单

通常情况下 使用以下步骤来创建一个surfaceView的模板。

  • 创建surfaceView

    创建自定义的surfaceView,实现它的两个接口

public class SurfaView extends SurfaceView implements SurfaceHolder.Callback,Runnable

通过实现它的两个几口,就会有三个回调方法

@Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

分别对应的是SurfaceView的创建,改变和销毁的过程

对于Runnable接口,会实现他的回调

    @Override
    public void run() {

    }
  • 初始化SurfaceView

    在自定义的SurfaceView构造方法中,需要对SurfaceView进行出还刷,我们首先要定义三个成员变量

     //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
     //子线程标志位
    private boolean mIsDrawing;

初始化方法就是对回调初始化以及注册

mHolder = getHolder();
mHolder.addCallback(this);

另外两个成员变量一Canvas和标志位。 对Canvas我们已经非常熟悉了,与在VieW的ondraw()方法中吏用Canvas绘图一样,在surfaceView中,我们也要使用Canvas来进行绘图,另一个标志位则是用来控制子线程的

  • 使用SurfaceView

通过SurfaceHolder对象的lockCanvas方法,就可以获得当前Canvas绘图对象,接下来,就可以与在View中进行的绘制操作一样进行绘制了,不过这里要注意的是Canvas对象还是继续上次的Canvas对象,而不是一 个新的对象。因此,之前的操作都将被保留,如果需要擦除,则可以在绘制前, 通过drawColoro方法来进行清屏操作

绘制的时候,充分利用SurfaceView的回调方法,在surfaceCreated方法中开启子线程进行绘制,而子线程开启了一个while(mIsDrawing)的循环来不停的绘制,而在具体的逻辑中,通过lookCanvas()方法获取Canvas对象进行绘制,,整个代码模块如下:

package com.lgl.playview;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * SurfaceView的使用
 * Created by lgl on 16/4/2.
 */
public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) {
                //提交
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

以上的代码基本可以满足大部分的SurfaceView绘制,唯一需要注意的就是绘制的时候mHolder.unlockCanvasAndPost(mCanvas);放在finally中,来保证每次都能将任务提交

3.SurfaceView实例

下面我们通过两个实例来玩玩SurfaceView

-1.正弦曲线

package com.lgl.playview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * SurfaceView的使用
 * Created by lgl on 16/4/2.
 */
public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;
    private int x, y = 0;
    private Path mPath;
    private Paint mPaint;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        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);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
            x += 1;
            y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
            mPath.lineTo(x, y);
        }
    }

    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);
            }
        }
    }
}

我们运行一下

我们再来看下一个例子

-2.绘图板

我们来实现一个画板

package com.lgl.playview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * SurfaceView的使用
 * Created by lgl on 16/4/2.
 */
public class SurfaView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用于绘制的Canvas
    private Canvas mCanvas;
    //子线程标志位
    private boolean mIsDrawing;
    private int x, y = 0;
    private Path mPath;
    private Paint mPaint;

    /**
     * 构造方法
     *
     * @param context
     * @param attrs
     */
    public SurfaView(Context context, AttributeSet attrs) {
        super(context, attrs);

        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);
        setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
//            x += 1;
//            y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
//            mPath.lineTo(x, y);
        }
    }

    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);
            }
        }
    }

    /**
     * 触摸事件
     *
     * @param event
     * @return
     */
    @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;
    }
}

这个可好玩多了

好了,到这里,第六章终于写完了,我有一种想吐的感觉,太长了,希望大家多支持哈

PPT下载:http://download.csdn.net/detail/qq_26787115/9480064

Demo下载:http://download.csdn.net/detail/qq_26787115/9480072

笔记下载:http://pan.baidu.com/s/1c0U7k2W 密码:9v0g

因为很长,所以用了MAC和windows两个平台写,PPT和Demo有些不完整,不过不影响观看!

Android群英传笔记——第六章:Android绘图机制与处理技巧的更多相关文章

  1. Android群英传笔记——第七章:Android动画机制和使用技巧

    Android群英传笔记--第七章:Android动画机制和使用技巧 想来,最 近忙的不可开交,都把看书给冷落了,还有好几本没有看完呢,速度得加快了 今天看了第七章,Android动画效果一直是人家中 ...

  2. Android群英传笔记——第三章:Android控件架构与自定义控件讲解

    Android群英传笔记--第三章:Android控件架构与自定义控件讲解 真的很久没有更新博客了,三四天了吧,搬家干嘛的,心累,事件又很紧,抽时间把第三章大致的看完了,当然,我还是有一点View的基 ...

  3. Android群英传笔记——第五章:Android Scroll分析

    Android群英传笔记--第五章:Android Scroll分析 滑动事件算是Android比较常用的效果了,而且滑动事件他本身也是有许多的知识点,今天,我们就一起来耍耍Scroll吧 一.滑动效 ...

  4. Android群英传笔记——第四章:ListView使用技巧

    Android群英传笔记--第四章:ListView使用技巧 最近也是比较迷茫,但是有一点点还是要坚持的,就是学习了,最近离职了,今天也是继续温习第四章ListView,也拖了其实也挺久的了,list ...

  5. Android群英传笔记——第十章:Android性能优化

    Android群英传笔记--第十章:Android性能优化 随着Android应用增多,功能越来越复杂,布局也越来越丰富了,而这些也成为了阻碍一个应用流畅运行,因此,对复杂的功能进行性能优化是创造高质 ...

  6. Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

    Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高 ...

  7. Android群英传笔记——第九章:Android系统信息和安全机制

    Android群英传笔记--第九章:Android系统信息和安全机制 本书也正式的进入尾声了,在android的世界了,不同的软件,硬件信息就像一个国家的经济水平,军事水平,不同的配置参数,代表着一个 ...

  8. Android群英传笔记——第二章:Android开发工具新接触

    Android群英传笔记--第二章:Android开发工具新接触 其实这一章并没什么可讲的,前面的安装Android studio的我们可以直接跳过,如果有兴趣的,可以去看看Google主推-Andr ...

  9. Android群英传笔记——第一章:Android体系与系统架构

    Android群英传笔记--第一章:Android体系与系统架构 图片都是摘抄自网络 今天确实挺忙的,不过把第一章的笔记做一下还是可以的,嘿嘿 1.1 Google的生态圈 还是得从Android的起 ...

随机推荐

  1. PHP Ajax JavaScript 实现 无刷新附件上传

    普通表单 前端页面 后台处理 带有文件的表单 刷新方式 前端界面 后台页面 无刷新方式 大文件上传 POST极值 upload极值 上传细节 前端页面 后台处理 总结 对一个网站而言,有一个基本的不可 ...

  2. 1.物理系统PhysicsWorld,RayCast

     1 3.0物理系统PhysicsWorld T07PhysicsWorld.h #ifndef __T07PhysicsWorld_H__ #define __T07PhysicsWorld_H ...

  3. iOS下JS与OC互相调用(六)--WKWebView + WebViewJavascriptBridge

    上一篇文章介绍了UIWebView 如何通过WebViewJavascriptBridge 来实现JS 与OC 的互相调用,这一篇来介绍一下WKWebView 又是如何通过WebViewJavascr ...

  4. JDBC编程-事务编程(四)

    事务的概念 事务的概念在我看来是指的是一组sql序列,这个序列是一块执行的单位,要么全部执行,要不全部执行,这样可以很好的对数据库进行并发控制. 因为数据库是多个用户都可以同时操作的,如果多个用户同时 ...

  5. 皮尔森相似度计算举例(R语言)

    整理了一下最近对协同过滤推荐算法中的皮尔森相似度计算,顺带学习了下R语言的简单使用,也复习了概率统计知识. 一.概率论和统计学概念复习 1)期望值(Expected Value) 因为这里每个数都是等 ...

  6. Cocos2D中Action的进阶使用技巧(二)

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 上回说到解决办法是使用CCTargetedAction类. C ...

  7. Tomcat内核之类加载器工厂

    Java虚拟机利用类加载器将类载入内存,以供使用.在此过程中类加载器要做很多的事情,例如读取字节数组.验证.解析.初始化等.而Java提供的URLClassLoader类能方便地将jar.class或 ...

  8. 使用Broadcast实现android组件之间的通信

    android组件之间的通信有多种实现方式,Broadcast就是其中一种.在activity和fragment之间的通信,broadcast用的更多本文以一个activity为例. 效果如图: 布局 ...

  9. javascript之event对象

    注意:以下给出的是在IE下的event事件说明,如果应用在非IE下可能会出现兼容性问题,需要结合具体的应用环境,使用兼容性的函数来处理 1.altKey 描述: 检查alt键的状态. 语法: even ...

  10. Cocoa触发方法调用的几种方法

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博 1.SEL触发 SEL就是selector的缩写,它表示Cocoa中的方法选择器,不明白?那请仔细了解Objecti ...