Android界面开发:View自定义实践绘制篇

4355次阅读  |  发布于5年以前

本文提供了三个综合性的完整实例来辅助理解。

第一次阅览本系列文章,请参见导读,更多文章请参见文章目录

本篇文章我们来分析View绘制方面的实践。

一个简单的自定义View

public class DrawView extends View {

    Paint paint = new Paint();

    public DrawView(Context context) {
        super(context);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.BLACK);
        canvas.drawCircle(150, 150, 150, paint);
    }
}

它在屏幕上绘制了一个圆形,如图:

在处理绘制的时候有以下几个关键点:

我们分别来看看这个关键的角色。

一 View

我们讨论的第一个问题就是View/ViewGroup的绘制顺序问题,绘制在View.draw()方法里调用的,具体的执行顺序是:

  1. drawBackground():绘制背景,不能重写。
  2. onDraw():绘制主体。
  3. dispatchDraw():绘制子View
  4. onDrawForeground():绘制滑动边缘渐变、滚动条和前景。

我们先从个小例子开始。

我们如果继承View来实现自定义View。View类的onDraw()是空实现,所以我们的绘制代码写在super.onDraw(canvas)的前面或者后面都没有关系,如下所示:

public class DrawView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制代码,写在super.onDraw(canvas)前后均可
    }
}

但是如果我们继承特定的控件,例如TextView。我们就需要去考虑TextView的绘制逻辑。

public class DrawView extends TextView {
    @Override
    protected void onDraw(Canvas canvas) {

        //写在前面,DrawView的绘制会先于TextView的绘制,TextView绘制的内容可以会覆盖DrawView
        super.onDraw(canvas);
        //写在后面,DrawView的绘制会晚于TextView的绘制,DrawView绘制的内容可以会覆盖TextView
    }
}

具体怎么做取决于你实际的需求,例如你如果想给TextView加个背景,就写在super.onDraw(canvas)前面,想给TextView前面加些点缀,就 写在super.onDraw(canvas)后面。

我们来写个例子理解下。

举例

public class LabelImageView extends AppCompatImageView {

    /**
     * 梯形距离左上角的长度
     */
    private static final int LABEL_LENGTH = 100;
    /**
     * 梯形斜边的长度
     */
    private static final int LABEL_HYPOTENUSE_LENGTH = 100;

    private Paint textPaint;
    private Paint backgroundPaint;
    private Path pathText;
    private Path pathBackground;


    public LabelImageView(Context context) {
        super(context);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //计算路径
        calculatePath(getMeasuredWidth(), getMeasuredHeight());
        canvas.drawPath(pathBackground, backgroundPaint);
        canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        super.onDrawForeground(canvas);
    }

    /**
     * 计算路径              x1   x2
     * ................................    distance(标签离右上角的垂直距离)
     * .                      .    .  .
     * .                        .    .. y1
     * .                          .   .
     * .                            . .
     * .                              . y2    height(标签垂直高度)
     * .                              .
     * ................................
     */
    private void calculatePath(int measuredWidth, int measuredHeight) {

        int top = 185;
        int right = measuredWidth;

        float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
        float x2 = right - LABEL_HYPOTENUSE_LENGTH;
        float y1 = top + LABEL_LENGTH;
        float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;

        pathText.reset();
        pathText.moveTo(x1, top);
        pathText.lineTo(right, y2);
        pathText.close();

        pathBackground.reset();
        pathBackground.moveTo(x1, top);
        pathBackground.lineTo(x2, top);
        pathBackground.lineTo(right, y1);
        pathBackground.lineTo(right, y2);
        pathBackground.close();
    }

    private void init() {
        pathText = new Path();
        pathBackground = new Path();

        textPaint = new Paint();
        textPaint.setTextSize(50);
        textPaint.setFakeBoldText(true);
        textPaint.setColor(Color.WHITE);

        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.RED);
        backgroundPaint.setStyle(Paint.Style.FILL);
    }
}

所以你可以看到,当我们继承了一个View,根据需求的不同可以选择性重写我们需要的方法,在super前插入代码和在super后插入代码,效果是不一样的。

二 Paint

Paint:顾名思义,画笔,通过Paint可以对绘制行为进行控制。

Paint有三种构造方法

public class Paint {
      //空的构造方法
      public Paint() {
          this(0);
      }

      //传入flags来构造Paint,flags用来控制Paint的行为,例如:抗锯齿等
      public Paint(int flags) {
          mNativePaint = nInit();
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
          setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
          // TODO: Turning off hinting has undesirable side effects, we need to
          //       revisit hinting once we add support for subpixel positioning
          // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
          //        ? HINTING_OFF : HINTING_ON);
          mCompatScaling = mInvCompatScaling = 1;
          setTextLocales(LocaleList.getAdjustedDefault());
      }

      //传入另外一个Paint来构造新的Paint
      public Paint(Paint paint) {
          mNativePaint = nInitWithPaint(paint.getNativeInstance());
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
          setClassVariablesFrom(paint);
      }  
}

2.1 颜色处理类

在Paint类中,处理颜色主要有三个方法。

setShader(Shader shader)

着色器是图像领域的一个通用概念,它提供的是一套着色规则。

public Shader setShader(Shader shader)

着色器具体由Shader的子类实现:

LinearGradient - 线性渐变

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)

举例

//线性渐变
Shader shader1 = new LinearGradient(0, 100, 200, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new LinearGradient(0, 600, 200, 600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new LinearGradient(0, 1100, 200, 1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);
canvas.drawRect(0, 1100, 1000, 1500, paint3);

SweepGradient - 辐射渐变

public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode)

举例

//辐射渐变
Shader shader1 = new RadialGradient(0, 100, 200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new RadialGradient(0, 600, 200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new RadialGradient(0, 1100, 200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);

BitmapShader - 位图着色

使用位图的像素来填充图形或者文字。

 public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)

举例

BitmapShader是一个很有用的类,可以利用该类做各种各样的图片裁剪。

//位图着色
Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

//绘制圆形
canvas.drawCircle(500, 500, 300, paint1);

ComposeShader - 组合Shader

ComposeShader可以将连个Shader组合在一起。

public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

PorterDuff.Mode用来指定两个Shader叠加时颜色的绘制策略,它有很多种策略,也就是以一种怎样的模式来与原图像进行合成,具体如下:

蓝色矩形为原图像,红色圆形为目标图像。

更多细节可以参见PorterDuff.Mode官方文档

setColorFilter(ColorFilter filter)

颜色过滤器可以将颜色按照一定的规则输出,常见于各种滤镜效果。

public ColorFilter setColorFilter(ColorFilter filter)

我们通常使用的是ColorFilter的三个子类:

LightingColorFilter - 模拟光照效果

public LightingColorFilter(int mul, int add)

mul 和 add 都是和颜色值格式相同的 int 值,其中 mul 用来和目标像素相乘,add 用来和目标像素相加。

举例

//颜色过滤器
ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
paint2.setColorFilter(colorFilter1);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);

PorterDuffColorFilter - 模拟颜色混合效果

public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)

PorterDuffColorFilter指定一种颜色和PorterDuff.Mode来与源图像就行合成,也就是以一种怎样的模式来与原图像进行合成,我们在上面已经讲过这个内容。

举例

//我们在使用Xfermode的时候也是使用它的子类PorterDuffXfermode
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方  
paint.setXfermode(xfermode); // 设置 Xfermode  
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆  
paint.setXfermode(null); // 用完及时清除 Xfermode  

ColorMatrixColorFilter - 颜色矩阵过滤

ColorMatrixColorFilter使用一个颜色矩阵ColorMatrix来对象图像进行处理。

public ColorMatrixColorFilter(ColorMatrix matrix)

ColorMatrix是一个4x5的矩阵

[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]

通过计算,ColorMatrix可以对要绘制的像素进行转换,如下:

R’ = a*R + b*G + c*B + d*A + e;  
G’ = f*R + g*G + h*B + i*A + j;  
B’ = k*R + l*G + m*B + n*A + o;  
A’ = p*R + q*G + r*B + s*A + t;  

利用ColorMatrixColorFilter(可以实现很多炫酷的滤镜效果。

setXfermode(Xfermode xfermode)

Paint.setXfermode(Xfermode xfermode)方法,它也是一种混合图像的方法。

Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以View中已有的内 容作为目标图像,选取一个PorterDuff.Mode作为绘制内容的颜色处理方案。

小结

关于PorterDuff.Mode,我们已经提到

这三种以不同的方式来使用PorterDuff.Mode,但是原理都是一样的。

2.2 文字处理类

Paint里有大量方法来设置文字的绘制属性,事实上文字在Android底层是被当做图片来处理的。

2.3 特殊效果类

setAntiAlias (boolean aa)

设置抗锯齿,默认关闭,用来是图像的绘制更加圆润。我们还可以在初始化的时候设置Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);。

setStyle(Paint.Style style)

设置填充风格,

如果是划线模式,我们针对线条还可以有多种设置。

setStrokeWidth(float width) - 设置线条粗细

setStrokeCap(Paint.Cap cap) - 设置线头的形状,默认为 BUTT

setStrokeJoin(Paint.Join join) - 设置拐角的形状。默认为 MITER

setStrokeMiter(float miter)- 设置 MITER 型拐角的延长线的最大值

setDither(boolean dither)

设置图像的抖动。

抖动是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。

当然这个效果旨在低位色的时候比较有用,例如,ARGB_4444 或者 RGB_565,不过现在Android默认的色彩深度都是32位的ARGB_8888,这个方法的效果没有那么明显。

setFilterBitmap(boolean filter)

设置是否使用双线性过滤来绘制 Bitmap 。

图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。

etPathEffect(PathEffect effect)

设置图形的轮廓效果。Android有六种PathEffect:

CornerPathEffect(float radius)

DiscretePathEffect(float segmentLength, float deviation)

DashPathEffect(float[] intervals, float phase)

PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)

SumPathEffect(PathEffect first, PathEffect second)

ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

举例

//图形轮廓效果
//绘制圆角
PathEffect cornerPathEffect = new CornerPathEffect(20);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(5);
paint1.setPathEffect(cornerPathEffect);

//绘制尖角
PathEffect discretePathEffect = new DiscretePathEffect(20, 5);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);
paint2.setPathEffect(discretePathEffect);

//绘制虚线
PathEffect dashPathEffect = new DashPathEffect(new float[]{20,10, 5, 10}, 0);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
paint3.setPathEffect(dashPathEffect);

//使用path来绘制虚线
Path path = new Path();//画一个三角来填充虚线
path.lineTo(40, 40);
path.lineTo(0, 40);
path.close();
PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40, 0, PathDashPathEffect.Style.TRANSLATE);
paint4.setStyle(Paint.Style.STROKE);
paint4.setStrokeWidth(5);
paint4.setPathEffect(pathDashPathEffect);

setShadowLayer(float radius, float dx, float dy, int shadowColor)

设置阴影图层,处于目标下层图层。

举例

paint1.setTextSize(200);
paint1.setShadowLayer(10, 0, 0, Color.RED);
canvas.drawText("Android", 80, 300 ,paint1);

注:在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己 的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。

setMaskFilter(MaskFilter maskfilter)

设置图层遮罩层,处于目标上层图层。

MaskFilter有两个子类:

举例

模糊效果

分别为:

//设置遮罩图层,处于目标上层图层
//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
paint2.setMaskFilter(blurMaskFilter);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);

注:在硬件加速开启的情况下, setMaskFilter(MaskFilter maskfilter)只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。关闭硬件加速可以调用 View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)或者在Activity标签里设置android:hardwareAccelerated="false"。

三 Canvas

Canvas实现了Android 2D图形的绘制,底层基于Skia实现。

3.1 界面绘制

Canvas提供了丰富的对象绘制方法,一般都以drawXXX()打头,绘制的对象包括:

这里的方法大都很简单,我们来描述下期中比较复杂的方法。

弧线

public void drawArc(float left, float top, float right, float bottom, float startAngle,
        float sweepAngle, boolean useCenter, @NonNull Paint paint) {
    native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
            useCenter, paint.getNativeInstance());
}

例如

paint.setStyle(Paint.Style.FILL);//填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint);
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint);
paint.setStyle(Paint.Style.STROKE);//画线模式
paint.setStrokeWidth(5);
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint);

位图

第一个方法很简单,就是在指定的坐标处开始绘制位图。我们着重来看看第二个方法,这个方法不是很常用(可能是计算比较复杂的锅😓),但这并不影响它强大的功能。

drawBitmapMesh()方法将位图分为若干网格,然后对每个网格进行扭曲处理。我们先来看看这个方法的参数:

我们来用drawBitmapMesh()方法实现一个水面涟漪效果。

举例

/**
 * 利用Canvas.drawBitmapMeshC()方法对图像做扭曲处理,模拟水波效果。
 * <p>
 * For more information, you can visit https://github.com/guoxiaoxing or contact me by
 * guoxiaoxingse@163.com
 *
 * @author guoxiaoxing
 * @since 2017/9/12 下午3:44
 */
public class RippleLayout extends FrameLayout {

    /**
     * 图片横向、纵向的格树
     */
    private final int MESH_WIDTH = 20;
    private final int MESH_HEIGHT = 20;

    /**
     * 图片顶点数
     */
    private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);

    /**
     * 原坐标数组
     */
    private final float[] originVerts = new float[VERTS_COUNT * 2];

    /**
     * 转换后的坐标数组
     */
    private final float[] targetVerts = new float[VERTS_COUNT * 2];

    /**
     * 当前空间的图像
     */
    private Bitmap bitmap;

    /**
     * 水波宽度的一半
     */
    private float rippleWidth = 100f;

    /**
     * 水波扩展的速度
     */
    private float rippleRadius = 15f;

    /**
     * 水波半径
     */
    private float rippleSpeed = 15f;

    /**
     * 水波动画是否在进行中
     */
    private boolean isRippling;

    public RippleLayout(@NonNull Context context) {
        super(context);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (isRippling && bitmap != null) {
            canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0, null, 0, null);
        } else {
            super.dispatchDraw(canvas);
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                showRipple(ev.getX(), ev.getY());
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 显示水波动画
     *
     * @param originX 原点 x 坐标
     * @param originY 原点 y 坐标
     */
    public void showRipple(final float originX, final float originY) {
        if (isRippling) {
            return;
        }
        initData();
        if (bitmap == null) {
            return;
        }
        isRippling = true;
        //循环次数,通过控件对角线距离计算,确保水波纹完全消失
        int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
        final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
        Observable.interval(0, 10, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .take(count + 1)
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(@NonNull Long aLong) throws Exception {
                        rippleRadius = aLong * rippleSpeed;
                        warp(originX, originY);
                        if (aLong == count) {
                            isRippling = false;
                        }
                    }
                });
    }

    /**
     * 初始化 Bitmap 及对应数组
     */
    private void initData() {
        bitmap = getCacheBitmapFromView(this);
        if (bitmap == null) {
            return;
        }
        float bitmapWidth = bitmap.getWidth();
        float bitmapHeight = bitmap.getHeight();
        int index = 0;
        for (int height = 0; height <= MESH_HEIGHT; height++) {
            float y = bitmapHeight * height / MESH_HEIGHT;
            for (int width = 0; width <= MESH_WIDTH; width++) {
                float x = bitmapWidth * width / MESH_WIDTH;
                originVerts[index * 2] = targetVerts[index * 2] = x;
                originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
                index += 1;
            }
        }
    }

    /**
     * 图片转换
     *
     * @param originX 原点 x 坐标
     * @param originY 原点 y 坐标
     */
    private void warp(float originX, float originY) {
        for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
            float staticX = originVerts[i];
            float staticY = originVerts[i + 1];
            float length = getLength(staticX - originX, staticY - originY);
            if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
                PointF point = getRipplePoint(originX, originY, staticX, staticY);
                targetVerts[i] = point.x;
                targetVerts[i + 1] = point.y;
            } else {
                //复原
                targetVerts[i] = originVerts[i];
                targetVerts[i + 1] = originVerts[i + 1];
            }
        }
        invalidate();
    }

    /**
     * 获取水波的偏移坐标
     *
     * @param originX 原点 x 坐标
     * @param originY 原点 y 坐标
     * @param staticX 待偏移顶点的原 x 坐标
     * @param staticY 待偏移顶点的原 y 坐标
     * @return 偏移后坐标
     */
    private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
        float length = getLength(staticX - originX, staticY - originY);
        //偏移点与原点间的角度
        float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
        //计算偏移距离
        float rate = (length - rippleRadius) / rippleWidth;
        float offset = (float) Math.cos(rate) * 10f;
        float offsetX = offset * (float) Math.cos(angle);
        float offsetY = offset * (float) Math.sin(angle);
        //计算偏移后的坐标
        float targetX;
        float targetY;
        if (length < rippleRadius + rippleWidth && length > rippleRadius) {
            //波峰外的偏移坐标
            if (staticX > originX) {
                targetX = staticX + offsetX;
            } else {
                targetX = staticX - offsetX;
            }
            if (staticY > originY) {
                targetY = staticY + offsetY;
            } else {
                targetY = staticY - offsetY;
            }
        } else {
            //波峰内的偏移坐标
            if (staticX > originY) {
                targetX = staticX - offsetX;
            } else {
                targetX = staticX + offsetX;
            }
            if (staticY > originY) {
                targetY = staticY - offsetY;
            } else {
                targetY = staticY + offsetY;
            }
        }
        return new PointF(targetX, targetY);
    }

    /**
     * 根据宽高,获取对角线距离
     *
     * @param width  宽
     * @param height 高
     * @return 距离
     */
    private float getLength(float width, float height) {
        return (float) Math.sqrt(width * width + height * height);
    }

    /**
     * 获取 View 的缓存视图
     *
     * @param view 对应的View
     * @return 对应View的缓存视图
     */
    private Bitmap getCacheBitmapFromView(View view) {
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache(true);
        final Bitmap drawingCache = view.getDrawingCache();
        Bitmap bitmap;
        if (drawingCache != null) {
            bitmap = Bitmap.createBitmap(drawingCache);
            view.setDrawingCacheEnabled(false);
        } else {
            bitmap = null;
        }
        return bitmap;
    }
}

路径

public void drawPath(@NonNull Path path, @NonNull Paint paint) {
    if (path.isSimplePath && path.rects != null) {
        native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
    } else {
        native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
    }
}

drawPath()可以绘制自定义图形,图形的路径用Path对象来描述。

Path对象可以描述很多图形,具体说来:

3.2 范围裁切

Canvas里的范围裁切主要有两类方法:

举例

clipReact

clipPath

//范围裁切
canvas.save();//保存画布
canvas.clipRect(200, 200, 900, 900);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢复画布

canvas.save();//保存画布
path.addCircle(500, 500, 300, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢复画布

3.3 几何变换

关于几何变换有三种实现方式:

Canvas常规几何变换

Canvas还提供了对象的位置变换的方法,其中包括:

举例

canvas.save();//保存画布
canvas.skew(0, 0.5f);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢复画布

canvas.save();//保存画布
canvas.rotate(45, 750, 750);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢复画布

注:1 为了不影响其他绘制操作,在进行变换之前需要调用canvas.save()保存画布,变换完成以后再调用canvas.restore()来恢复画布。 2 Canvas几何变换的顺序是相反的,例如我们在代码写了:canvas.skew(0, 0.5f); canvas.rotate(45, 750, 750); 它的实际调用顺序是canvas.rotate(45, 750, 750); -> canvas.skew(0, 0.5f)

Matrix自定义几何变换

Matrix也实现了Canvas里的四种常规变换,它的实现流程如下:

  1. 创建 Matrix 对象;
  2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas。

Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。

举例

//Matrix几何变换
canvas.save();//保存画布
matrix.preSkew(0, 0.5f);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢复画布

canvas.save();//保存画布
matrix.reset();
matrix.preRotate(45, 750, 750);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢复画布

Matrix除了四种基本的几何变换,还可以自定义几何变换。

这两个方法都是通过多点的映射的方式来直接设置变换,把指定的点移动到给出的位置,从而发生形变。

举例

//Matrix几何变换
canvas.save();//保存画布
matrix.setPolyToPoly(src, 0, dst, 0, 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, 0, 0, paint1);
canvas.restore();//恢复画布

Camera三维变换

在讲解Camera的三维变换之前,我们需要先理解Camera的坐标系系统。

我们前面说过,Canvas使用的是二维坐标系。

而Camera使用的是三维坐标系,这里偷个懒😊,借用凯哥的图来描述一下。

关于Camera坐标系:

比如我们在Camera坐标系里做个X轴方向的旋转

Camera的三维变换包括:旋转、平移与移动相机。

旋转

平移

移动相机

举例

旋转

//Camera三维变换
canvas.save();//保存画布

camera.save();//保存camera
camera.rotateX(45);
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布

平移

//Camera三维变换
canvas.save();//保存画布

camera.save();//保存camera
camera.translate(500, 500, 500);
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布

移动相机

//Camera三维变换
canvas.save();//保存画布

camera.save();//保存camera
camera.setLocation(0, 0, - 1000);//相机往前移动,图像变小
canvas.translate(500, 750);//camera也是默认在原点(0, 0)位置,所以我们要把画布平移到图片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻转完图片,再将画布从图片中心(500, 750)平移到原点(0, 0)
camera.restore();//恢复camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢复画布

四 Path

Path描述了绘制路径,用它可以完成很多复杂的图形绘制。

我们再来看看Path里的方法。

4.1 添加图形

例如:addCircle(float x, float y, float radius, Direction dir)

public void addCircle(float x, float y, float radius, Direction dir) {
    isSimplePath = false;
    native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
}

该方法的参数含义:

其他的方法都是这个方法类似。

4.2 画线(直线或者曲线)

直线

//从当前位置,向目标位置画一条直线,该方法使用相对于原点的绝对坐标
public void lineTo(float x, float y) {
    isSimplePath = false;
    native_lineTo(mNativePath, x, y);
}

//从当前位置,向目标位置画一条直线,该方法使用相对于当前位置的相对坐标
public void rLineTo(float dx, float dy) {
    isSimplePath = false;
    native_rLineTo(mNativePath, dx, dy);
}

当前位置:当前位置指的是最后一次盗用Path的方法的终点位置,初始原点为(0, 0)

这里说到当前位置,我们再提一个方法Path.moveTo(float x, float y),它可以移动当前位置到一个新的位置。

举例

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
path.lineTo(300, 400);// 由当前位置 (0, 0) 向 (300, 400) 画一条直线
path.rLineTo(400, 0);// 由当前位置 (300, 400) 向正右方400像素的位置画一条直线
canvas.drawPath(path, paint);

贝塞尔曲线

贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。简单来说,贝塞尔曲线就是将任意一条曲线转换为精确的数学公式。

在贝塞尔曲线中,有两类点:

一阶贝塞尔曲线

B(t)为时间为t时的坐标,P0为起点,P1为终点。

二阶贝塞尔曲线

三阶贝塞尔曲线

贝塞尔曲线的模拟可以使用bezier-curve

我们再来看看Path类提供的关于贝塞尔曲线的方法。


//二阶贝塞尔曲线,绝对坐标,(x1, y1)表示控制点,(x2, y2)表示终点
public void quadTo(float x1, float y1, float x2, float y2) {
    isSimplePath = false;
    native_quadTo(mNativePath, x1, y1, x2, y2);
}

//二阶贝塞尔曲线,相对坐标
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
    isSimplePath = false;
    native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}

//三阶贝塞尔曲线,绝对坐标,(x1, y1)、(x2, y2)表示控制点,(x3, y3)表示终点
public void cubicTo(float x1, float y1, float x2, float y2,
                    float x3, float y3) {
    isSimplePath = false;
    native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

//三阶贝塞尔曲线,相对坐标
public void rCubicTo(float x1, float y1, float x2, float y2,
                     float x3, float y3) {
    isSimplePath = false;
    native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

我们来用贝塞尔曲线实现一个杯中倒水效果。

举例

/**
 * 控制点的X坐标不断左右移动,形成波浪效果。
 * <p>
 * For more information, you can visit https://github.com/guoxiaoxing or contact me by
 * guoxiaoxingse@163.com
 *
 * @author guoxiaoxing
 * @since 2017/9/11 下午6:11
 */
public class WaveView extends View {

    private static final String TAG = "WaveView";

    /**
     * 波浪从屏幕外开始,在屏幕外结束,这样效果更真实
     */
    private static final float EXTRA_DISTANCE = 200;

    private Path mPath;
    private Paint mPaint;

    /**
     * 控件宽高
     */
    private int mWidth;
    private int mHeight;

    /**
     * 控制点坐标
     */
    private float mControlX;
    private float mControlY;

    /**
     * 波浪峰值
     */
    private float mWaveY;

    /**
     * 是否移动控制点
     */
    private boolean mMoveControl = true;

    public WaveView(Context context) {
        super(context);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

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

        mControlY = mHeight - mHeight / 8;
        mWaveY = mHeight - mHeight / 32;
    }

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

        //波浪从屏幕外开始,效果更真实
        mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
        //二阶贝塞尔曲线
        mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
        //闭合曲线
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaint);

        //mControlX坐标在 -EXTRA_DISTANCE ~ mWidth + EXTRA_DISTANCE 范围内,先自增再自减,左右移动
        //形成波浪效果
        if (mControlX <= -EXTRA_DISTANCE) {
            mMoveControl = true;
        } else if (mControlX >= mWidth + EXTRA_DISTANCE) {
            mMoveControl = false;
        }
        mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;

        //水面不断上升
        if (mControlY >= 0) {
            mControlY -= 2;
            mWaveY -= 2;
        }

        Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);

        mPath.reset();
        invalidate();
    }


    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setColor(Color.parseColor("#4CAF50"));
    }
}

弧线


//画弧线
public void arcTo(float left, float top, float right, float bottom, float startAngle,
        float sweepAngle, boolean forceMoveTo) {
    isSimplePath = false;
    native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}

我们来看看这个方法的参数:

注:可以发现,这个方法与同样用来画弧线的方法Canvas.drawArc()少了个boolean useCenter参数,这是因为arcTo()方法只用来画弧线。

4.3 辅助设置和计算

public void setFillType(FillType ft) - 设置填充方式

方法用来设置填充方式,填充的方式有四种:

WINDING:non-zero winding rule,非零环绕数原则,该原则基于所有图形的绘制都有绘制方向(前面提到的Direction描述的顺时针与逆向时针),对于平面上的任意一点,向任意方向射出一条射线,射线遇到每个顺时针 的交点则加1,遇到逆时针的交点则减1,最后的结果如果不为0,则认为该点在图形内部,染色。如果结果为0,则认为该点在图形外部,不染色。

EVEN_ODD:even-odd rule,奇偶原则,对于平面上的任意一点,向任意方向射出一条射线,这条射线与图形相交(不是相切)的次数为奇数则说明这个点在图形内部,则进行染色。若为偶数则认为在图形外部,不进行染色。 这是一中交叉染色的情况。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8