阅读 66

自定义View实用小技巧

1.不用传入Context参数的DP转PX,在安卓中进行绘制最后显示都是以PX为单位的,所以我们一般需要用将设计图上的DP转为PX。

public static float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
复制代码

2.三角函数获取坐标值 通用代码

我们在绘制自定义View的过程中不可避免的经常会接触到三角函数,现提供一个通用的获取X,Y点的代码, 要注意的是我们绘制的时候0°是三点钟方向而非传统认知的12点钟方向,毕竟View的坐标系默认是以左上角为原点的。

在这里插入图片描述
据图所示我们最终的X,Y点其实就是(cos * 半径,sin * 半径),化为代码就为:

float cos = (float) Math.cos(Math.toRadians(angle));
float sin = (float) Math.sin(Math.toRadians(angle));
复制代码

这里注意我们的角度都以默认0°为起始点进行相加,如果画线的话则为:

canvas.drawLine(getWidth()/2,getHeight() / 2,
        (float) Math.cos(Math.toRadians(angle)) * RADIUS,  //RADIUS为半径,angle为角度,图示角度为90+90+90+60=240
        (float) Math.sin(Math.toRadians(angle)) * RADIUS,
        paint);
复制代码

3.Xfermode的使用

可以运用Xfermode绘制出多种重叠,交集等效果 如图:

在这里插入图片描述

public class XfermodeView extends View {
    private static final float RADIUS = DisplayUtil.dp2px(100);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Bitmap bitmap;

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

    private void init() {
        savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
        bitmap = getBitmap((int) RADIUS * 2);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.parseColor("#3F51B5"));
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);
        canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);
    }

    Bitmap getBitmap(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
复制代码

使用Xfermode之后的效果:

在这里插入图片描述

public class XfermodeView extends View {
    private static final float RADIUS = DisplayUtil.dp2px(100);
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    RectF savedArea = new RectF();
    Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    Bitmap bitmap;


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


    private void init() {
        savedArea.set(RADIUS, RADIUS, RADIUS * 2, RADIUS * 2);
        bitmap = getBitmap((int) RADIUS * 2);
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.parseColor("#3F51B5"));
        int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存状态 开启离屏缓冲
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, RADIUS, paint);//DST
        paint.setXfermode(xfermode);
        canvas.drawBitmap(bitmap, getWidth() / 2, getHeight() / 2, paint);//SRC
        paint.setXfermode(null);
        canvas.restoreToCount(saved);
    }


    Bitmap getBitmap(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
复制代码

可以看到模式设置为了SRC_IN,则最终效果为取SRC和DST的交集同时显示SRC的交集部分

  • 使用Xfermode需要注意的点:绘制之前使用离屏缓冲保存画布状态,绘制之后还原。开启离屏缓冲的原因是如果不开启那么是Xfermode是没效果的,因为默认的DST蒙版将会被认为是整个View。

    int saved = canvas.saveLayer(null,null, Canvas.ALL_SAVE_FLAG);//保存状态 开启离屏缓冲
    ...
    canvas.restoreToCount(saved);
    复制代码
  • 设置离屏缓冲时还可以指定裁取的大小,防止性能的浪费。

    RectF savedArea = new RectF();
    savedArea.set(left, top, right, bottom);
    int saved = canvas.saveLayer(savedArea, paint);
    复制代码

Tips设置setXfermode之前绘制的是DST,后绘制的是SRC(图例SRC是图片,DST是绘制的圆形,采用SRC_IN,最终取交集并且显示SRC图片的内容),具体的效果图可以参考下图:

在这里插入图片描述

4.文字的绘制

中心点的确定:

需要注意的是文字的X,Y起始点并不是左上角而是左下角。

canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 , paint);
复制代码

我们可以通过:

paint.setTextAlign(Paint.Align.CENTER);
复制代码

来设置文字的中心点,如此设置之后X的起始点就为你所定义的位置了。 但是绘制过后文字是会偏上的,因为默认的点为BaseLine,我们需要将文字下移偏移量才是一个真正的中点值。

在这里插入图片描述

paint.setTextSize(DisplayUtil.dp2px(50));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
float offset = (rect.top + rect.bottom) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
复制代码

这样减去偏移量文字将会下移为真正的中点,但是注意这种方法是基于BaseLine的,所以当文字确定不会改变的时候用这种方式比较合适。

在这里插入图片描述

会改变的文字用:

Paint.FontMetrics fontMetrics = new Paint.FontMetrics();

paint.getFontMetrics(fontMetrics);
paint.setTextSize(DisplayUtil.dp2px(100));
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;
canvas.drawText("abcd", getWidth() / 2, getHeight() / 2 - offset, paint);
复制代码

这种方式不会随着文字的BaseLine而改变,防止因为文字改变可能出现的跳跃问题。

5.文字的左对齐

需要减去左边的文字默认间距,如下:

// 绘制文字左对齐
paint.setTextAlign(Paint.Align.LEFT);
Rect rect = new Rect();
paint.getTextBounds("abcd", 0, "abcd".length(), rect);
canvas.drawText("abcd", 0 - rect.left, 300, paint);
复制代码

6.文字的多行绘制

  • 如果是仅仅多行绘制那么非常简单,直接使用StaticLayout就可以了:

     {
        staticLayout = new StaticLayout(text, textPaint, 600, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
         //StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,
         // 它是纯粹用来绘制文字的。 StaticLayout 支持换行,它既可以为文字设置宽度上限来让文字自动换行,也会在 \\n 处主动换行。
         //参数说明
         //width 是文字区域的宽度,文字到达这个宽度后就会自动换行;
         //align 是文字的对齐方向;
         //spacingmult 是行间距的倍数,通常情况下填 1 就好;
         //spacingadd 是行间距的额外增加值,通常情况下填 0 就好;
         //includeadd 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。
     }    
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         // 使用 StaticLayout 代替 Canvas.drawText() 来绘制文字,
         // 以绘制出带有换行的文字
         canvas.save();
         canvas.translate(50, 40);
         staticLayout.draw(canvas);
         canvas.restore();
     }
    复制代码
  • 文字的精确折行

一般用于跟图片相交的需求使用: 主要是两个API的使用:

  1. paint.breakText();
  2. canvas.drawText();
int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
//截取字符串。
//参数为绘制的文字,开始字符截取的字符,终止字符,是否顺时,截取的宽度,保存截取的宽度
canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
//第二和第三个参数为字符串的起始点和终止点
复制代码

我们的中心思想就是每次用breakText计算出当次的起点文字到终点文字的长度,根据长度计算出文字的终止点是哪里,然后用drawText根据起止点和终止点截取文字并绘制。 下边以一个实例来说明,上代码:

在这里插入图片描述

  public class ImageTextView extends View {
        private static final float IMAGE_WIDTH = DisplayUtil.dp2px(120);
        private static final float IMAGE_Y = DisplayUtil.dp2px(50);
        private boolean isLeft = true;
        private boolean isInImage = false;
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        Bitmap bitmap;
        Paint.FontMetrics fontMetrics = new Paint.FontMetrics();
        String text = "This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text,This is text.";
        float[] cutWidth = new float[1];
    
        public ImageTextView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }

    private void init() {
        bitmap = getAvatar((int) IMAGE_WIDTH);
        paint.setTextSize(DisplayUtil.dp2px(14));
        paint.getFontMetrics(fontMetrics);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制文字
        canvas.drawBitmap(bitmap, getWidth() / 2 - IMAGE_WIDTH / 2, IMAGE_Y, paint);

        int length = text.length();
        float verticalOffset = -fontMetrics.top;

        for (int start = 0; start < length; ) {
            int maxWidth;
            float textTop = verticalOffset + fontMetrics.top;
            float textBottom = verticalOffset + fontMetrics.bottom;
             //判断是否在图片区域内
            if (textTop > IMAGE_Y && textTop < IMAGE_Y + IMAGE_WIDTH 
                    || textBottom > IMAGE_Y && textBottom < IMAGE_Y + IMAGE_WIDTH) {
                // 文字和图片在同一行,减去图片的宽度
                isInImage = true;
                maxWidth = (int) (getWidth() / 2 - IMAGE_WIDTH / 2);
            } else {
                isInImage = false;
                // 文字和图片不在同一行
                maxWidth = getWidth();
            }
            int count = paint.breakText(text, start, length, true, maxWidth, cutWidth);
            if (isInImage) {//如果是图片显示区域内
                if (isLeft) { //在图片左边
                    isLeft = false;
                    canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
                } else { //在图片右边
                    isLeft = true;
                    canvas.drawText(text, start, start + count, getWidth() / 2 + IMAGE_WIDTH / 2, verticalOffset, paint);
                    verticalOffset += paint.getFontSpacing();  //再右边才换行
                }
            } else {
                canvas.drawText(text, start, start + count, 0, verticalOffset, paint);
                verticalOffset += paint.getFontSpacing(); //换行
            }
            start += count;
        }
    }

    Bitmap getAvatar(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ic_default_apk, options);
    }
}
复制代码

如上,此例我们总体的思路就是判断当前文字是否在图片的显示高度之类,如果在图片高度范围之内则做截取字符的处理,在判断是否超过高度的时候可以根据自己的逻辑来设置,这里只是提供一种思路,实际情况可以根据自己的需求计算处理。

7.canvas的裁剪和变换

canvas的裁剪主要有4个API:

  1. canvas.clipRect();
  2. canvas.clipPath();
  3. canvas.clipOutPath();
  4. canvas.clipOutRect();

当进行裁剪之后你绘制的部分只能是在你绘制的部分中被显示出来,如下所示:

在这里插入图片描述

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.clipRect(0, 0, 100, 100);
    canvas.drawBitmap(bitmap, 0, 0, paint);
}
复制代码

可以看到只绘制了被切割矩形的部分。

Tips:这里还需要注意的一点是当进行了clipPath操作之后画笔的抗锯齿效果就会无效了,画出的东西很有可能是带有毛边的,比如用clipPath切割一个圆形然后绘制一个圆形的头像,在这种情况下就可以考虑Xfermode而非clipPath了。

canvas的变换:

  1. canvas.rotate(degree);

  2. canvas.translate(x,y);

  3. canvas.scale(x,y);

  4. canvas.skew(x,y);

这里只需要注意一点,canvas的变换是改变的坐标系起始点也就是左边系的原点,比如当我调用tranlate之后在调用rotate的情况是这样的:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    bitmap = getAvatar(100);
    canvas.translate(200,200);
    canvas.rotate(45);
    canvas.drawBitmap(bitmap, 0, 0, paint);
}
复制代码

在这里插入图片描述
可以看到最后绘制是在0,0点绘制的bitmap图片,也就是说我们每次对坐标系的操作其实都是操作的左边系的原点。

Tips:需要注意的一点是通常进行canvas的变换或裁剪之前都需要调用canvas.save()去保存canvas状态,绘制完成之后调用canvas.restore()去还原canvas的坐标,如果不还原的话之后的绘制都会以你改变后的原点为基础进行绘制。

关注下面的标签,发现更多相似文章
评论