一,自定义 View 基础:

1,画笔 Paint

文本相关:

方法 描述
setColor(@ColorInt int color) 设置画笔颜色
setStrokeWidth(float width) 设置画笔粗细
setTextSkewX(float f) 设置倾斜,负右斜,正为左
setARGB(int a, int r, int g, int b) 设置颜色,a为透明度
setTextSize(float textSize) 设置绘制文字大小
setFakeBoldText(boolean fakeBoldText) 是否粗体
setTextAlign(Paint.Align align) 设置文字对齐方式,LEFT,CENTER,RIGHT
setUnderlineText(boolean underlineText) 设置下划线
setStyle(Style style) 设置画笔样式,FILL,STROKE,FILL_AND_STROKE setTypeface(Typeface typeface

位图相关

方法 描述
setDither(boolean dither) 设置抖动处理
setAlpha(int a) 设置透明度
setAntiAlias(boolean aa) 是否开启抗锯齿
setFilterBitmap() 是否开启优化Bitmap
setColorFilter(ColorFilter filter) 设置颜色过滤
setMaskFilter(MaskFilter maskfilter) 设置滤镜的效果
setShader(Shader shader) 设置图像渐变效果
setStrokeJoin(Paint.Join join) 设置图像结合方式
setXfermode(Xfermode xfermode) 设置图像重叠效果
setPathEffect(PathEffect effect) 设置路径效果 reset() 恢复默认设置

2,路径

moveTo() 绘制起始的点
lineTo() 绘制连接的点
close() 形成闭环

arcTo() 弧线路径 参数:生成椭圆的矩形、弧线开始的角度、弧线扫描过的角度、是否强制地将弧线的起始点作为绘制起始位置

**Region 区域:**一块任意形状的封闭图形。使用RegionIterator,用于区域相交填充操作。

3,画布 Canvas

1)清除画布

/**
 * 画布清屏
 */
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

2)绘制画布背景

/**
 * 绘制画布背景
 */
//绘制画布背景,argb
canvas.drawARGB(99, 255, 0, 255);
canvas.drawRGB(255, 0, 255);
canvas.drawColor(Color.LTGRAY);

3)绘制圆形

/**
 * 绘制圆形
 */
//设置画笔的基本属性
//设置画笔颜色
mPaint.setColor(Color.RED);
//设置画笔填充样式
//仅填充内部
mPaint.setStyle(Paint.Style.FILL);
//填充内部和描边
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
//仅填充描边
mPaint.setStyle(Paint.Style.STROKE);
//设置描边宽度,单位px,当填充样式是FILL_AND_STROKE时候有效
mPaint.setStrokeWidth(20);
//使用画布画圆
canvas.drawCircle(200, 200, 150, mPaint);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(200, 200, 130, mPaint);

4)绘制点

/**
 * 绘制点
 */
//点的大小
mPaint.setStrokeWidth(20);
//绘制点,x坐标、y坐标
canvas.drawPoint(340, 340, mPaint);

5)绘制直线

/**
 * 绘制直线
 */
//直线粗细
mPaint.setStrokeWidth(20);
//绘制直线起点x坐标、起点y坐标、终点x坐标、终点y坐标
canvas.drawLine(360, 360, 660, 660, mPaint);

6)绘制

/**
 * 绘制矩形
 */
//矩形的边框粗细
mPaint.setStrokeWidth(5);
//仅填充矩形描边
mPaint.setStyle(Paint.Style.STROKE);
//绘制矩形,RectF是保存float类型的矩形,Rect是保存int类型的矩形,左上右下
mRectF.set(400F, 400F, 450F, 450F);
mRect.set(500, 500, 550, 550);
canvas.drawRect(mRectF, mPaint);
canvas.drawRect(mRect, mPaint);

7)绘制文字

/**
 * 绘制文字
 * setColor(@ColorInt int color) 设置画笔颜色
 * setStrokeWidth(float width) 设置画笔粗细
 * setTextSkewX(float f) 设置倾斜,负右斜,正为左
 * setARGB(int a, int r, int g, int b) 设置颜色,a为透明度
 * setTextSize(float textSize) 设置绘制文字大小
 * setFakeBoldText(boolean fakeBoldText) 是否粗体
 * setTextAlign(Paint.Align align) 设置文字对齐方式,LEFT,CENTER,RIGHT setUnderlineText(boolean underlineText) 设置下划线
 * setStyle(Style style) 设置画笔样式,FILL,STROKE,FILL_AND_STROKE setTypeface(Typeface typeface) 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
 */
mPaint.setTextSize(90f);
canvas.drawText("Android Stack", 200, 1000, mPaint);

二,绘制文字

准备基础类BaseView,继承View类。

public class BaseView extends View {
    static final int LINE_OFFSET = 60;

    protected Paint notePaint = new Paint();
    protected Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    protected Rect textRect = new Rect();

    public BaseView(Context context) {
        this(context, null);
    }

    public BaseView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BaseView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

在 BaseView 里准备了 2 个画笔。

绘制文字的方法canvas.drawText,需要指定字符串,绘制起始坐标,画笔 paint。

canvas.drawText(text, x, y, textPaint);

起始坐标的 y 值所在横线在后面也称为 baseline 。

获取文字边界

从图中可以看到文字的部分笔画是可以超出边线的。比如开头的j

使用 Paint.getTextBounds 方法获取文字的边界。

textPaint.getTextBounds(onShowText, 0, onShowText.length(), textRect); // 获取text边界

绘制上图的部分代码

private void drawBounds(Canvas canvas, float tx, float ty, String onShowText) {
    canvas.drawText(onShowText, tx, ty, textPaint); // 写字
    textPaint.getTextBounds(onShowText, 0, onShowText.length(), textRect); // 获取text边界

    notePaint.setStrokeWidth(3);
    notePaint.setColor(Color.parseColor("#EF6C00"));

    // 左边线
    canvas.drawLine(tx, ty - textRect.height() - LINE_OFFSET, tx, ty + LINE_OFFSET, notePaint);

    // 右边线
    canvas.drawLine(tx + textRect.width(), ty - textRect.height() - LINE_OFFSET, tx + textRect.width(), ty + LINE_OFFSET, notePaint);

    notePaint.setColor(Color.parseColor("#FF0277BD"));
    // 上边线
    canvas.drawLine(tx - LINE_OFFSET, ty - textRect.height(), tx + textRect.width() + LINE_OFFSET, ty - textRect.height(), notePaint);

    notePaint.setColor(Color.parseColor("#00695C"));
    // y - baseline
    canvas.drawLine(tx - LINE_OFFSET, ty, tx + textRect.width() + LINE_OFFSET, ty, notePaint);
}

获取 text 尺寸信息

需要使用 Paint.FontMetrics 类。它有 5 个属性

属性 介绍
top 从baseline到最高的文字顶部的最大距离
ascent 单行距(singled spaced)时,baseline上方空间的推荐距离
descent 单行距时,baseline下方空间的推荐距离
bottom baseline到最下方字符的最大距离
leading 多行文字之间推荐使用的额外空间

使用 Paint.getFontMetrics() 方法获取 Paint.FontMetrics

Paint.FontMetrics fm = textPaint.getFontMetrics();

注意,topascent 是负值。

绘制上图的方法

private void drawFontMetrics(Canvas canvas, float x, float y, String text) {
    canvas.drawText(text, x, y, textPaint);
    textPaint.getTextBounds(text, 0, text.length(), textRect);
    Paint.FontMetrics fm = textPaint.getFontMetrics();

    notePaint.setStrokeWidth(1);
    notePaint.setColor(Color.BLACK);
    canvas.drawText(String.format(Locale.CHINA, "top:%.2f, bottom:%.2f", fm.top, fm.bottom), x, y + notePaint.getTextSize() * 2.5f, notePaint);
    canvas.drawText(String.format(Locale.CHINA, "ascent:%.2f, descent:%.2f, leading:%.2f", fm.ascent, fm.descent, fm.leading), x, y + notePaint.getTextSize() * 4f, notePaint);

    notePaint.setColor(Color.parseColor("#FFD84315"));

    // fm top线
    canvas.drawLine(x - L_BIAS, y + fm.top, x + textRect.width() + L_BIAS, y + fm.top, notePaint);

    notePaint.setColor(Color.parseColor("#FF00695C"));

    // fm bottom线
    canvas.drawLine(x - L_BIAS, y + fm.bottom, x + textRect.width() + L_BIAS, y + fm.bottom, notePaint);

    notePaint.setColor(Color.parseColor("#4527A0"));

    // fm ascent线
    canvas.drawLine(x - L_BIAS, y + fm.ascent, x + textRect.width() + L_BIAS, y + fm.ascent, notePaint);

    notePaint.setColor(Color.parseColor("#0E0822"));

    // fm descent线
    canvas.drawLine(x - L_BIAS, y + fm.descent, x + textRect.width() + L_BIAS, y + fm.descent, notePaint);
}

对齐文字中间

有了上面的基础,我们可以很快知道如何实现文字“居中对齐”。将文字的水平中心线或竖直中心线对齐到某个点。

例如绘制一个平面坐标系

绘制出2个坐标轴,然后绘制上刻度,并写上数值。 数值文字对应到刻度相应的位置上。我们主要使用 Paint.getTextBounds 获取到文字的尺寸信息,再计算出合适的位置。

private void drawChart(Canvas canvas) {
    final float x0 = 100;
    final float y0 = 300; // 原点在画布上的坐标
    final float halfLine = 5; // 刻度长度的一半

    textPaint.setColor(Color.BLACK);
    textPaint.setTextSize(20);
    notePaint.setStrokeWidth(2);
    canvas.drawLine(x0, y0, x0 + 500, y0, notePaint); // 绘制横轴
    canvas.drawLine(x0, y0, x0, y0 - 290, notePaint); // 绘制纵轴

    // 绘制横轴刻度和数字
    for (int i = 1; i <= 4; i++) {
        int step = i * 100;
        String n = String.valueOf(step);
        float x = x0 + step;
        canvas.drawLine(x, y0 - halfLine, x, y0 + halfLine, notePaint); // 刻度

        // 文字对齐
        textPaint.getTextBounds(n, 0, n.length(), textRect);
        canvas.drawText(n, x - textRect.width() / 2f, y0 + textRect.height() + halfLine, textPaint);
    }

    // 绘制纵轴刻度和数字
    for (int i = 1; i <= 2; i++) {
        int step = i * 100;
        String n = String.valueOf(step);
        float y = y0 - step;
        canvas.drawLine(x0 - halfLine, y, x0 + halfLine, y, notePaint); // 刻度

        // 文字对齐
        textPaint.getTextBounds(n, 0, n.length(), textRect);
        canvas.drawText(n, x0 - halfLine * 2 - textRect.width(), y + textRect.height() / 2f, textPaint);
    }

    // 绘制原点 0
    String origin = "0";
    textPaint.getTextBounds(origin, 0, origin.length(), textRect);
    canvas.drawText(origin, x0 - textRect.width(), y0 + textRect.height(), textPaint);
}

三,裁剪画布 canvas clip path

使用 canvas.clipPath(path) 方法。

clipPath 方法相当于按给定的路径裁剪画布。 根据这个用途,我们可以做出圆角图片的效果。

圆角图片示例

圆角图片是一种常见的设计。Android 官方库中并没有直接显示圆角图片的方法。 如果是纯色,渐变色的背景,我们可以用 shape 的 corners 来实现圆角。 这里是利用 clipPathsuper.onDraw(canvas) 之前将画布裁出圆角。需要传入一个 Path 来指定裁剪范围。

新建一个 RoundImageView 类,继承自 AppCompatImageView

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatImageView;

public class RoundImageView extends AppCompatImageView {
    private float vw, vh;
    private Path path = new Path();

    private float topLeftR;
    private float topRightR;
    private float botLeftR;
    private float botRightR;

    public RoundImageView(Context context) {
        this(context, null);
    }

    public RoundImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
        topLeftR = typedArray.getDimensionPixelSize(R.styleable.RoundImageView_topLeftR, 0);
        topRightR = typedArray.getDimensionPixelSize(R.styleable.RoundImageView_topRightR, 0);
        botLeftR = typedArray.getDimensionPixelSize(R.styleable.RoundImageView_botLeftR, 0);
        botRightR = typedArray.getDimensionPixelSize(R.styleable.RoundImageView_botRightR, 0);
        typedArray.recycle();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        vw = getWidth();
        vh = getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        path.reset();
        if (vw > topLeftR && vh > topRightR) {
            path.moveTo(topLeftR, 0);
            path.lineTo(vw - topRightR, 0);
            path.quadTo(vw, 0, vw, topRightR);
            path.lineTo(vw, vh - botRightR);
            path.quadTo(vw, vh, vw - botRightR, vh);
            path.lineTo(botLeftR, vh);
            path.quadTo(0, vh, 0, vh - botLeftR);
            path.lineTo(0, topLeftR);
            path.quadTo(0, 0, topLeftR, 0);
            path.close();
            canvas.clipPath(path);
        }
        super.onDraw(canvas);
    }
}

里面使用了 TypedArray,需要新增 declare-styleable 资源。

<resources>
    <declare-styleable name="RoundImageView">
        <attr name="topLeftR" format="dimension" />
        <attr name="topRightR" format="dimension" />
        <attr name="botLeftR" format="dimension" />
        <attr name="botRightR" format="dimension" />
    </declare-styleable>
</resources>

使用这个类。在 layout 中直接使用。分别定义 4 个圆角半径参数。给 src 设置纯色。 如果给定图片,图片内容接触到 view 端点时,才会看得出有圆角效果。

<com.rustfisher.tutorial2020.customview.view.RoundImageView
    style="@style/RoundIvCube"
    app:botLeftR="8dp"
    app:botRightR="8dp"
    app:topLeftR="8dp"
    app:topRightR="8dp" />

<com.rustfisher.tutorial2020.customview.view.RoundImageView
    style="@style/RoundIvCube"
    app:botLeftR="25dp"
    app:botRightR="25dp"
    app:topLeftR="25dp"
    app:topRightR="25dp" />

<com.rustfisher.tutorial2020.customview.view.RoundImageView
    style="@style/RoundIvCube"
    app:botLeftR="16dp" />

<com.rustfisher.tutorial2020.customview.view.RoundImageView
    style="@style/RoundIvCube"
    app:topLeftR="16dp" />

<com.rustfisher.tutorial2020.customview.view.RoundImageView
    style="@style/RoundIvCube"
    app:botRightR="16dp"
    app:topLeftR="16dp" />

style定义

<style name="RoundIvCube">
        <item name="android:layout_width">50dp</item>
        <item name="android:layout_height">50dp</item>
        <item name="android:src">#303F9F</item>
        <item name="android:layout_margin">8dp</item>
    </style>

运行结果

可以看到各个图形的圆角。但是我们也能发现有锯齿。这个裁剪方法没能做到光滑圆润的圆角。

******************