第五章 阴影、渐变和位图的运算
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:阴影的颜色
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) ;
}
}
运行如下:
径向渐变的主要构造方法如下:
- 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 | 异或操作,去除两层交集部分 |
|
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) ;
}
}
运行效果如下: