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

时间:2023-02-10 13:04:36

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


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

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

  上面的代码中,我们绘制了两行文字。第一行“世上无难事只怕有心人”为红色发光效果。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 常量来表示
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

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

5.3.1 线性渐变(LinearGradient)

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

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

  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 绘图模式)
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

  如果两种无法满足绘图需求时,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 所示

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

  从上图可以看出,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) ;
    }
}
运行如下:
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
5.3.2 径向渐变(RadialGradient)  径向渐变是以指定的点为中心,向四周以渐变颜色进行圆周扩散,和线性渐变一样。支持两种或多种颜色。径向渐变的示意图如图 5-8 所示
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

  • 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。在大部分时候,镜像模式的渐变效果看起来会更舒服讨人喜爱

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

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

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的完整五子连珠视屏链接):
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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°度
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

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

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,偶数行的小机器人与技术行是垂直翻转的
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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种运算模式如图所示。其实从命名就能大概知道么中位图的含义
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

  下面的案例是将两种渐变(线性渐变和位图渐变)进行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) ;

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

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

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



5.4、位图运算


5.4.1 PorterDuffXfermode

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

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

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

  位图运算模式定义在 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 代表上层(正方形)
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
CLEAR 绘制的内容不会提交到画布
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DARKEN 取两图层全部区域,交集部分颜色加深
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DST 显示下层位图
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DST_ATOP 取上层非交集与下层交集部分
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DST_IN 取两层的交集部分,交集内容取决于下层
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DST_OUT 取下层的非交集部分
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
DST_OVER 上下层都显示,运算后下层在上面
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
LIGHTEN 取两层的全部内容,点亮交集部分的颜色
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
MULTIPLY 取两图层交集部分并叠加颜色
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SCREEN 取两图层全区域,交集部分变为透明
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SRC 显示上层位图
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SRC_ATOP 取下层非交集部分与上层交集部分
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SRC_IN 取两层交集部分,交集内容取决于上层
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SRC_OUT 取上层的非交集部分
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
SRC_OVER 上下层都显示,运算后上层在上面
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
XOR 异或操作,去除两层交集部分
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
  为了实现位图运算,创建 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 所示
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

  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所示
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

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

  如图 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同时也是遮罩层。运行如下:
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

  黑边,大黑边啊 !!!!遮罩层圆形位图的 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) ;

    }
}
  运行如下,已经完全实现了想要的结果,黑边不见了
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算

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

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

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

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) ;
    }
}
  运行效果如下:
株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算