第7章 CustomView绘图进阶

一、贝济埃曲线

概述:

在Path系列函数中,除了一些基本的设置和绘图用法外,还有一个强大的工具——贝济埃曲线。它能将利用moveTo、lineTo连续的生硬路径变得平滑,也能够实现很多炫酷的效果,比如水波纹等。

1.贝济埃曲线的来源

1962年法国工程师皮埃尔·贝济埃发表,他运用贝济埃曲线来为汽车的主体进行设计。

在数学的数值分析领域中,贝济埃曲线是计算机图形学中相当重要的参数曲线。更高维度的广泛化贝济埃曲线就称作贝济埃曲面,其中贝济埃三角是一种特殊的实例。

2.贝济埃曲线的公式

1)一阶贝济埃曲线

公式如下:

B(t) = (1-t)P_{_{0}} + tP_{_{1}},t\in [0,1]

P_{0}为起始点,P_{1}为终点,t表示当前时间,B(t)表示公式的结果值。

注意:曲线的含义就是随着时间的变化,公式的结果值所形成的轨迹。

在下面动画中,黑色点表示在当前时间t下公式的B(t)的取值;而红色的那条线就表示在各个时间点下不同取值的B(t)所形成的轨迹。

总而言之,对于一阶贝济埃曲线,可以理解为在由起始点和终点形成的这条直线上匀速移动的点。

2)二阶贝济埃曲线

B(t)=(1-t)^{^{2}}P_{0} + 2t(1-t)P_{1}+t^{2}P_{2},t\in [0,1]

P_{0}是起始点,P_{2}是终点,P_{1}是控制点。

P_{0}P_{1}形成了一条一阶贝济埃曲线,Q_{0}在这条直线上随时间匀速运动。

P_{1}P_{2}也形成了一条一阶贝济埃曲线,Q_{1}在这条直线上随时间匀速运动。

动态点Q_{0}Q_{1}又形成了一条一阶贝济埃曲线,在这条曲线上动态移动的点是B,而B点的移动轨迹就是二阶贝济埃曲线的最终形态。

从上面可知,之所以称为二阶贝济埃曲线,是因为B点的移动轨迹是建立在两条一阶贝济埃曲线的中间点Q_{0}Q_{1}的基础上的。

3)三阶贝济埃曲线

B(t)=P_{0}(1-t)^{3}+3P_{1}t(1-t)^{2}+3P_{2}t^2(1-t)+P_{3}t^3,t\in [0,1]

同样P_{0}是起始点,P_{3}是终点,P_{1}是第一个控件点,P_{2}是第二个控件点。

这里有三条一阶贝济埃曲线,分别是P_{0}P_{1}P_{1}P_{2}P_{2}P_{3},它们随时间变化的点分别为Q_{0}Q_{1}Q_{2},然后这三点相连形成两条一阶贝济埃曲线,分别是Q_{0}Q_{1}Q_{1}Q_{2},它们随时间变化的点为R_{0}R_{1}。同样,R_{0}R_{1}可以连接形成一条一阶贝济埃曲线,在R_{0}R_{1}这条贝济埃曲线上随时间移动的点是B,而B点的移动轨迹就是三阶贝济埃曲线的最终形状。

从上面可看出,所谓几阶贝济埃曲线,全部是由一条条一阶贝济埃曲线搭起来的。在上图中,P_{0}P_{1}P_{2}P_{3}形成一阶贝济埃曲线,Q_{0}Q_{1}Q_{2}形成二阶贝济埃曲线,R_{0}R_{1}形成三阶贝济埃曲线。

4)四阶贝济埃曲线

5)五阶贝济埃曲线

对于四阶和五阶贝济埃曲线,在Android中是用不到的,Path最多支持到三阶贝济埃曲线,所以对它们的形成原理也不再介绍了。

3.贝济埃曲线与Photoshop钢笔工具

在绘图工具Photoshop中的钢笔工具,它所使用的路径弯曲效果就是二阶贝济埃曲线。

    

贝济埃曲线之quadTo:

在Path类中有4个函数与贝济埃曲线相关,分别如下:

// 二阶贝济埃曲线
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
// 三阶贝济埃曲线
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

1.quadTo使用原理

public void quadTo(float x1, float y1, float x2, float y2)
● (x1,y1)是控制点坐标
● (x2,y2)是终点坐标
对于起始点,如果没有调用Path.moveTo(x,y)指定,默认以控件左上角点(0,0)为起始点。

尝试画出一条波浪线。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Path path = new Path();
    path.moveTo(100, 300);
    path.quadTo(200, 200, 300, 300);
    path.quadTo(400, 400, 500, 300);
    canvas.drawPath(path, paint);
}

需要注意的是,第一个起始点是需要调用path.moveTo(l00,300)函数来指定的,后一个path.quadTo()函数是以前一个path.quadTo()函数的终点为起始点的。所以,一般在使用贝济埃曲线寻找控制点时,如果没有思路,则可以尝试使用Photoshop的钢笔工具画图分析。而且在自定义控件的时候,如果UED(视觉设计人员)在实现一个效果时使用的是钢笔工具,那么在代码中就可以尝试使用贝济埃曲线来实现。

2.示例:传统捕捉手势轨迹

只需在自定义控件中拦截OnTouchEvent,然后根据手指的移动轨迹来绘制Path即可。

public class NormalGestureTrackView extends View {
    private Path mPath = new Path();
    private Paint mPaint;

    public NormalGestureTrackView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
    }
    ...
}
public boolean onTouchEvent(MotionEvent event) {
    switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPath.moveTo(event.getX(), event.getY());
            return true;
        case MotionEvent.ACTION_MOVE:
            mPath.lineTo(event.getX(), event.getY());
            postInvalidate();
            break;
        default:
            break;
    }
    return super.onTouchEvent(event);
}
    

这里有两点要注意:

第一,case MotionEvent.ACTION_DOWN时返回true的问题。返回true表示当前控件已经消费了下按动作,之后的ACTION_MOVE、ACTION_UP动作也会继续传递到当前控件中;如果case MotionEvent.ACTION_DOWN时返回false,那么后续ACTION_MOVE、ACTION_UP动作就不会再传递到这个控件中。

第二,重绘控件使用的是postInvalidate()函数,也可以使用Invalidate()函数。区别是Invalidate()函数一定要在主线程中执行,否则报错;而postInvalidate()函数可以在任何线程中执行。其实,在 postlnvalidate()函数中就是利用handler给主线程发送刷新界面的消息来实现的,所以它可以在任何线程中执行而不会出错。而正因为它是通过发送消息来实现的,所以它的界面刷新速度可能没有直接调用Invalidate()函数那么快。所以,在确定当前线程是主线程的情况下,还是以使用Invalidate()函数为好。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    canvas.drawPath(mPath, mPaint);
}

我们把画出的图像放大,可以明显看出,在两点连接处有明显的转折,而且S顶部横、纵坐标变化比较快的位置,看起来跟图片放大后的马赛克效果一样。

然而,利用Path绘图是不可能出现马赛克现象的,因为除位图以外的任何Canvas绘图都是失量图,也就是利用数学公式作出来的图,无论如何放大,都是不可能出现马赛克现象的。

这里之所以看起来像马赛克效果,是因为曲线是由各个不同点之间连接而成的,而线与线之间并没有平滑过渡,所以当坐标变化比较剧烈时,线与线之间的转折就显得特别明显。

3.优化:使用quadTo()函数实现手势过渡

从两条线段可以看出,我们在使用Path.lineTo()函数时,直接把手指触点A、B、C连接起来了。而钢笔工具要实现这三个点之间的流畅过渡,就只能将这两条线段的中间点作为起始点和终点,而将手指的倒数第二个触点B作为控制点。

这样做,在结束的时候,A到P0和P1到C的距离岂不是没画进去?是的,如果Path最终没有闭合,那么这两段距离是被抛弃的。因为手指在滑动时,每两个点之间的距离很小,所以P1到C的距离可以忽略不计。

private float mPreX, mPreY;
public boolean onTouchEvent(MotionEvent event) {
    switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mPath.moveTo(event.getX(), event.getY());
            mPreX = event.getX();
            mPreY = event.getY();
            return true;
        case MovetionEvent.ACTION_MOVE:
            float endX = (mPreX + event.getX()) / 2;
            float endY = (mPreY + event.getY()) / 2;
            mPath.quadTo(mPreX, mPreY, endX, endY);
            mPreX = event.getX();
            mPreY = event.getY();
            invalidate();
        default:
            break;
    }
    return super.onTouchEvent(event);
}

当接收到ACTION_DOWN消息的时候,利用mPath.moveTo(event.getX(), event.getY())函数将Path的初始位置设置到手指的触点处。如果不调用mPath.moveTo()函数,则默认是从点(0, 0)开始的。然后定义两个变量mPreX,mPreY来表示手指的前一个点。通过上面的分析知道,这个点是用来做控制点的。最后返回true,让 ACTION_MOVE、ACTION_UP事件继续向这个控件传递。
当接收到ACTION_MOVE消息的时候,需要先找到当前手指所在位置要绘制贝济埃曲线的终点,我们说过,终点是这条线段的中间位置,所以很容易求出它的坐标endX,endY;控制点是上一个手指位置,即mPreX,mPreY;起始点就是上一条线段的中间点。这样一来就与钢笔工具的绘制过程完全对应了:把各条线段的中间点作为起始点和终点,把终点前一个手指位置作为控制点。

 如图,除了第一次起始点和控件点都是(mPreX,mPreY),之后的起点和终点都是线段的中心点。

绘制顺序:
(1)起始点-终点-控制点:黑(mPreX,mPreX)-黑(endX,endY)-黑(mPreX,mPreX)【绿色路径】
(2)起始点-终点-控制点:黑(endX,endY)-橙(endX,endY)-橙(mPreX,mPreY)【湛蓝色路径】
(3)起始点-终点-控制点:橙(endX,endY)-绿(endX,endY)-绿(mPreX,mPreY)【粉色路径】
因为quadTo参数列表为(起点,终点),所以控件点要找准。
还要注意,上一次的终点在哪,这样才知道下一次的起点。

    

从对比图中可以明显看出,通过quadTo()函数实现的曲线更顺滑。

贝济埃曲线之rQuadTo:

1.概述

public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
● dx1:控制点x坐标,表示【相对上一个终点】x坐标的位移值。正值表相加,负值表相减。
● dy1:控制点y坐标,表示【相对上一个终点】y坐标的位移值。正值表相加,负值表相减。
● dx2:终点x坐标,表示【相对上一个终点】x坐标的位移值。正值表相加,负值表相减。
● dy2:终点y坐标,表示【相对上一个终点】y坐标的位移值。正值表相加,负值表相减。
这4个参数传递的都是相对值,即相对上一个终点的位移值。
比如【上一个终点坐标】=(300, 400)
rQuadTo(100, -100, 200, 100)
=> 控制点坐标=(300+100, 400-100)=(400, 300)
=> 终点坐标=(300+200, 400+100)=(500, 500)
path.moveTo(300, 400);
path.quadTo(400, 300, 500, 500);
等价于<=>
path.moveTo(300, 400);
path.rQuadTo(100, -100, 200, 100);

2.使用rQuadTo()函数实现波浪线

Path path = new Path();
path.moveTo(100, 300);
path.quadTo(200, 200, 300, 300);
path.quadTo(400, 400, 500, 300);
canvas.drawPath(path, paint);
————————转换为rQuadTo()函数实现————————
Path path = new Path();
path.moveTo(100, 300);
path.rQuadTo(100, -100, 200, 0);
path.rQuadTo(100, 100, 200, 0);
canvas.drawPath(path, paint);

示例:波浪效果

1.实现全屏波纹

只要我们再多实现几个波形,就可以覆盖整个屏幕了。

首先,在构造函数中,初始化一些必要的变量。

public class AnimWaveView extends View {
    private Paint mPaint;
    private Path mPath;
    private int mItemWaveLength = 1200;
    private int dx;
    public AnimWaveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL);
    }
    ...
}

要构造Paint时,需要把它改为填充模式。然后在onDraw()函数中整屏画满波形。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPath.reset();
    int originY = 300;
    int halfWaveLen = mItemWaveLength / 2;
    mPath.moveTo(-mItemWaveLength, originY);
    for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
        mPath.rQuadTo(halfWaveLen / 2, -100, halfWaveLen, 0);// 波长中的前半个波
        mPath.rQuadTo(halfWaveLen / 2, 100, halfWaveLen, 0);// 波长中的后半个波
    }
    canvas.drawPath(mPath, mPaint);
}

利用for循环画出当前屏幕中可以容纳得下的所有波形。可以看到,屏幕左右都多画了一个波长的图形,这是为了波形移动做准备的。到这里,已经能画出一整屏的波形了,最后把整体波形闭合起来。

mPath.lineTo(getWidth(), getHeight());
mPath.lineTo(0, getHeight());
mPath.close();

下图中所标出的区域就是利用以上代码lineTo()函数闭合的区域,屏幕两边多出的波形在闭合以后是看不到的。

2.实现移动动画

调用path.moveTo()函数的时候,将起始点向右移动即可实现。而且只要我们移动一个波长的长度,波纹就会重合,就可以实现无限循环。

public void startAnim() {
    ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
    animator.setDuration(2000);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            dx = (int) animation.getAnimatedValue();
            postInvalidate();
        }
    });
    animator.start();
}

动画的长度为一个波长,将当前值保存在类的成员变量dx中。

在初始化时开始动画。

public AnimWaveView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // 初始化Paint,省略
    ...
    startAnim();
}

在画图时,只需要在path.moveTo()函数中加上现在的移动值即可。

mPath.moveTo(-mItemWaveLength + dx, originY);

完整绘图代码:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPath.reset();
    int originY = 300;
    int halfWaveLen = mItemWaveLength / 2;
    mPath.moveTo(-mItemWaveLength + dx, originY);
    for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
        mPath.rQuadTo(halfWaveLen / 2, -100, halfWaveLen, 0);// 波长中的前半个波
        mPath.rQuadTo(halfWaveLen / 2, 100, halfWaveLen, 0);// 波长中的后半个波
    }
    mPath.lineTo(getWidth(), getHeight());
    mPath.lineTo(0, getHeight());
    mPath.close();
    canvas.drawPath(mPath, mPaint);
}

二、setShadowLayer与阴影效果

setShadowLayer()函数能实现如下效果:

● 定制阴影模糊程度

● 定制阴影偏移距离

● 清除和显示阴影

setShadowLayer()构造函数:

1.概述

public void setShadowLayer(float radius, float dx, float dy, int color)
● radius:模糊半径,值越大越模糊,为0时,阴影消失不见
● dx:阴影的横向偏移距离,正值向右,负值向左
● dy:阴影的纵向偏移距离,正值向下,负值向上
● color:阴影的颜色(对图片阴影无效)

setShadowLayer()函数使用的是高斯模糊算法。高斯模糊的具体算法是:对于正在处理的每一个像素,取周围若干个像素的RGB值并且平均,这个平均值就是模糊处理过的像素。如果对图片中的所有像素都这么处理,那么处理完成的图片就会变得模糊。其中,所取周围像素的半径就是模糊半径。所以,模糊半径越大,所得平均像素与原始像素相差就越大,也就越模糊。

绘制阴影的画笔颜色为什么对图片无效?

使用setShadowLayer()函数所产生的阴影,文字和绘制的图形的阴影都是使用自定义的阴影画笔颜色来绘制的;而图片的阴影则是直接产生一张相同的图片,仅对阴影图片的边缘进行模糊。之所以生成一张相同的阴影图片,是因为如果统一使用某种颜色来绘制阴影,则可能会与图片的颜色相差很大,而且不协调,比如某张图片的色彩非常丰富,而阴影如果使用灰色来做,可能就会显得很突兀。为了解决这个问题,针对图片的阴影就不再统一颜色了,而是复制出这张图片,把复制出来的图片的边缘进行模糊,作为阴影。但这样做又会引发一个问题:如果我们只想将图片阴影做成灰色怎么办?使用setShadowLayer()函数自动生成阴影是没办法了,在7.3节中将具体讲述如何给图片添加纯色阴影。

注意:setShadowLayer()函数只有文字绘制阴影支持硬件加速,其他都不支持硬件加速。所以,为了方便起见,需要在自定义控件中禁用硬件加速。

2.示例

public class ShadowLayerView extends View {
    private Paint mPaint = new Paint();
    private Bitmap mDogBmp;
    public ShadowLayerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(25);
        mPaint.setShadowLayer(1, 10, 10, Color.GRAY);
        mDogBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
    }
    ...
}

在绘图时,将所有内容绘制出来即可。 

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    canvas.drawText("阿宽", 100, 100, mPaint);
    canvas.drawCircle(300, 100, 50, mPaint);
    canvas.drawBitmap(mDogBmp, null, 
                      new Rect(500, 50, 500 + mDogBmp.getWidth(), 50 + mDogBmp.getHeight()),
                      mPaint);
}

3.setShadowLayer()函数各参数的含义

如上效果很容易理解各参数含义。这里有两点要注意:

● 图片的阴影是不受阴影画笔颜色影响的,它是一张图片的副本。

● 无论图片还是图形,模糊时,仅模糊边界部分,随着模糊半径的增大,向内、向外延伸。

清除阴影:

方法一:setShadowLayer()函数中radius参数设置为0
方法二:public void clearShadowLayer()

public void setShadow(boolean showShadow) {
    mSetShadow = showShadow;
    postInvalidate();
}
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mSetShadow) {
        mPaint.setShadowLayer(mRadius, mDx, mDy, Color.GRAY);
    } else {
        mPaint.clearShadowLayer();
    }
    canvas.drawText("启舰", 100, 100, mPaint);
    canvas.drawCircle(200, 200, 50, mPaint);
    canvas.drawBitmap(mDogBmp, null, 
                      new Rect(200, 300, 200 + mDogBmp.getWidth(), 300 + mDogBmp.getHeight()),
                      mPaint);
}

示例:给文字添加阴影

1.通过XML属性添加阴影

<TextView
    ...
    android:shadowRadius="3"
    android:shadowDx="5"
    android:shadowDy="5"
    android:shadowColor="@android:color/darker_gray"/>

setShadowLayer()是在API 1时引入的函数,而且添加了TextView类和它的派生类来支持阴影设置。

TextView的派生类如下:

public class TextView
extends View implements ViewTreeObserver.OnPreDrawListener

java.lang.Object
   ↳android.view.View
    ↳android.widget.TextView
Known direct subclasses

ButtonCheckedTextViewChronometerDigitalClockEditTextTextClock

从图中可以看到TextView、EditText、Button中的文字自动添加了阴影。而且对于EditText而言,新输入的文字依然有阴影效果。

2.通过代码添加阴影

TextView及其派生类都有一个Paint.setShadowLayer的同名函数,如下:

public void setShadowLayer(float radius, float dx, float dy, int color)
———————————————————————————————————————————————————————————————————————
可以很容易地实现TextView及其派生类的阴影:
TextView tv = (TextView) findViewById(R.id.tv);
tv.setShadowLayer(2, 5, 5, Color.GRAY);

三、BlurMaskFilter发光效果与图片阴影

这张效果图中涉及三个发光效果:文字、图形和位图。

对发光效果,有如下结论:

● 与setShadowLayer()函数一样,发光效果使用的也是高斯模糊算法,并且只会影响边缘部分图像,内部图像是不受影响的。

● 发光效果是无法指定发光颜色的,采用边缘部分的颜色取样来进行模糊发光。所以,边缘是什么颜色,发出的光就是什么颜色的。

概述:

1.setMaskFilter()函数的简单使用

public MaskFilter setMaskFilter(MaskFilter maskfilter)
———————————————————————————————————————————————————————
● MaskFilter有两个派生类:
➀BlurMaskFilter:能够实现发光效果。
➁EmbossMaskFilter:可以用于实现浮雕效果,用处很少。
注:setMaskFilter()函数不支持硬件加速,必须关闭硬件加速才可以。
public BlurMaskFilter(float radius, Blur style)
● radius:模糊半径,同样采用高斯模糊算法。
● style:发光样式:Blur.INNER(内发光)、Blur.SOLID(外发光)、
                  Blur.NORMAL(内外发光)、Blur.OUTER(仅显示发光效果)
public class BlurMaskFilterView extends View {
    private Paint mPaint;
    public BlurMaskFilterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.INNER));
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(200, 200, 100, mPaint);
    }
}

2.BlurStyle发光效果

给图片添加纯色阴影:

先来分析一下setShadowLayer()函数的阴影形成过程(假定阴影画笔是灰色的)。对于文字和图形,首先产生一个跟原型一样的灰色副本;然后对这个灰色副本应用BlurMaskFilter,使其内外发光;最后偏移一段距离,这样就形成了所谓的阴影。
所以,我们要给图片添加灰色阴影效果,就可以仿照这个过程:首先绘制一幅跟图片一样大小的灰色图像,然后给灰色图像应用BlurMaskFilter使其内外发光,最后偏移原图形一段距离绘制阴影。
这里涉及三点:
● 绘制一幅根图片一样大小的灰色图像。
● 对灰色图像应用BlurMaskFilter使其内外发光。
● 偏移原图形一段距离绘制阴影。

1.抽取灰色图像

如果我们需要画一张位图所对应的灰色图像,就需要新建一张一样大小的空白图片,而且,新图片的透明度要与原图片保持一致。这样一来,如何从原图片中抽出Alpha值成为关键。即只需要创建一张与原图片一样大小且Alpha值相同的图片即可。

public Bitmap extractAlpha();
—————————————————————————————
功能:新建一张空白图片,该图片具有与原图片一样的Alpha值,把这个新建的Bitmap作为结果返回。
public BitmapShadowView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    mPaint = new Paint();
    mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cat_dog);
    mAlphaBmp = mBitmap.extractAlpha();
}

上述代码,先禁用硬件加速,这基本上是做自定义控件的标配;再利用extractAlpha()函数来生成仅具有透明度的空白图像。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = 400;
    int height = width * mAlphaBmp.getHeight() / mAlphaBmp.getWidth();
    // 绘制灰色阴影
    mPaint.setColor(Color.GRAY);
    canvas.drawBitmap(mAlphaBmp, null, new Rect(10, 10, width, height), mPaint);
    // 绘制黑色阴影
    canvas.translate(width, 0);
    mPaint.setColor(Color.BLACK);
    canvas.drawBitmap(mAlphaBmp, null, new Rect(10, 10, width, height), mPaint);
}

2.绘制阴影

在上面灰色纯色图像的基础上,将此灰色图像使用BlurMaskFilter使其内外发光。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = 400;
    int height = width * mAlphaBmp.getHeight() / mAlphaBmp.getWidth();

    mPaint.setColor(Color.GRAY);
    canvas.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
    canvas.drawBitmap(mAlphaBmp, null, new Rect(10, 10, width, height), mPaint);
}

再在灰色模糊阴影的基础上画上原图像,就形成了模糊阴影。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = 400;
    int height = width * mAlphaBmp.getHeight() / mAlphaBmp.getWidth();
    // 绘制阴影
    mPaint.setColor(Color.GRAY);
    canvas.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
    canvas.drawBitmap(mAlphaBmp, null, new Rect(10, 10, width, height), mPaint);
    // 绘制原图像
    canvas.translate(-5, -5);
    mPaint.setMaskFilter(null);
    canvas.drawBitmap(mBitmap, null, new Rect(0, 0, width, height), mPaint);
}

在绘制原图像前,先将原图像向左上角移动一部分,这样就可以将阴影露出一部分,而不会完全被原图像所覆盖;然后把MaskFilter置空,否则新画出来的图像也将具有模糊效果;最后将原图像画出来。

  

四、Shader与BitmapShader

Shader概述:shader [ˈʃeɪdər]:着色器; 着色程序;

Shader在三维软件中被称为着色器,是用来给空白图形上色的。在Photoshop中有一个印章工具,能够指定印章的样式来填充图形。印章的样式可以是图像、颜色、渐变色等。这里的Shader实现的效果与印章类似,我们也是通过给Shader指定对应的图像、渐变色等来填充图形的。

public Shader setShader(Shader shader)
——————————————————————————————————————
Shader类只有一个基类,其中只有两个函数:
setLocalMatrix(Matrix localM)和getLocalMatrix(Matrix localM),用来设置坐标变换矩阵。

public class Shader
extends Object

java.lang.Object
   ↳android.graphics.Shader
Known direct subclasses

BitmapShaderComposeShaderLinearGradientRadialGradientSweepGradient

Shader is the based class for objects that return horizontal spans of colors during drawing. A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader.

Shader类作为基类主要是返回绘制时的水平距离,它的子类可以通过Paint.setShader(shader)添加到Paint里,然后获取shader的颜色进行绘制。

可以看到,Shader其实是一个空类,它的功能主要是靠它的派生类来实现的。

BitmapShader的基本用法:

public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
——————————————————————————————————————————————————————————————————
这就相当于Photoshop中的印章工具。
● bitmap:指定图案
● tileX:指定当X轴【[超出]单张图片大小】时所使用的重复策略
● tileY:指定当Y轴【[超出]单张图片大小】时所使用的重复策略
TileMode取值如下:
● TileMode.CLAMP:用边缘色彩来填充多余空间
● TileMode.REPEAT:重复原图像来填充多余空间
● TileMode.MIRROR:重复使用镜像模式的图像来填充多余空间

1.示例

public class BitmapShaderView extends View {
    private Paint mPaint;
    private Bitmap mBmp;
    public BitmapShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        mBmp = BitmapFactory.decodeResource(getResources(), R.drawable.dog_edge);
        mPaint.setShader(new BitmapShader(mBmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    }
}
<com.example.customwidgets.BitmapShaderView
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:layout_margin="20dp" />

  

2.TileMode模式解析

mPaint.setShader(new BitmapShader(mBmp, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR));

mPaint.setShader(new BitmapShader(mBmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));

 (先竖向填充,再横向填充)

mPaint.setShader(new BitmapShader(mBmp, Shader.TileMode.MIRROR, Shader.TileMode.REPEAT);

记住,无论哪两种模式混合,先填充Y轴,再填充X轴。

3.绘图位置与图像显示

假如我们只画一个小矩形而不完全覆盖整个控件,那么setShader()函数中所设置的图片是从哪里开始画的呢?

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    float left = getWidth() / 3;
    float top = getHeight() / 3;
    float right = getWidth() * 2 / 3;
    float bottom = getHeight() * 2 / 3;
    mPaint.setShader(new BitmapShader(mBmp, Shader.TileMode.MIRROR, Shader.TileMode.REPEAT));
    canvas.drawRect(left, top, right, bottom, mPaint);
}

在绘图时,并不是完全覆盖控件大小,而是取控件中间位置的1/3区域显示。

可见,这张效果图是从上例示例部分的完整图片中抠出来的一小部分。

其实这正好说明了一个问题:无论利用绘图函数绘制多大的图像、在哪里绘制,都与Shader无关。因为Shader总是从控件的左上角开始的,我们绘制的只是显示出来的部分而己。没有绘制的部分虽然已经生成,但是不会显示出来。

示例一:望远镜效果

原理:先准备一张背景图,然后将背景图作为BitmapShader,只需要在手指所在位置画一个圆,就可以将圆形部分的图像显示出来了。

public class TelescopeView extends View {
    private Paint mPaint;
    private Bitmap mBitmap, mBitmapBG;
    private int mDx = mDy = -1;
    public TelescopeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
    }
    ...
}
<com.example.customwidgets.TelescopeView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
public boolean onTouchEvent(MotionEvent event) {
    switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDx = (int) event.getX();
            mDy = (int) event.getY();
            postInvalidate();
            return true;
        case MotionEvent.ACTION_MOVE:
            mDx = (int) event.getX();
            mDy = (int) event.getY();
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mDx = -1;
            mDy = -1;
            break;
    }
    postInvalidate();
    return super.onTouchEvent(event);
}
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mBitmapBG == null) {
        mBitmapBG = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);// 新建一个透明的空bitmap
        Canvas canvasbg = new Canvas(mBitmapBG);
        canvasbg.drawBitmap(mBitmap, null, new Rect(0, 0, getWidth(), getHeight()), mPaint);// 此时屏幕仍是一片空白
    }
    if (mDx != -1 && mDy != -1) {
        mPaint.setShader(new BitmapShader(mBitmapBG, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
        canvas.drawCircle(mDx, mDy, 150, mPaint);
    }
}

我们主要来看下OnDraw函数:
在onDraw函数中,第一部分,就是新建一个空的透明的bitmap,这个bitmap的大小与控件一样,然后把我们的背景图进行拉伸,画到这个空白的bitmap上。 

以下代码作用仅仅是将望远镜看的原图mBitmap按控件大小进行伸缩绘制到承载它的画面的mBitmapBG上。以后就用它来作Shader的指定图案。

if (mBitmapBG == null) {
    // 新建一个透明的空bitmap
    mBitmapBG = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvasbg = new Canvas(mBitmapBG);
    // 此时屏幕仍是一片空白,但已将mBitmap绘制在承载canvasbg画面的mBitmapBG上了
    canvasbg.drawBitmap(mBitmap, null, new Rect(0, 0, getWidth(), getHeight()), mPaint);
}

来看下Canvas的两个构造函数,以及我们这里用到的drawBitmap()函数: 

public Canvas()
Construct an empty raster canvas. Use setBitmap() to specify a bitmap to draw into. 
The initial target density is Bitmap#DENSITY_NONE; 
this will typically be replaced when a target bitmap is set for the canvas.
————————————————————————————————————————————————————————————————————————————————————
public Canvas(Bitmap bitmap)
Construct a canvas with the specified bitmap to draw into. The bitmap must be mutable.
The initial target density of the canvas is the same as the given bitmap's density.
译:使用要绘制的指定位图构造画布。位图必须是可变的。画布的初始目标密度与给定位图的密度相同。
————————————————————————————————————————————————————————————————————————————————————
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
Draw the specified bitmap, scaling/translating automatically to fill the destination rectangle. 
If the source rectangle is not null, it specifies the subset of the bitmap to draw.

Note: if the paint contains a maskfilter that generates a mask which extends beyond 
the bitmap's original width/height (e.g. BlurMaskFilter), 
then the bitmap will be drawn as if it were in a Shader with CLAMP mode. 
Thus the color outside of the original width/height will be the edge color replicated.

This function ignores the density associated with the bitmap. 
This is because the source and destination rectangle coordinate spaces are in their respective densities, 
so must already have the appropriate scaling factor applied.

由于这里的canvasbg是用mBitmapBG创建的,所以所画的任何图像都会直接显示在mBitmapBG上,而我们创建的mBitmapBG是与控件一样大的,所以当把mBitmapBG做为Shader来设置给mPaint时,mBitmapBG会正好覆盖整个控件,而不会有多余的空白像素。

可以通过如下代码效果证明以上观点:

if (mDx != -1 && mDy != -1) {
    canvas.drawBitmap(mBitmapBG, null, new Rect(0, 0, getWidth(), getHeight()), mPaint);
)
→ 当手指按下或移动时,显示的是R.drawable.scenery这张与控件尺寸一样的图片。

这里需要注意的就是我们在将原图像画到mBitmapBG时,进行了拉压缩,把它拉伸到根当前控件一样大小。
然后利用Shader的知识,利用OnMotionEvent来捕捉用户的手指位置,当用户手指下按时,在手指位置画一个半径为150的圆形,把对应的位置的图像显示出来就可以了:

if (mDx != -1 && mDy != -1) {
    mPaint.setShader(new BitmapShader(mBitmapBG, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
    canvas.drawCircle(mDx, mDy, 150, mPaint);
}

<application android:theme="@android:style/Theme.Light/Black.NoTitleBar">决定了遮罩的颜色是白/黑色。

因为<ImageView layout_width/height="fill_parent">,所以下面代码效果也是一样:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mDx != -1 && mDy != -1) {
        mPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
        canvas.drawCircle(mDx, mDy, 150, mPaint);
    }
}

这里分两步。第一步,将图片缩放到控件大小,以完全覆盖控件,否则就会使用BitmapShader的填充模式。这里先新建一张空白的位图,这张位图的大小与控件的大小一样,然后对背景进行拉伸,画到这张空白的位图上。之所以在onDraw()函数中创建mBitmapBG,而不是在初始化代码中创建,是因为在初始化时,getWidth()和getHeight()函数是获取不到值的。

第二步,在mDx、mDy都不是-1时(手指按下或移动时),将新建的mBitmapBG作为BitmapShader设置给Paint,然后在手指所在位置画一个圆圈,把圆圈部分的图像显示出来。Shader是着色器!!!!所以Paint有Shader属性时,当然就是用它来着色用的。无论利用绘图函数绘制多大的图像、在哪里绘制,都与Shader无关。因为Shader总是从控件的左上角开始的。我们绘制的只是显示出来的部分而己。没有绘制的部分虽然已经生成,但是不会显示出来。

注:TileMode用于指定当控件区域大于指定的图片区域时,空白区域的颜色填充模式。如果指定区域小于当前控件区域,就没效果。


示例二:生成不规则头像

public class AvatorView extends View {
    private Paint mPaint;
    private Bitmap mBitmap;
    private BitmapShader mBitmapShader;
    public AvatorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avator);
        mPaint = new Paint();
        // 用边缘色彩来填充多余空间
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    }
    ..
}

这里初始化时创建一个BitmapShader,X轴和Y轴填充模式都是TileMode.CLAMP。其实,这里的填充模式没什么用,因为我们只需要显示当前图片,所以不存在多余的空白区域,使用哪种填充模式都可以。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Matrix matrix = new Matrix();
    float scale = (float) getWidth() / mBitmap.getWidth();
    matrix.setScale(scale, scale);
    mBitmapShader.setLocalMatrix(matrix);
    mPaint.setShader(mBitmapShader);

    float half = getWidth() / 2;
    canvas.drawCircle(half, half, getWidth() / 2, mPaint);
}

这里用Matrix进行的缩放,像上面一个示例缩放也是可以的。如果我们用canvas.drawRoundRect()函数画出一个圆角矩形:

canvas.drawRoundRect(new RectF(200,200,getWidth()-200, scale*mBitmap.getHeight()-200),
                               50, 50, mPaint);

五、Shader之LinearGradient

概述:

1.构造函数

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)
● (x0,y0):起始渐变点坐标
● (x1,y1):结束渐变点坐标
● color0:起始颜色,颜色值必须使用0xAARRGGBB形式
● color1:终止颜色,颜色值必须使用0xAARRGGBB形式
● tile:与BitmapShader一样,用于指定当控件区域【大于指定的渐变区域】时,空白区域的颜色填充模式
public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile)
● (x0,y0):起始渐变点坐标
● (x1,y1):结束渐变点坐标
● colors[]:指定渐变的颜色值数组,同样颜色值必须使用0xAARRGGBB形式
● positions[]:与渐变的颜色相对应,取值是0~1的Float类型,表示每种颜色在整条渐变线中的百分比位置

2.双色渐变使用示例

public class LinearGradientView extends View {
    private Paint mPaint;
    public LinearGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setShader(new LinearGradient(0, getHeight()/2, getWidth(), getHeight()/2,
                         0xffff0000, 0xff00ff00, Shader.TileMode.CLAMP));
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    }
}
<com.example.customwidgets.LinearGradientView
        android:layout_width="fill_parent"
        android:layout_height="100dp" />

 

3.多色渐变使用示例

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int[] colors = {0xffff0000,0xff00ff00,0xff0000ff,0xffffff00,0xff00ffff};
    float[] pos = {0f, 0.2f, 0.4f, 0.6f, 1.0f};
    LinearGradient multiGradient = new LinearGradient(0, getHeight()/2, getWidth(), getHeight()/2, colors, pos, Shader.TileMode.CLAMP);
    mPaint.setShader(multiGradient);
    canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
}

同样从控件的左边中点渐变到右边中点。这里指定了5种渐变颜色,而且指定了每种颜色的位置,前4种颜色按20%均匀分布,最后两种颜色相距40%。

4.TileMode填充模式

LinearGradient multiGradient = new LinearGradient(0, 0, getWidth()/2, getHeight()/2, colors, pos, Shader.TileMode.MIRROR/REPEAT/CLAMP);

5.Shader填充与显示区域

有关填充方式与显示区域的问题,所有Shader都是一样的:Shader的布局和显示是分离的;Shader总是从控件左上角开始布局的;如果单张图片无法覆盖整个控件,则会使用TileMode重复模式来填充空白区域;而canvas.draw系列函数则只表示哪部分区域被显示出来。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int[] colors = {0xffff0000,0xff00ff00,0xff0000ff,0xffffff00,0xff00ffff};
    float[] pos = {0f, 0.2f, 0.4f, 0.6f, 1.0f};
    LinearGradient multiGradient = new LinearGradient(0, 0, getWidth()/2, getHeight()/2, colors, pos, Shader.TileMode.CLAMP);
    mPaint.setShader(multiGradient);
    mPaint.setTextSize(100);
    canvas.drawText("欢迎关注ITZYJR的blog", 0, 200, mPaint);
}

示例:闪光文字效果

1.原理

1)初始状态

我们需要一个渐变的LinearGradient,颜色从文字的黑色到中间的绿色,然后再到黑色,填充模式为Shader.TileMode.CLAMP,初始位置在文字的左侧。

(1)把渐变图像用红边框了起来。由于填充模式是Shader.TileMode.CLAMP,所以右侧文字的位置会被填充为边缘颜色——黑色。

(2)特地把文字写成了白色,而文字真正的颜色应该是其底部LinearGradient的填充颜色。

2)运动中


由于使用Shader.TileMode.CLAMP填充模式,所以指定渐变区域两边的空白区域都会被填充为LinearGradient的边缘颜色,即文字的黑色。

由于文字会显示其下文LinearGradient的填充颜色,所以现在文字的颜色就会有一部分变成了绿色。

3)终止状态

同样,由于使用Shader.TileMode.CLAMP填充模式,所以文字会被填充原本的颜色。

得到如下结论:

第一,创建的LinearGradient初始位置在文字左侧,而且大小与文字所占位置相同,填充模式使用边缘填充。

第二,从起始位置和终止位置可以看出,LinearGradient渐变的运动长度是两个文字的长度。

2.实现

首先,自定义控件是要派生View还是TextView。由于实现的是闪光文字效果,需要用户指定文字内容,而且TextView中已经自带文字绘制过程,不需要我们处理,所以派生自TextView比较方便。

public class ShimmerTextView extends TextView {
    private int mDx;
    private LinearGradient mLinearGradient;
    public ShimmerTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = getPaint();// 使用TextView自带的画笔
        int length = (int) mPaint.measureText(getText().toString());
        createAnim(length);
        createLinearGradient(length);
    }
    ...
}

Paint中有一个函数可以测量指定文字的长度:

public float measureText(String text)

LinearGradient的长度与文字的长度相同,而ValueAnimator的动画长度是文字长度的两倍。

private void createAnim(int length) {
    ValueAnimator animator = ValueAnimator.ofInt(0, 2 * length);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mDx = (Integer) animation.getAnimatedValue();
            postInvalidate();
        }
    });
    animator.setRepeatMode(ValueAnimator.RESTART);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.setDuration(2000);
    animator.start();
}
private void createLinearGradient(int length) {
    mLinearGradient = new LinearGradient(-length, 0, 0, 0,
                                         new int[]{getCurrentTextColor(), 0xff00ff00, getCurrentTextColor()},
                                         new float[]{0, 0.5f, 1},
                                         Shader.TileMode.CLAMP);
}
protected void onDraw(Canvas canvas) {
    Matrix matrix = new Matrix();
    matrix.setTranslate(mDx, 0);
    mLinearGradient.setLocalMatrix(matrix);
    mPaint.setShader(mLinearGradient);

    super.onDraw(canvas);
}

我们需要注意的是,super.onDraw(canvas)是父类的绘制函数,TextView会在onDraw()函数中重绘文字,所以我们需要在绘制文字前设置Shader,以便显示文字下文的Shader。但是,Shader需要设置给绘制文字的画笔才有效,而绘制文字所用的画笔可以通过getPaint()函数得到,这就是在初始化mPaint时使用getPaint()函数的原因。

<com.harvic.LinearGradient.ShimmerTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="24dp"
    android:layout_margin="20dp"
    android:text="欢迎关注启舰的blog"/>

六、Shader之RadialGradient

双色渐变:

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
● (centerX,centerY):渐变中心坐标点
● radius:渐变半径
● centerColor:渐变中心点的颜色,取值必须为0xAARRGGBB形式
● edgeColor:渐变圆边缘的颜色,取值必须为0xAARRGGBB形式
● tileMode:指定当控件区域大于指定的渐变区域时,空白区域的颜色填充方式
public class RadialGradientView extends View {
    private Paint mPaint;
    private RadialGradient mRadialGradient;
    private int mRadius;

    public RadialGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint();
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mRadialGradient == null) {
            mRadius = getWidth() / 2;
            mRadialGradient = new RadialGradient(getWidth()/2, getHeight()/2, mRadius,
                                                 0xffff0000, 0xff00ff00, TileMode.REPEAT);
            mPaint.setShader(mRadialGradient);
        }
        canvas.drawCircle(getWidth()/2, getHeight()/2, mRadius, mPaint);
    }
}

为什么要在onDraw()函数中初始化mRadialGradient?

因为getWidth()和getHeight()函数需要在生命周期函数onLayout()执行完成后才会有值 。

多色渐变:

RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, TileMode tileMode)
● colors:所需要的渐变颜色数组
● stops:表示每种渐变颜色所在的位置百分点,取值0~1,数量与colors数组保持一致。
         一般第一个数值取0,最后一个数值取1。
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mRadialGradient == null) {
        mRadius = getWidth()/2;
        int[] colors = new int[] {0xffff0000, 0xff00ff00, 0xff0000ff, 0xffffff00};
        float[] stops = new float[] {0f, 0.2f, 0.5f, 1f};
        mRadialGradient = new RadialGradient (getwidth()/2, getHeight()/2, mRadius, colors, stops, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
    }
    canvas.drawCircle(getWidth()/2,getHeight()/2, mRadius, mPaint);
}

由于绘制的圆半径与放射渐变的半径一样,所以不存在空白区域填充的问题,TileMode.REPEAT并没有被用到。

TileMode填充模式:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mRadiusGradient == null) {
        mRadius = getWidth()/6;
        mRadialGradient = new RadialGradient(getWidth()/2, getHeight()/2, mRadius,
                                             0xffff0000, 0xff00ff00, TileMode.MIRROR);
        mPaint.setShader(mRadialGradient);
    }
    canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
}

大总结:只要把握两点就可以运用自如

◆ TileMode填充模式

◆ Shader的布局与canvas.draw系列函数显示区域的关系

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值