株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

第五章 阴影、渐变和位图的运算


5.1、概述

  本章节介绍阴影、渐变和位图运算等技术。阴影只是一个狭隘的说法,实际上也包括发光等效果。Android 也提供了强大的渐变功能,渐变能为物体带来更真实的质感。比如:可以使用渐变绘制一颗五指棋或一根金属圆棒;位图运算就更有趣了。 Android 为 Bitmap的运算提供了多达 16 种运算方法。获得的结果也不尽相同。不过,主要还是在于灵活运用。

5.2、阴影

  可以文字和图片指定阴影(Shader)。在绘图中,有一个叫 layer(层)的概念,默认情况下。我们的文字和图形绘制在主层(main layer)上,其实也可以将内容绘制在新建的 layer 上。实际上阴影就是在 main layer 的下面添加一个阴影层(Shader layer)。可以为阴影指定模糊度、偏移量和阴影颜色   Paint类定义了一个名为 setShadowlayer的方法:   public void setShadowLayer(float radius, float dx, float dy, int shadowColor),参数意义如下:
  • radius:阴影半径
  • dx:x 方向的偏移量
  • dy:y 方向的偏移量
  • shadowColor:阴影的颜色
  阴影 layer 显示阴影时,shadow layer 有两种类型: View.LAYER_TYPE_SOFTWAREView.LAYER_TYPE_HARDWARE,( 相关连接:View layer )layer 的默认类型为 LAYER_TYPE_HARDWARE,但阴影只能在 View.LAYER_TYPE_SOFTWARE 环境工作。所以,我们需要调用 View类的 public void setLayerType(int layerType, Paint paint) 方法为 Paint对象指定层的类型:setLayerType(View.LAYER_TYPE_SOFTWARE, paint) ( 相关连接:硬件加速 setlayertype )   我们通过一个案例来实现文字添加阴影和发光的效果
public class ShaderView extends View {

    public ShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

        Paint paint = new  Paint();
        paint.setAntiAlias(true);
        paint.setTextSize(32) ;
        this.setLayerType(View.LAYER_TYPE_SOFTWARE, paint) ;
        paint.setShadowLayer(10, 1, 1, Color.RED) ;
        canvas.drawText("世上无难事只怕有心人", 100, 100, paint) ;
        paint.setShadowLayer(10, 5, 5, Color.BLUE) ;
        canvas.drawText("失去的不会再回来,好好的不要让明天的你为今天的你叹息", 100, 220, paint) ;
    }

}

  上面的代码中,我们绘制了两行文字。第一行“世上无难事只怕有心人”为红色发光效果。setShadowLayer(10,1,1,Color.RED)语句定义了一个模糊半径为 10, x 方向 和 y 方向偏移量都为 1 的红色阴影。当偏移量足够小时,我们看到的其实是发光效果。paint。setShadowLayer(10,5,5,Color.BLUE)语句定义了一个模糊半径为 10,x 方向和 y 方向偏移量为 5 的蓝色阴影。注意阴影必须在 LAYER_TYPE_SOFTWARE 模式下才能工作

  强调:一旦定义了阴影层,接下来所有绘制都会带阴影效果,如果想取消取消阴影,请将 setShadowLayer()方法的 radius 参数设置为 0

5.3、渐变

  渐变(Gradient)是绘图过程中颜色或位图以特定规律进行变化,能增强物体的质感和审美情趣。生活中的渐变非常多。例如:公路两边的电线杆、树木、建筑物的阳台、铁轨的枕木延伸到远方等等,很多的自然理象都充满了渐变的形势特点。Android同样对渐变进行了完善的支持,通过渐变,可以绘制出更加逼真的效果

  Graphics2D 渐变种类有:

  • 线性渐变:LinearGradient
  • 径向渐变:RadialGradient
  • 扫描渐变:SweepGradient
  • 位图渐变:BitmapShader
  • 混合渐变:ComposeShader

  其中。线性渐变、径向渐变和扫描渐变属于 颜色渐变;指定 2种或 2种以上的颜色。根据颜色过渡算法自动计算中间的过渡颜色,从而形成渐变效果,对于我们来说,无需关注中间的渐变颜色。位图渐变则不是简单的颜色渐变,而是以图片作为贴片有规律的变化,类是与壁纸的平铺。混合渐变则能将多种渐变进行组合,实现更加复杂的渐变效果。

  如果 A,B 分别代表 2种颜色,我们将渐变分为三种:

  • AABB 型:A、B 两种颜色只出现一次,通过 TileMode类的 CLAMP 常量来表示
  • ABBA 型:A、B 两种颜色镜像变化,通过 TileMode类的 MIRROR 常量表示
  • ABAB 型:A、B两种颜色重复变化,通过 TileMode 类的 PEOEAT 常量来表示

  定义渐变时,必须指定一个渐变的区域,根据定义的渐变内容和渐变模式填满该区域。每一种渐变都被定义城里一个类,他们都继承自同一个父类——Shader。惠土师,调用 Paint类的 public Shader setShader(Shader shader) 方法,指定一种渐变类型,绘制出来的绘图填充区域都将使用指定的渐变颜色或位图进行填充。本质上说,前面谈到的填充(FILL)和渐变的(Gradient)都大同小异。我们需重点掌握每隔渐变类的构造方法的参数以及意义

  讨论渐变虽然更多的是指填充区域的渐变,但绘图的样式为 STROKE时,线条同样可以应用渐变的效果

5.3.1 线性渐变(LinearGradient)
  线性渐变(LinearGradient)根据指定的角度、颜色和模式使用渐变色填充绘图区域。我们必须定义两个点(x0,y0)和(x1,y1),渐变的方向与这两个点的连线垂直(如图 5-3所示)

  LinearGradient 的构造方法如下:

  • public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
    本方法用于两种颜色的渐变,各种参数意义如下:
  • x0,y0:用于决定线性方向的第一个点的坐标(x0,y0)
  • x1,y1:用于决定线性方向的第二个点的坐标(x1,y1)
  • color0:第一种颜色
  • color1:第二种颜色
  • tile:渐变模式

  假设我们绘制了三个矩形,第一个矩形渐变区域与矩形恰好一致,第二个矩形的渐变区域大于矩形区域,第三个矩形的渐变区域小于矩形区域。均采用 TileMode 的 CLAMP模式,代码如下:

public class GradientView extends View {

    private static final int OFFSET = 100 ;
    private Paint paint ;

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

        paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setStyle(Style.STROKE);
    }

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

        Rect rect1 = new Rect(100, 100, 500, 300);

        LinearGradient lg = new LinearGradient(
                rect1.left, rect1.top, rect1.right, rect1.bottom,
                Color.RED, Color.BLUE, TileMode.CLAMP) ;

        paint.setShader(lg) ;
        canvas.drawRect(rect1 ,paint) ;

        //第二个矩形(坐标下移)
        canvas.translate(0, rect1.height()+OFFSET);
        //放大矩形 2
        Rect rect2 =  new Rect(rect1); 
        rect2.inset(-100, -100);

        lg = new LinearGradient(
                rect2.left, rect2.top, rect2.right, rect2.bottom,
                Color.RED, Color.BLUE, TileMode.CLAMP);
        paint.setShader(lg);
        canvas.drawRect(rect1, paint);

        paint.setShader(lg) ;
        canvas.drawRect(rect1, paint) ;


        //第三个矩形
        canvas.translate(0, rect1.height()+OFFSET) ;
        //缩小矩形 3
        Rect rect3 = new Rect(rect1);
        rect3.inset(100, 100) ;

        lg = new LinearGradient(
                rect3.left, rect3.top, rect3.right, rect3.bottom,
                Color.RED, Color.BLUE, TileMode.CLAMP);

        paint.setShader(lg);
        canvas.drawRect(rect1, paint); 

    }

}
  效果如 5-5 所示(右图为 STOKE 绘图模式)

  从上图我们了可以看出,当渐变区域和矩形区域大小不同时,表现出来的效果也有不同

  如果两种无法满足绘图需求时,LinearGradient 支持三种 或者三种以上的颜色的渐变,对应的构造方法如下:

  • public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile)
    这是一个功能更加强大的构造方法,我们来看看该构造方法参数额作用
  • x0,y0:起点的起始坐标
  • x1,y1 :终止点的坐标
  • colors : 多种颜色
  • positions:颜色的位置(比例)
  • TileMode:渐变模式

  参数 colors 和 positions 都是数组,前者用于指定多种颜色,后者用于指定每种颜色的起始比例位置。positions 数组的元素个数与 colors要相同。并且是 0 ~ 1 的数值,[0,1]是临界值,如果小于 0 则当 0处理。如果大于 1则当 1 处理。假如在绘图区域和渐变区域大小相同的情况下,color 包含了 三种颜色:rea,yellow,green,在渐变区域这三种颜色的其实比例位置为 0,0.3,1,则颜色渐变如图 5-6 所示,比例位置为 0.2,0.5,0.8 则颜色渐变如图 5-7 所示

  从上图可以看出,0.2不理位置处设置 red,0 ~ 0.2 之间的颜色也是 red。0.8 比例位置处 green,0.8 ~ 1之间的颜色也是green。当然,这里呈现出来的效果时在回去区域和渐变区域相同的前提条件下,修改渐变区域的大小或者 TileMode渐变模式,结果必定不一样

  通过 LinearGradient实现文字的闪烁

public class MyTextView extends TextView {

    private LinearGradient mLinearGradient ;
    private Matrix mGradientMatrix ;
    private Paint mPaint ;
    private int mViewWidth = 0 ;
    private int mTranslate = 0 ;

    private boolean mAnmating = true ;

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if(mViewWidth == 0){
            mViewWidth = getMeasuredWidth() ;
            if(mViewWidth > 0){
                mPaint = getPaint() ;
                mLinearGradient = new LinearGradient(
                        -mViewWidth, 0, 
                        0, 0, 
                        new int[]{  0x33ffffff, 0xffffffff, 0x33ffffff }, 
                        new float[]{0.0f ,0.5f ,1.0f}, 
                        Shader.TileMode.CLAMP) ;
                mPaint.setShader(mLinearGradient) ;
                mGradientMatrix = new Matrix() ;
            }
        }
    }

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

        if(mAnmating && mGradientMatrix != null){
            mTranslate += mViewWidth /10 ;
            if(mTranslate >  2* mViewWidth){
                mTranslate = -mViewWidth ;
            }
        }
        mGradientMatrix.setTranslate(mTranslate, 0) ;
        mLinearGradient.setLocalMatrix(mGradientMatrix) ;
        postInvalidateDelayed(50) ;
    }
}
运行如下:
5.3.2 径向渐变(RadialGradient)  径向渐变是以指定的点为中心,向四周以渐变颜色进行圆周扩散,和线性渐变一样。支持两种或多种颜色。径向渐变的示意图如图 5-8 所示

  径向渐变的主要构造方法如下:

  • public RadialGradient(float x, float y, float radius, int color0, int color1, TileMode tile)
    该构造方法支持两种颜色,下面是参数的作用
    • x,y:中心点坐标
    • radius:渐变的半径
    • color:起始颜色
    • TileMode:渐变模式
  • public RadialGradient(float centerX, float centerY, float radius, int colors[], float stops[], TileMode tileMode)
    该构造方法支持 3种或者 3种以上的颜色,各个参数的作用如下
    • x,y:中心点坐标
    • radius;渐变半径
    • colors:多种颜色
    • positions : 颜色的位置(比例)
    • TileMode :渐变模式

  下面我们在 View 上绘制相同大小的正方形和圆,使用一致的径向渐变,模式为 TileMode.MIRROR。在大部分时候,镜像模式的渐变效果看起来会更舒服讨人喜爱

  利用径向渐变,我们可以画出五子棋的棋子,五子棋分为黑色和白色两种不同的棋子。为了画出更逼真的效果。需要考虑棋子的返光效果。光点不能是正中心,而因该是向右下角偏移;同时,为了棋子加上阴影,棋子似乎跃然纸上。黑色棋子使用个黑白绘制,白色棋子则使用灰白绘制,在一个棋盘上来展示出两种棋子,从而练习一下线条、圆及渐变等图像的绘制。代码如下:

public class FiveChessView extends View {

    /**其值的大小*/
    private static final int SIZE = 120;
    /**发光点的偏移大小*/
    private static final int OFFSET = 10 ;
    private Paint paint ;

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

        paint = new  Paint(paint.ANTI_ALIAS_FLAG) ;
    }


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

        int width = this.getMeasuredWidth() ;
        int height = this.getMeasuredHeight() ;

        int rows = height / SIZE ;
        int cols = width / SIZE ;

        //画棋盘
        drawChessBoard(canvas, rows, cols);

        //棋子
        drawChess(canvas, 2, 3, ChessType.BLACK);
        drawChess(canvas, 2, 4, ChessType.BLACK);
        drawChess(canvas, 3, 4, ChessType.WHITE);
        drawChess(canvas, 3, 5, ChessType.WHITE);
    }


    private void drawChessBoard(Canvas canvas, int rows, int cols) {

        paint.setColor(Color.GRAY) ;
        //取消阴影
        paint.setShadowLayer(0,0,0,Color.GRAY) ;
        for (int i = 0; i <= rows; i++) {
            canvas.drawLine(0, i*SIZE, cols*SIZE,i*SIZE , paint) ;
        }
        for(int i = 0 ; i <= cols ; i++){
            canvas.drawLine(i*SIZE, 0, i*SIZE, rows*SIZE, paint);
        }
    }

    private void drawChess(Canvas canvas, int x, int y, ChessType chessType){
        //棋子颜色
        int colorOuter = chessType == ChessType.BLACK ? Color.BLACK : Color.GRAY ;
        int colorInner = Color.WHITE ;

        //定义渐变,发光点在想右下角偏移
        RadialGradient rg = new RadialGradient(
                x*SIZE + OFFSET, y*SIZE+OFFSET, SIZE/2.5f,
                colorInner, colorOuter, TileMode.CLAMP) ;

        paint.setShader(rg) ;

        //画棋子
        this.setLayerType(View.LAYER_TYPE_SOFTWARE, paint) ;
        paint.setShadowLayer(6, 4, 4, Color.parseColor("#AACCCCCC"));
        canvas.drawCircle(x*SIZE, y*SIZE, SIZE/2, paint) ;

    }

    enum ChessType{
        BLACK,WHITE
    }

}
  运行效果如下( 慕课网:hyman的完整五子连珠视屏链接):

5.3.3 扫描渐变(SweepGradient)
  Sweep,这是什么单词? 英文字典翻译为“清除、扫除”。不过,我觉得叫扫描渐变也挺好。 SweepGradient类似于军事题材电影中雷达扫描效果。固定圆心,将半径假想为有形并旋转一周而绘制的渐变颜色。SweepGradient定了两个主要的构造方法:

  • public SweepGradient(float cx, float cy, int color0, int color1)
    支持两种颜色的扫描渐变,参数的作用如下:
    • cx,cy:原点坐标
    • color0:其实颜色
    • color1:结束颜色
  • public SweepGradient(float cx, float cy, int colors[], float positions[])
    支持多种颜色的扫描渐变,参数作用如下:
    • cx,cy:原点坐标
    • colors:多种颜色
    • positions:颜色的位置(比例)

  参数的作用和使用在前面的已大致介绍。下面我们通过一个键的案例来说明 SweepGradient的使用方法,代码如下:

public class SweepGradientView extends View {

    public SweepGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

        Paint paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
        SweepGradient sg = new  SweepGradient(
                300, 300, Color.GREEN, Color.YELLOW) ;
        paint.setShader(sg) ;
        canvas.drawRect(new Rect(0,0,600,600), paint) ;

    }

}
  如图 5-12 所示,渐变的起始颜色为绿色,终止颜色为黄色,和 RadialGradient 不同,RadialGradient的渐变是一圆心为中心向四周扩散( 就如同击水时,产生的涟漪一样。向外围扩散。)扩散的距离有参数 radius决定,一旦超过该距离,将根据 TileMode渐变模式重复绘制渐变颜色;SweepGradient是从 0度方向开始,已指定的点为中心,保存中心不动,将半径旋转一周,不需要需要指定半径和渐变模式。因为颜色的渐变指向无穷远处,而且只旋转 360°度

  图 5-12 所示的效果在绿色和黄色之间没有过渡色,显得特别突出,可以使用第二个构造方法。定义三种或者三种以上的颜色,第一种颜色和最后一种颜色相同就即可。下面的代码颜色了这种用法,定义 SweepGradient对象时,参时 positions 为 null ,表示各种颜色所占比例平均分配

public class SweepGradient2View extends View {

    public SweepGradient2View(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;

        SweepGradient sg = new SweepGradient(
                300, 300, 
                new int[]{Color.GREEN,Color.YELLOW,Color.RED,Color.GREEN}, null) ;

        paint.setShader(sg) ;
        canvas.drawOval(new RectF(0,0,600,600), paint) ;
    }

}
  运行如下:

5.3.4 位图渐变(BitmapShader)
  位图渐变其实就是在绘制的图形中将指定的位图作为背景,如果图形比位图小,则通过渐变模式进行平铺,TileMode.CLAMP模式不平铺、TileMode.REPEAT 模式表示平铺、TileMode.MIRROR 模式也表示平铺,但是交错的位图是彼此的镜像,方向相反。可以同时指定水平和垂直两个方向的渐变模式

  • BitmapShader 只有一个构造方法
  • public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
  • 参数如下:
    • bitmap:位图
    • tileX:x 方向的重复模式
    • tileY:Y 方向的重复模式

  以下代码我们编写一个案例使用 ic_launcher填充绘制的图形中

public class BitmapShaderView extends View {

    public BitmapShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


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


        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
        BitmapShader bs = new  BitmapShader(bmp, 
                TileMode.REPEAT, TileMode.MIRROR) ;

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setShader(bs) ;
        canvas.drawRect(new Rect(0,0,getMeasuredWidth(),getMeasuredHeight()), paint) ;
    }
}
  从图 5-14 中看出,水平方向的渐变模式为 TileMode.REPEAT ,所以小机器人重复出现了,而垂直方向的渐变模式为 TileMode.MIRROR,偶数行的小机器人与技术行是垂直翻转的

5.3.5 混合渐变(ComposeShader)
  混合渐变 ComposeShader是将两种不同的渐变通过位图运算后得到的一种更加复杂的渐变。位图运算有 16种之多,在下一小节中将介绍,本节介绍 ComposeShader的基本使用方法:

  • public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode)
  • public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
  • shaderA 和 shaderB 是两个渐变对象,mode 为位图运算类型,16种运算模式如图所示。其实从命名就能大概知道么中位图的含义

  下面的案例是将两种渐变(线性渐变和位图渐变)进行Mode.SRC_ATOP 运算得到的新的混合渐变,如图 5-16 所示,Mode.SRC_ATOP 预算是值显示第一个位图的全部,而第二个位图只显示二者的交集部分并显示在上面

public class ComposeShaderView extends View {

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

    }

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

        //位图渐变
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher) ;

        BitmapShader bs = new  BitmapShader(bmp,
                TileMode.REPEAT, TileMode.MIRROR);

        //线性渐变
        LinearGradient lg = new LinearGradient(
                0, 0, getMeasuredWidth(), 0, 
                Color.RED, Color.BLUE, TileMode.CLAMP);

        //混合渐变
        ComposeShader cs = new ComposeShader(bs, lg, Mode.SRC_ATOP);

        Paint paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setShader(cs) ;
        canvas.drawRect(new Rect(0,0,getMeasuredWidth(),getMeasuredHeight()), paint) ;

    }

5.3.6 渐变与 Matrix
  渐变类都继承自同一个父类——Shader,该类并不复杂。不过定义了一个非常有用的方法:public void setLocalMatrix(Matrix localM),该方法能和渐变结合,在填充渐变颜色的时候实现位移、旋转、缩放和斜拉的效果

  下面的案例中,我们做了一个旋转的圆,圆内使用 SweepGradient渐变填充,看起来像一张光盘。首先,我们创建一个Matrix对象 mMatrix。mMatrix定义了一圆心为中心渐变的旋转效果,注意不是旋转 Canvas ,而是旋转 SweepGradient。onDraw()方法中不断调用 invalidate()重绘自己,每重绘一次就旋转 3°度,于是就形成了一个旋转的动画

public class Sweep extends View {

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
    private float mRotate ;
    private Matrix mMatrix = new  Matrix();
    private Shader mShader ;

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

        setFocusable(true) ;
        setFocusableInTouchMode(true) ;

        float x = 100f ;
        float y = 100f ;

        mShader  =new  SweepGradient(x, y,
                new int[]{Color.RED,Color.BLUE,Color.GREEN,Color.RED}, null) ;

        mPaint.setShader(mShader) ;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
//      Paint paint =mPaint;

        float x = 100f;
        float y = 100f;

        canvas.translate(200, 200);
        canvas.drawColor(Color.WHITE);
        mMatrix.setRotate(mRotate,x,y);
        mShader.setLocalMatrix(mMatrix) ;
        mRotate += 3;

        if(mRotate >= 360){
            mRotate = 0;
        }

        invalidate() ;
        canvas.drawCircle(x, y, 200, mPaint) ;

    }
}



5.4、位图运算


5.4.1 PorterDuffXfermode
  位图运算为位图的功能繁衍提供了强大的技术基础,大大增强了位图的可塑性和延伸性。使很多看起来非常复杂的效果和功能都轻易的实现。比如:圆形头像、不规则图片、橡皮擦、稀奇古怪的自定义进度条等等。但是因为运算样式多,容易造成心理上难以逾越的门槛。

  在 Graphics2D中,类 PorterDuffXfermode提供对位图运算模式的定义和支持,“ProterDuff”是两个人的名组合:Tomas Proter 和 Tom Duff。他们是最早在 SIGGRAPH 上提出图像混合概念的大神级人物。创建 ProterDuffXfermode 对象是,可以提供多达 16 种运算模式,运行官方提供的 API Demos APP,找到 Graphics/Xfermodes,能看到如图所示的运行结果

  位图运算模式定义在 ProterDuff 类的内部枚举类型 Mode中,对应了 16 个不同的枚举值

    public enum Mode {
        /** [0, 0] */
        CLEAR       (0),
        /** [Sa, Sc] */
        SRC         (1),
        /** [Da, Dc] */
        DST         (2),
        /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
        SRC_OVER    (3),
        /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
        DST_OVER    (4),
        /** [Sa * Da, Sc * Da] */
        SRC_IN      (5),
        /** [Sa * Da, Sa * Dc] */
        DST_IN      (6),
        /** [Sa * (1 - Da), Sc * (1 - Da)] */
        SRC_OUT     (7),
        /** [Da * (1 - Sa), Dc * (1 - Sa)] */
        DST_OUT     (8),
        /** [Da, Sc * Da + (1 - Sa) * Dc] */
        SRC_ATOP    (9),
        /** [Sa, Sa * Dc + Sc * (1 - Da)] */
        DST_ATOP    (10),
        /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
        XOR         (11),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
        DARKEN      (12),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
        LIGHTEN     (13),
        /** [Sa * Da, Sc * Dc] */
        MULTIPLY    (14),
        /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
        SCREEN      (15),
        /** Saturate(S + D) */
        ADD         (16),
        OVERLAY     (17);
    }
  我们通过下面的表格来理解个运算模式的作用
枚举值说明效果
原始DST 代表下层(圆)
SRC 代表上层(正方形)
CLEAR绘制的内容不会提交到画布
DARKEN取两图层全部区域,交集部分颜色加深
DST显示下层位图
DST_ATOP取上层非交集与下层交集部分
DST_IN取两层的交集部分,交集内容取决于下层
DST_OUT取下层的非交集部分
DST_OVER上下层都显示,运算后下层在上面
LIGHTEN取两层的全部内容,点亮交集部分的颜色
MULTIPLY取两图层交集部分并叠加颜色
SCREEN取两图层全区域,交集部分变为透明
SRC显示上层位图
SRC_ATOP取下层非交集部分与上层交集部分
SRC_IN取两层交集部分,交集内容取决于上层
SRC_OUT取上层的非交集部分
SRC_OVER上下层都显示,运算后上层在上面
XOR异或操作,去除两层交集部分
  为了实现位图运算,创建 PorterDuffXfermode 对象后,调用 Paint 类的 public Xfermode setXfermode(Xfermode xfermode)方法,ProterDuffXfermode 是 Xfermode 的子类,将 PorterDuffXfermode 对象作为实际参数传入即可,形如:   paint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
5.4.1 图层(Layer)
  Canvas 在一般情况下可以看做是一张画布,所有的绘图操作,如:位图、圆、直线等都在这张画布上绘制,Canvas同时还定义了相关属性如:Matrix、颜色等等。但是,倘若需啊哟实现一些相对复杂的绘图操作,比如:多层动画、地图(地图可以有多个地图层叠加而成。比如:镇区层、道路层、兴趣点层)等,需要 Canvas提供的图层(Layer)支持,缺省情况下可以看做一个图层 Layer。如果需要按照层来绘图,Canvas 需要创建一些中间层。layer 按照“栈结构”来管理。示意图 5-18 所示

  j既然是栈结构,自然存在入栈和出栈两种行为。layer 入栈时,后续的绘图操作都发生在这一个 layer上,而 layer 出栈时,将本图层绘制到的图像“绘制”到上层或是 Canvas上,复制 layer到 Canvas上时,还可以指定 layer 的透明度

  其实在《5.2 阴影》这一小节中。就介绍了 layer 的概念,我们可以将它翻译成“图层”,Canvas 默认的图层称之为“主图层(main layer)”,阴影显示在“阴影图层(shader layer)”中,实际上,我们还能自己创建新的图层并入栈,创建图层通过 saveLayer()方法,该方法有下面几个重载的版本

  • public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags)
  • public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint)
  • public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint, @Saveflags int saveFlags
  • public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint)

  还能通过 savaLayerAlpha( )方法为图层指定透明度:

  • public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags)
  • public int saveLayerAlpha(@Nullable RectF bounds, int alpha)
  • public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, @Saveflags int saveFlags)
  • public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)

  savaLayer( )方法中,left、top、right、buttom与欧诺个Yui确定图层的位置和大小;参数 Paint 官方文档对作用的描述是This is copied, and is applied to the offscreen when restore() is called。 可以指定为 null;saveFlags 用于指定保存的表示位,虽然也有好几个值可供选择,但是官方推荐使用Canvas.ALL_SAVE_FLAG。 在 saveLayerAlpha( )方法中,参数 alpha自然就是指定图层的透明度(0 ~ 255) 了,这 两个方法的返回值为一个 int 值,代表当前入栈的 layer 的 id,通过该 id 能明确是哪一个 layer 出栈

  layer 从栈中弹出(出栈),需要调用 public void restoreToCount(int saveCount) 方法,该方法的参数 就是 saveLayer( ) 或 saveLayerAlpha( ) 的返回值

5.4.3 位图运算机巧
  要实现位图的混合运算,一方面需要通过 PorterDuffXfermode 指定的运算模式,另一方面还需要借助 layer进行“离屏缓存”,达到类是 PhotoShop中的“遮罩层”。归纳起来,大概有下面几个参考步骤(其实可以更加简化步骤)

  •  准备好分别代表 DST 和 SRC 的位图,同时准备第三个位图,该位图用于绘制 DST 和 SRC 运算后的结果
  •  创建大小合适的图层(layer)并入栈
  •  先将 DST 位图绘制在第三个位图上
  •  调用 Paint 的 setXfermode( )方法定义的位图运算模式
  •  再将 SRC 位图绘制在第三个位图上
  •  清除位图运算模式
  •  图层 (layer)出栈
  •  将第三个为图绘制在 View的 Canva上以便显示

  为了加深 layer 在位图运算中的作用,我们循次渐进,由浅入深的学习。首先,在不使用 layer 情况下,按照上面的思路绘制一个圆和一个正方形,圆作为 DST,正方形作为 SRC,并执行Mode.SRC 的运算模式

public class PorterDuffXferView extends View {

    public PorterDuffXferView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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


        Bitmap dst = Bitmap.createBitmap(300,300,Config.ARGB_8888) ;
        Bitmap src = dst.copy(Config.ARGB_8888, true) ;
        Bitmap bmp  = Bitmap.createBitmap(450,450,Config.ARGB_8888) ;

        Canvas c1 = new Canvas(dst);
        Canvas c2 = new Canvas(src);
        Canvas c3 = new Canvas(bmp);

        Paint p1 = new Paint() ;
        p1.setColor(Color.GRAY) ;
        c1.drawCircle(150, 150, 150, p1) ;

        Paint p2 = new  Paint() ;
        p2.setColor(Color.GREEN) ;
        c2.drawRect(0, 0,300,300,p2) ;

        //      定义画笔
        Paint paint = new  Paint() ;
        //画圆
        c3.drawBitmap(dst, 0, 0,null) ;
        //定义位图的运算模式
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC)) ;
        //画正方形
        c3.drawBitmap(src, 150, 150,paint) ;
        //清除运算效果
        paint.setXfermode(null) ;
        //绘制到 Canvas上
        canvas.drawBitmap(bmp, 0,0, null);

    }
}
  运行这段代码,结果却大事所望。从图 5-19 中看出,与我们前面介绍的相差甚远,根本不是我们想要的结果,正确的效果因该是如图 5-20所示

  该问题需要用到图层(layer)来解。实际上,在Mode.SRC 运算模式中,如果不愿意看到 DST(圆)的非交集部分(左上角的灰色部分),不使用 layer 是解决不了问题的,我们必须在正方形区域定义一个图层,绘图后,图层区域内的部分将会显示,而显示区域的部分即会消失,图 5-21 的示意图可以加深理解

  如图 5-21 所示,虚线部分表示图层(layer),绘图时,先创建图层并入栈,该图层的 left 和 top 应该与圆的原点坐标相同,right 和 buttom 则应该与正方形的 right 和 buttom相同,接下来依次绘制圆形和正方形,所有绘制都作用在前面的创建的图层上,通过 restoreToCount( )方法将图层出栈后,显示出来的其实是图层之内的部分,图层之外的部分就不会显示,我们重构一下上面的代码

public class PorterDuffXfer1View extends View {

    public PorterDuffXfer1View(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

        Bitmap dst = Bitmap.createBitmap(300,300,Config.ARGB_8888) ;
        Bitmap src = Bitmap.createBitmap(300,300,Config.ARGB_8888) ;
        Bitmap bmp = Bitmap.createBitmap(450, 450, Config.ARGB_8888) ;

        Canvas c1  = new  Canvas(dst) ;
        Canvas c2 = new  Canvas(src) ;
        Canvas c3 = new  Canvas(bmp) ;

        Paint p1 = new Paint() ;
        p1.setColor(Color.GRAY) ;
        c1.drawCircle(150, 150, 150, p1) ;

        Paint p2 = new Paint() ;
        p2.setColor(Color.GREEN) ;
        c2.drawRect(0, 0, 300, 300, p2) ;

        //定义画笔
        Paint paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
        //创建位图
        int layer = c3.saveLayer(150, 150, 450, 450, null, Canvas.ALL_SAVE_FLAG) ;
        //画圆
        c3.drawBitmap(dst, 0, 0,null) ;
        //定义位图的运算模式
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC));
        //画正方形
        c3.drawBitmap(src, 150, 150,paint) ;
        //清除运算效果
        paint.setXfermode(null) ;
        //恢复
        c3.restoreToCount(layer) ;
        canvas.drawBitmap(bmp, 0, 0,null) ;

    }
}
  不出所料,效果出来了。以上示例展示了 Mode.SRC 的用法, 强调说明:图层——layer 的位置和大小要根据实际情况进行设置

5.5、示例 1:圆形头像

  现在很多 App会网页在显示头像时(如,微博),不在使用千篇一律的方形,而是使用更加活波的圆形。手机或相机拍出来的照片都是矩形,如果要显示圆形,必须采用技术手段来解决。   我们先来分析一下基本的解决思路。将照片作为 DST,SRC 则是新创建的回来实心圆的位图,其实也是遮罩层。如果既要显示出 SRC 形状,又要显示 DST的内容,则必须使用 DST_IN 运算模式(DST 表示显示 DST 内容,IN 表示只显示香蕉部分)。我们首先来看一下初级代码
public class CirclePhotoView extends View {

    private Bitmap bmpCat ;
    private Bitmap bmpCircleMask ;
    private Canvas cvsCricle ;
    private Paint paint ;

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

        paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
        bmpCat = BitmapFactory.decodeResource(getResources(), R.drawable.cat) ;

        int minWidth = Math.min(bmpCat.getWidth(), bmpCat.getHeight()) ;

        bmpCircleMask = Bitmap.createBitmap(minWidth,minWidth,Config.ARGB_8888) ;
        cvsCricle = new Canvas(bmpCircleMask) ;
        int r = minWidth / 2 ;
        cvsCricle.drawCircle(r, r, r, paint) ;
    }

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

        canvas.drawBitmap(bmpCat, 0, 0,null) ;
        paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)) ;
        canvas.drawBitmap(bmpCircleMask,bmpCat.getWidth()/5 , 0,paint) ;
    }
}
  图片是一张萌死还不带负责的小奶猫,加载到 bmpCat 对象中。另外,创建一个名为 bmpCricleMask 的 bitmap 对象,并为该对象创建一个关联的 Canvas,在 bmpCirleMask 上画了一个实心圆,实心圆的直径为图片的短的边的长。bmpCircleMask同时也是遮罩层。运行如下:

  黑边,大黑边啊 !!!!遮罩层圆形位图的 4个角变成了黑色,我们要的应该是透明,要解决这个问题,必须使用图层 (layer)。创建一个和 bmpCircleMask 一样大小。将 DST(小猫) 和 SRC(实心圆) 都绘制在该图层上,奇迹就出现了。。

public class CirclePhoto1View extends View {

    private Bitmap bmpCat ;
    private Bitmap bmpCircleMask ;
    private Canvas cvsCircle ;
    private Paint paint ;

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

        bmpCat = BitmapFactory.decodeResource(getResources(), R.drawable.cat) ;
        paint = new  Paint(paint.ANTI_ALIAS_FLAG) ;

        int minWidth = Math.min(bmpCat.getWidth(), bmpCat.getHeight()) ;
        bmpCircleMask = Bitmap.createBitmap(minWidth,minWidth,Config.ARGB_8888)  ;
        cvsCircle = new  Canvas(bmpCircleMask) ;

        int r = minWidth / 2 ;
        cvsCircle.drawCircle(r, r, r, paint) ;

    }

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

        int w = bmpCat.getWidth()/5 ;

        int layer = canvas.saveLayer(w, 0, bmpCircleMask.getWidth()+w, bmpCircleMask.getHeight()
                , null, Canvas.ALL_SAVE_FLAG) ;

        canvas.drawBitmap(bmpCat, 0, 0,null) ;
        paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN) ) ;
        canvas.drawBitmap(bmpCircleMask, w, 0,paint) ;
        canvas.restoreToCount(layer) ;

    }
}
  运行如下,已经完全实现了想要的结果,黑边不见了

  利用这个方法,我们其实可以实现任意形状的图片,本质上,遮罩层是什么形状,图片就会显示什么形状。如果熟悉 Photoshop中的蒙版,其实 layer 和 蒙版的概念基本相同。需要注意:遮罩层最好使用 .png图片,这种格式的图片才支持透明像素,才能真正的不规则照片。如下图 就是将小猫和Android 机器人通过 DST_IN 位图运算后得到的效果

  代码实现和上面一模一样,下面是源码

public class AnomalousPhotoView extends View {

    private Bitmap bmpCat ;
    private Bitmap bmpMask ;
    private Paint paint ;

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

        bmpCat = BitmapFactory.decodeResource(getResources(), R.drawable.cat) ;
        bmpMask = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher) ;

        paint = new  Paint(Paint.ANTI_ALIAS_FLAG) ;
    }

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

        int w = bmpCat.getWidth()/2 ;
        int h = bmpCat.getHeight()/5;

        int layer = canvas.saveLayer(w, h, w+bmpMask.getWidth(), h+bmpMask.getHeight(),
                null, Canvas.ALL_SAVE_FLAG) ;

        canvas.drawBitmap(bmpCat, 0, 0,null) ;
        paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)) ;
        canvas.drawBitmap(bmpMask, w,h,paint) ;

        canvas.restoreToCount(layer) ;
    }
}

5.6 示例 2,刮刮乐

  刮刮乐是个很好玩的小应用,娱乐性很强,靠的是运气。在图片上随机产生一个中间信息,蒙上一层颜色,用户使用手指在屏幕上涂挂,颜色即被擦除,最后看到中间信息

  从技术上实现来说,刮刮乐有两层图层,一个是不会变化的中奖信息图层,一个是蒙上一层灰色的图层。当用户手指在屏幕上涂抹时,我们需要将灰色抹掉。中奖信息其实并不是需要改变的,换句话说,涂抹时,无需重绘中奖信息

  所以,对于中奖信息位图来说,因该采用更简单的实现,可以在图片上中奖信息后作为 View的背景(Backgroud),当手指在屏幕上涂抹时就不需要考虑他的重绘问题了,我们在创建一个 Bitmap对象,初始蒙上一层灰色,手指在屏幕上移动时同时绘制线条,将线条与灰色做 Mode.CLEAR 运算,相交部分即被清除,变成了透明效果,于是我们就能看到背景了

  实现刮刮乐需要经历下面啷个步骤:

  • 绘制背景
  • 背景需要一张图片,资源中的图片不能编辑,所以必须调用 Bitmap 的 copy( )方法,复制一张同样的图片并设置可编辑的标识,画上随机生成的中奖信息,调用 View类的 public void setBackground(Drawable background)方法设置背景(该方法有兼容性问题)

  • 在屏幕上绘制线条
  • 定义一个 Bitmap 对象,初始画上一层灰色,当手指在屏幕上移动时,不断绘制曲线,曲线和灰色做 Mode.CLEAR 运算,实现清除效果

  下面代码是对其大致的实现

public class GuaGuaLeView extends View {

    private Random random ;
    private Paint paint ;
    private Paint clearPaint ;
    private static final String[] PRIZE = {
        "恭喜,你中了一等奖,奖金 1亿元",
        "恭喜,你中了二等奖,奖金 5000万元",
        "恭喜,你中了三等奖,奖金 100元",
        "很遗憾,你没有中奖,继续加油"
    } ;

    /**涂抹的粗细*/
    private static final int FINGET = 50 ;
    /**缓冲区*/
    private Bitmap bmpBuffer ;
    /**缓存区画布*/
    private Canvas cvsBuffer ;
    private int curX,curY ;

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

        random = new Random() ;
        paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
        paint.setTextSize(36) ;
        paint.setColor(Color.BLUE) ;

        clearPaint = new  Paint(paint.ANTI_ALIAS_FLAG) ;
        clearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
        clearPaint.setStrokeWidth(FINGET) ;
        clearPaint.setStrokeCap(Cap.ROUND) ;
        clearPaint.setStrokeJoin(Join.ROUND) ;

        //画背景
        drawBackground();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //初始化缓冲区
        bmpBuffer = Bitmap.createBitmap(w,h,Config.ARGB_8888) ;
        cvsBuffer = new Canvas(bmpBuffer) ;
        //为缓冲区蒙上灰色
        cvsBuffer.drawColor(Color.parseColor("#ff808080")) ;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bmpBuffer, 0, 0,paint) ;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            curX = x;
            curY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            cvsBuffer.drawLine(curX, curY, x, y, clearPaint) ;
            invalidate() ;
            curX = x;
            curY = y;
            break ;
        case MotionEvent.ACTION_UP:
            invalidate() ;
            break ;
        default:
            break;
        }

        return true;
    }

    /**
     * 绘制背景,齐总包含背景图片,中奖信息
     */
    private void drawBackground() {

        Bitmap bmpBackgroud = BitmapFactory.decodeResource(getResources(), R.drawable.luxi) ;
        //从资源中读取的  bmpBackgroud不可以修改,复制出一张可以修改的图片
        Bitmap bmpBackgroundMutable = bmpBackgroud.copy(Config.ARGB_8888, true) ;
        //在图片上画出中间信息
        Canvas cvsBackground = new  Canvas(bmpBackgroundMutable) ;

        //计算出文字所占的区域,将文字放在正中间

        String text = PRIZE[getPrizeIndex()] ;
        Rect rect = new  Rect() ;
        paint.getTextBounds(text, 0, text.length(), rect) ;
        int x = (bmpBackgroundMutable.getWidth() - rect.width()) /2 ;
        int y = (bmpBackgroundMutable.getHeight() - rect.height()) / 2 ;
        this.setLayerType(View.LAYER_TYPE_SOFTWARE, paint) ;
        paint.setShadowLayer(10, 0, 0, Color.GREEN) ;
        cvsBackground.drawText(text, x, y, paint) ;
        paint.setShadowLayer(0 , 0, 0, Color.YELLOW) ;

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){
            this.setBackground(new BitmapDrawable(getResources(), bmpBackgroundMutable)) ;
        }else{
            this.setBackground(new BitmapDrawable(bmpBackgroundMutable)) ;
        }
    }

    /**随机生成中间信息*/
    private int getPrizeIndex(){

        return random.nextInt(PRIZE.length) ;
    }
}
  运行效果如下:
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值