Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解

Posted codingblock

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解相关的知识,希望对你有一定的参考价值。

上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定义View的详细绘制方法。如果把自定义View比作盖一座房子,那么上篇文章就相当于教会了我们怎么一步步的搭建房子的骨架,而本篇文章将要教会我们的是为房子的骨架添砖加瓦直至成型,甚至是怎么装修。

Canvas

为了后文更为方便的讲解Canvas的常用方法的使用,我们先来做一些准备工作,创建一个自定义View框架,先初始化一下Paint画笔,并设置相关方法:

public class StudyView extends View {

    private Paint mPaint;
    private Context mContext;

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

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

    private void init(Context context) {
        mContext = context;
        mPaint = new Paint();
        mPaint.setAntiAlias(true); // 消除锯齿
        mPaint.setStrokeWidth(5); // 设置笔尖宽度
        mPaint.setStyle(Paint.Style.STROKE); // 不填充
    }

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

绘制圆弧和扇形

Canvas提供drawArc()方法,通过传递不同的参数可用来绘制圆弧和扇形,此方法有两个重载方法,详细参数如下:

  • drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
  1. left:扇形或圆弧所占区域的左边界线x坐标
  2. top:扇形或圆弧所占区域的上边界线y坐标
  3. right:右边界线x坐标
  4. bottom:下边界线y坐标
  5. startAngle:扇形或圆弧的起始角度
  6. sweepAngle:扫过的角度
  7. userCenter:此参数可以理解为true就是画扇形,false就是画圆弧
  8. paint:画笔
  • drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

此方法第一个参数是一个RectF类,也是边界,就是把一个方法的left,top,right,bottom封装到了RectF类中,剩余参数与上一个方法一致。

接下来用着两个重载方法分别绘制两个90°的扇形和两个90°的圆弧:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 绘制扇形
    canvas.drawArc(0, 0, 200, 200, 0, 90, true, mPaint);
    RectF rectF = new RectF(0, 0, 200, 200);
    canvas.drawArc(rectF, 180, 90, true, mPaint);

    // 绘制圆弧
    canvas.drawArc(300, 0, 500, 200, 0, 90, false, mPaint);
    RectF rectF1 = new RectF(300, 0, 500, 200);
    canvas.drawArc(rectF1, 180, 90, false, mPaint);
}

绘制效果如下图所示,另外需要说明的一点是,drawArc的第五个参数startAngle中的角度,0°是指坐标系中第四象限中与x重合的角度,顺时针方向代表角度增大的方向,如下图中红色线条所示。

技术分享图片

绘制Bitmap

在Canvas中提供了drawBitmap方法,此方法可以让我们直接获取一张图片绘制到画布上,有了它可以让我们的自定义View锦上添花,同时也让我们实现一些复杂效果有了一个更加方便的途径。下面是drawBitmap的几个比较常用的重载方法:

  • drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
  1. bitmap:Bitmap资源文件
  2. left和top:代表了图片左上角落入的位置坐标。
  3. top:看2
  4. paint:画笔
  • drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
  1. src:在Bitmap图片上截取一部分作为绘制源,可null
  2. det:将绘制目标拉伸平铺到det指定的矩形中
  • drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
    同第二个重载方法,几乎一毛一样。

  • drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

  1. matrix:Matrix的参数传入是的drawBitmap功能变得异常强大,让此方法有意思了许多,通过matrix可以实现图片的平移(postTranslate())、缩放(postScale())、旋转(postRotate())、错切(postSkew())等等花式炫酷效果,由于Matrix的用法稍微多一些,篇幅限制,这里就先一带而过了,感兴趣的朋友可以自行探索。

在onDraw方法中drawBitmap的以上重载方法,注意在使用完Bitmap之后记得用Bitmap.recycle()来回收掉资源,以防止oom。

/** drawBitmap */
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), android.R.mipmap.sym_def_app_icon);
// 绘制图片
canvas.drawBitmap(bitmap, 0, 300, null);
// 将图片拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, null, new RectF(200, 300, 500, 500), null);
// 截取图片的四分之一拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2), new RectF(500, 300, 800, 500), null);

Matrix matrix = new Matrix();
matrix.postTranslate(800, 300); // 将bitmap平移到此位置
canvas.drawBitmap(bitmap, matrix, mPaint);

// 为防止oom,及时回收bitmap
bitmap.recycle();

效果如下图(红框内)。

技术分享图片

绘制圆形

  • drawCircle(float cx, float cy, float radius, Paint paint)
  1. cx:圆心x坐标
  2. cy:圆心y坐标
  3. radius:半径
canvas.drawCircle(100, 700, 100, mPaint);

效果如下图:

技术分享图片

绘制点

  • drawPoint(float x, float y, Paint paint)
  1. x:点的x坐标
  2. y:点的y坐标
  • drawPoints(float[] pts, Paint paint) 绘制一组点
  1. pts:float数组,两位为一组,两两结合代表x、y坐标,例如:pts[0]、pts[1]代表第一个点的x、y坐标,pts[2]、pts[3]代表第二个点的x、y坐标,依次类推。
  • drawPoints(float[] pts, int offset, int count, Paint paint) 绘制一组点
  1. pts:float数组,两位为一组,两两结合代表x、y坐标,例如:pts[0]、pts[1]代表第一个点的x、y坐标,pts[2]、pts[3]代表第二个点的x、y坐标,依次类推。
  2. offset:代表数组开始跳过几个只开始绘制点,注意这里不是指数组的下标,而是代表跳过几个值。
  3. count:在跳过offset个值后,处理几个值,注意这里的count不是代表点的个数,而是代表数组中值的个数。
canvas.drawPoint(100, 700, mPaint); // 绘制一个点

float[] points = new float[] {
    130, 700,
    160, 700,
    190, 700,
    210, 700,
    240, 700
};

canvas.drawPoints(points, 2, 4, mPaint); // 绘制一组点(代表跳过前两个值,处理4个值,也就是实际绘制2个点)

效果如下图:

技术分享图片

绘制椭圆

  • drawOval(float left, float top, float right, float bottom, Paint paint)
  1. left
  2. top
  3. right
  4. bottom

在left、top、right、bottom围成的区域内绘制一个椭圆。

  • drawOval(RectF oval, Paint paint)
  1. 将第一个重载方法的left、top、right、bottom封装到RectF类中,与扇形的重载方法异曲同工。
RectF rectF2 = new RectF(300, 600, 700, 800); // 创建一个RectF
canvas.drawOval(rectF2, mPaint);

效果如下图:

技术分享图片

绘制矩形

  • drawRect(float left, float top, float right, float bottom, Paint paint)
  • drawRect(Rect r, Paint paint)
  • drawRect(RectF rect, Paint paint)

drawRect的参数非常好理解,这里就不啰嗦了,直接上代码看效果:

canvas.drawRect(rectF2, mPaint);

注:这里的rectF2即上文绘制椭圆时创建的RectF对象。

技术分享图片

绘制圆角矩形

  • drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
  • drawRoundRect(RectF rect, float rx, float ry, Paint paint)

drawRoundRect是绘制圆角矩形,用法和drawRect类似,唯一不同的是多了两个参数:

  1. rx:x轴方向的圆角弧度
  2. ry:y轴方向的圆角弧度

上代码,看效果:

canvas.drawRoundRect(rectF2, 60, 30, mPaint);

这里为了突出两个方向的圆角弧度,特地将rx和ry设置差距比较大,效果如下图:

技术分享图片

绘制直线

  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
  • drawLines(float[] pts, int offset, int count, Paint paint)
  • drawLines(float[] pts, Paint paint)

drawLine和drawLines一个是绘制一个点,一个是绘制一组点,其中drawLines中的float数组中四个值为一组点,其用法可以参照drawPoints。

canvas.drawLine(100, 820, 800, 820, mPaint);

float[] lines = new float[]{
        100f, 850f, 800f, 850f,
        100f, 900f, 800f, 900f,
        100f, 950f, 800f, 950f
};
canvas.drawLines(lines, mPaint); // 按floats数组中,四个数为1组,绘制多条线

效果如下图:

技术分享图片

drawPath() 绘制不规则图形

上面的这些Canvas方法固然已经很强大了,但是我们如果想要绘制一些不规则的图形怎么办,这时候就要用到强大的drawPath()方法了,通过对Path进行设置不同的坐标、添加不同图形,最后传入drawPath方法中可以绘制出复杂的且不规则的形状。以下是drawPath的方法及参数:

  • drawPath(Path path, Paint paint)

这里的关键参数就是Path,Path类的方法较多,大部分用法类似,这里挑几个说一下:

  • Path类
  1. addArc(RectF oval, float startAngle, float sweepAngle) 往path里面添加一个圆弧
  2. addCircle(float x, float y, float radius, Path.Direction dir) 添加一个圆形
  3. addOval(RectF oval, Path.Direction dir) 添加一个椭圆
  4. addRect(RectF rect, Path.Direction dir) 添加一个矩形
  5. lineTo(float x, float y) 连线到坐标(x,y)
  6. moveTo(float x, float y) 将path绘制点移动到坐标(x,y)
  7. close() 用直线闭合图形,调用此方法后,path会将最后一处点与起始用直线连接起来,path起始点为moveTo()方法的坐标上,如果没有调用moveTo()起始点将默认为(0,0)坐标。
  8. ...

接下来使用drawPath绘制一个楼梯:

// 使用 Path 绘制一个楼梯
Path path = new Path();
path.lineTo(0, 1000);
path.lineTo(100, 1000);
path.lineTo(100, 1100);
path.lineTo(200, 1100);
path.lineTo(200, 1200);
path.lineTo(300, 1200);
path.lineTo(300, 1300);
path.lineTo(400, 1300);
path.lineTo(400, 1400);
path.lineTo(0, 1400);
path.lineTo(0, 1000);
path.close();
canvas.drawPath(path, mPaint);

效果如下图:

技术分享图片

再用drawPath方法绘制一个Android小机器人:

/ 使用 Path 绘制一个Android机器人
// 绘制两个触角
path.reset();
path.moveTo(625, 1050);
path.lineTo(650, 1120);
path.moveTo(775, 1050);
path.lineTo(750, 1120);

path.addArc(new RectF(600, 1100, 800, 1300), 180, 180); // 绘制头部
path.addCircle(666.66f, 1150, 10, Path.Direction.CW); // 绘制眼睛,CW:顺时针绘制, CCW:逆时针绘制
path.addCircle(733.33f, 1150, 10, Path.Direction.CW);
path.addRect(new RectF(600, 1200, 800, 1300), Path.Direction.CW);  // 身体
canvas.drawPath(path, mPaint);

效果图如下:

技术分享图片

最后,上文中Canvas示例的全部代码如下:

public class StudyView extends View {

    private Paint mPaint;
    private Context mContext;

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

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

    private void init(Context context) {
        mContext = context;
        mPaint = new Paint();
        mPaint.setAntiAlias(true); // 消除锯齿
        mPaint.setStrokeWidth(5); // 设置笔尖宽度
        mPaint.setStyle(Paint.Style.STROKE); // 不填充
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /** 1、drawArc */

        // 绘制扇形
        canvas.drawArc(0, 0, 200, 200, 0, 90, true, mPaint);
        RectF rectF = new RectF(0, 0, 200, 200);
        canvas.drawArc(rectF, 180, 90, true, mPaint);

        // 绘制圆弧
        canvas.drawArc(300, 0, 500, 200, 0, 90, false, mPaint);
        RectF rectF1 = new RectF(300, 0, 500, 200);
        canvas.drawArc(rectF1, 180, 90, false, mPaint);

        /** 2、drawBitmap */
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), android.R.mipmap.sym_def_app_icon);
        // 绘制图片
        canvas.drawBitmap(bitmap, 0, 300, null);
        // 将图片拉伸平铺在RectF矩形内
        canvas.drawBitmap(bitmap, null, new RectF(200, 300, 500, 500), null);
        // 截取图片的四分之一拉伸平铺在RectF矩形内
        canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2), new RectF(500, 300, 800, 500), null);

        Matrix matrix = new Matrix();
        matrix.postTranslate(800, 300); // 将bitmap平移到此位置
        canvas.drawBitmap(bitmap, matrix, mPaint);

        // 为防止oom,及时回收bitmap
        bitmap.recycle();

        /** 3、drawCircle */
        canvas.drawCircle(100, 700, 100, mPaint);

        /** 4、绘制一个点 */
        canvas.drawPoint(100, 700, mPaint); // 绘制一个点

        float[] points = new float[] {
            130, 700,
            160, 700,
            190, 700,
            210, 700,
            240, 700
        };

        canvas.drawPoints(points, 2, 4, mPaint); // 绘制一组点(代表跳过前两个值,处理4个值,也就是实际绘制2个点)


        RectF rectF2 = new RectF(300, 600, 700, 800); // 创建一个RectF

        /** 5、drawOval 绘制椭圆 */
        canvas.drawOval(rectF2, mPaint);

        /** 6、drawRect 绘制矩形*/
        canvas.drawRect(rectF2, mPaint);
        canvas.drawRoundRect(rectF2, 60, 30, mPaint);

        /** 7、drawLine */
        canvas.drawLine(100, 820, 800, 820, mPaint);

        float[] lines = new float[]{
                100f, 850f, 800f, 850f,
                100f, 900f, 800f, 900f,
                100f, 950f, 800f, 950f
        };
        canvas.drawLines(lines, mPaint); // 按floats数组中,四个数为1组,绘制多条线


        /** 8、drawPath */

        // 使用 Path 绘制一个楼梯
        Path path = new Path();
        path.moveTo(0, 1000);
        path.lineTo(100, 1000);
        path.lineTo(100, 1100);
        path.lineTo(200, 1100);
        path.lineTo(200, 1200);
        path.lineTo(300, 1200);
        path.lineTo(300, 1300);
        path.lineTo(400, 1300);
        path.lineTo(400, 1400);
        path.close();
        canvas.drawPath(path, mPaint);

        // 使用 Path 绘制一个Android机器人

        // 绘制两个触角
        path.reset();
        path.moveTo(625, 1050);
        path.lineTo(650, 1120);
        path.moveTo(775, 1050);
        path.lineTo(750, 1120);

        path.addArc(new RectF(600, 1100, 800, 1300), 180, 180); // 绘制头部
        path.addCircle(666.66f, 1150, 10, Path.Direction.CW); // 绘制眼睛,CW:顺时针绘制, CCW:逆时针绘制
        path.addCircle(733.33f, 1150, 10, Path.Direction.CW);
        path.addRect(new RectF(600, 1200, 800, 1300), Path.Direction.CW);  // 身体
        canvas.drawPath(path, mPaint);
    }
}

完整效果图如下:

技术分享图片

其实Canvas除了可以绘制图形之外,还可以绘制文字,Canvas的绘制文字的方法有drawText()、drawTextOnPath()、drawTextRun()等方法,在绘制文字是和Paint的结合更为紧密,所以讲绘制文字的方法放在下文和Paint一起讲可能效果会更好一些,好了,废话不多说了,接下来咱们就开始Paint的篇章。

Paint

为了更为清晰的讲解Paint的用法,先来新建一个自定义类,暂叫PaintStudyView,接下来创建一个它的大体骨架,在此类中定义了一些变量,变量的意义请见注释:

public class PaintStudyView extends View {

    private Paint mTextPaint;  // 绘制文字的Paint
    private Paint mPointPaint; // 绘制参考点的Paint
    private Context mContext;

    private final static float Y_SPACE = 100; // y轴方向的间距

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

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

    private void init(Context context) {
        mContext = context;
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true); // 消除锯齿
        mTextPaint.setStrokeWidth(1); // 设置笔尖宽度
        mTextPaint.setStyle(Paint.Style.FILL); // 填充
        mTextPaint.setTextSize(30);

        mPointPaint = new Paint();
        mPointPaint.setAntiAlias(true);
        mPointPaint.setStrokeWidth(5);
        mPointPaint.setColor(Color.RED); // 将参考点的Paint设置为红色
        mPointPaint.setStyle(Paint.Style.STROKE);// 不填充
    }

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

Canvas的绘制文字的相关方法:

drawText()的重载方法

drawText() 是Canvas的绘制文字中的最长用的方法,它只能按照从左至右的普通方式来绘制文字。

  • drawText(String text, float x, float y, Paint paint)
  1. text:待绘制的文字内容
  2. x:文字绘制位置的x坐标
  3. y:文字绘制位置的y坐标
  4. paint:Paint画笔,可以通过Paint.setTextAlign()来决定文字的方位,有:Paint.Align.LEFT(居左),Paint.Align.RIGHT(居右),Paint.Align.CENTER(居中)三个位置。
  • drawText(String text, int start, int end, float x, float y, Paint paint)
  1. start:代表从text中的第几个字符开始截取绘制,包含第start个字符。
  2. end:代表截取到text的第几个字符,不包含第end个字符。

例如:我是一个自定义View的控件,start=1,end=6,截取后为:是一个自定

下面两个重载方法可以参考第二个很容易就能理解:

  • drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
  • drawText(char[] text, int index, int count, float x, float y, Paint paint)

以下示例说明了文字的不同位置,同时也说明了第二个和第四个重载方法对字符串截取时的用法:

String str = "我是一个自定义View的控件";// 待绘制文字

float x = getWidth() / 2;
float y = 100;
canvas.drawPoint(x, y, mPointPaint); // 绘制参考点,便于观察文字处于x,y坐标的位置,从而来学习setTextAlign()方法

mTextPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(str, x, y, mTextPaint);

y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(str, 0, 6, x, y, mTextPaint);

y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(str.toCharArray(), 1, 6, x, y, mTextPaint);

效果图如下:

技术分享图片

其中的红点为额外添加的参考坐标,目的是为了突出setTextAlign中参数的位置。

drawTextOnPath()的重载方法

drawTextOnPath() 由方法名字我们就可以看出来他可以按照Path的走向来绘制文字,例如我们在path中传入一个圆弧,那么绘制出来的文字走向就是圆弧状的,是不是很酷,来看一下它的重载方法:

  • drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
  1. text:同drawText的第一个参数。
  2. path:Path参数,用法在前文已经说过了。
  3. hOffset:水平方向的偏移量。
  4. vOffset:垂直方向的偏移量。

关键点:有一点一定要提的就是,这里的hOffset是相对于path路径的水平偏移量,而vOffset也是相对于path路径的垂直偏移量,这么说可能还有点不清楚,结合下面的示例来说明,请仔细体会这里的意思:

// 1、下开口圆弧方向绘制文字
mTextPaint.setTextAlign(Paint.Align.LEFT);
y += Y_SPACE;
Path path = new Path();
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 180,180);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint); // 按照path路径绘制文字,不偏移
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);// 向水平、垂直方向各偏移30
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);// 向水平、垂直方向各偏移60

// 2、上开口圆弧方向绘制文字
path.reset();
y += Y_SPACE;
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 0, 180);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);
path.close();

// 3、竖直方向绘制文字
path.reset();
path.moveTo(200, y);
path.lineTo(200, y + 4 * Y_SPACE);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);


y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;

// 4、水平方向绘制文字
path.reset();
path.moveTo(x, y);
path.lineTo(x + 4 * Y_SPACE, y);
canvas.drawPath(path, mPointPaint); // 参考弧度线
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);

如下是效果图,注意看图片中的红色部分,红色的线是用代码绘制出来的path参考线,红色的箭头是path的水平和垂直方向的走向,结合下图可以更好的理解drawTextOnPath的hOffset和vOffset参数。

技术分享图片

  • drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)

这个方法的套路想必不用解释了。

drawTextRun()的重载方法

  • drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)
  • drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)

drawTextRun()可以文字的是从左到右还是从右到左的顺序来绘制,其中倒数第二个参数isRtl就是用来控制方向的,true就是倒序绘制,false就是正序绘制,其他的参数就没啥好说的了,这个方法用法比较简单,这里就不贴代码了。另外这个方法是在API 23才开始添加的,使用时要注意。

到目前为止,Canvas的常用用法基本介绍完了,接下来就可以着重来看Paint的使用了,Paint和Canvas两者是不可分离的,两者协作,相辅相成。所以在下面的用法示例中不免要用到Canvas的相关方法。

使用Paint测量文字的尺寸,定位文字

我们在开发自定义控件时,免不了要精确定位文字的文字,例如必须把文字放在某个区域的正中间,或者必须让一行文字的几何中心精确的处于某个点上,这时我们如果不懂这里的窍门可能就要盲目的试位置了,这样一点一点试出来的位置很不可靠,可能换个屏幕尺寸位置就不对了,接下来怎么来看看怎么样用最优雅的姿势来精确的定位文字。

其实在水平方向的定位还比较好说,直接使用Paint.setTextAlign()就能搞定大多需求,主要是在水平方向上稍稍复杂一点,想要定位位置,首先需要先获取文字的高度,要用到Paint的以下两个方法:

  • float ascent():根据文字大小获取文字顶端到文字基线的距离(返回的是负值)
  • float descent():根据文字大小获取文字底部到文字基线的距离(返回的事正值)

有了这两个方法那就非常好办了,首先用代码结合效果图说明一下基线、ascent、descent和文字的关系:

y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawLine(x - 300, y, x+300, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);// 水平方向上让文字居中
float ascent = mTextPaint.ascent(); // 根据文字大小获取文字顶端到文字基线的距离(返回的是负值)
float descent = mTextPaint.descent(); // 根据文字大小获取文字底部到文字基线的距离(返回的事正值)
canvas.drawLine(x - 300, y + ascent, x+300, y + ascent, mPointPaint);
canvas.drawLine(x - 300, y + descent, x+300, y + descent, mPointPaint);
canvas.drawText(str, x, y, mTextPaint);

效果图如下,它们直接的关系注意看图片里面的说明。

技术分享图片

接下来就让文字的中心落在参考点上:

// 将文字的中心定位在参考点上
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawText(str, x, y - ascent / 2 - descent / 2, mTextPaint);

效果图如下,仔细看参考点(红点)和文字的位置:

技术分享图片

利用Paint.setShader()(着色器)绘制渐变色

使用 setShader() 方法可以添加渐变颜色也可以使用图片作为背景,其参数是一个Shader类,传入不同的Shader子类可以实现不同的渐变效果或者添加背景图片,其子类有一下几种:

  • LinearGradient:线性渐变
  • RadialGradient:放射状渐变
  • SweepGradient:扫描渐变
  • BitmapShader:添加背景图片
  • ComposeShader:多种Shader组合

上面接个Shader的子类在使用方式上都差不多,这里只用LinearGradient为例说明一下,并注意对LinearGradient构造器的最后一个参数传入不同的参数对应的效果图:

/* Shader 渐变 */
y = 100;
Shader shader = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
        Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.CLAMP);
mTextPaint.setShader(shader);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);

y += 3 * Y_SPACE;
Shader shader1 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
        Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.REPEAT);
mTextPaint.setShader(shader1);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);

y += 3 * Y_SPACE;
Shader shader2 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
        Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.MIRROR);
mTextPaint.setShader(shader2);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);

效果图如下:

技术分享图片

除了以上这些,Paint的用法还有很多很多,一时半会也列不完,博主在这也只能抛砖引玉,感兴趣的朋友可查看官方API自行探索。


最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:


以上是关于Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解的主要内容,如果未能解决你的问题,请参考以下文章

Android查缺补漏(View篇)--自定义 View 中 wrap_content 无效的解决方案

Android查缺补漏(View篇)--事件分发机制源码分析

Android查缺补漏(View篇)--事件分发机制

Android查缺补漏(View篇)--布局文件中的“@+id”和“@id”有什么区别?

Android查缺补漏(View篇)--在 Activity 的 onCreate() 方法中为什么获取 View 的宽和高为0?

Android查缺补漏(线程篇)-- IntentService的源码浅析