Android实现特定形状的图片

这篇博客详细介绍了在Android中如何实现心形图片,包括三种不同的实现方法:使用Canvas的clipPath方法剪裁Canvas,通过Paint.setShader只给指定区域上色,以及利用PorterDuffXfermode混合图层。每种方法都有其优缺点,并提供了相应的代码实现。

https://blog.csdn.net/YX_BB/article/details/104561359
在上篇文章中,我们实现了圆形图片圆角图片,基本上已经满足了日常开发中的需要。那如果想要更多的图形效果该怎么办呢?与我们在现实中的绘图方式一致,用笔(Paint)在纸(Canvas)上按照一定的路径(Path),即可形成特定的图画。一个图形的Path是固定,所以,我们可以通过改变Paint和Canvas来实现想要的效果。如下
在这里插入图片描述

这里以心形图片为例,展示几种不同的实现方法,首先先实现心形图片的Path。

公式:t 表示点(x,y)在坐标系中的角度
x = 16 sin^3 t
y = 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t

   /**
     * 心形曲线
     */
    private Path getHeartPath() {
        int n = 100;
        // 计算缩放比例
        float scale = getWidth() / 17f / 2f;
        // 将360度平均分为 100份,每个弧度对应一个点
        float interval = (float) (2 * Math.PI / 100);
        // 定义初始弧度degree
        float degree = 0;
        Point[] points = new Point[n];
        for (int i = 0; i < n; i++) {
            // 根据心形曲线公式,计算出每个弧度对应的点的坐标
            // 当degree = 90度的时候,x取最大值16。当degree = 180度的时候,y取最小值 -17。
            // 即保证y * scale * 17 * 2 = height的时候,曲线的与控件相切,以此计算出scale的值
            float x = (float) ((16 * Math.pow(Math.sin(degree), 3)) * scale);
            float y = (float) ((13 * Math.cos(degree) - 5 * Math.cos(2 * degree) - 2 * Math.cos(3 * degree) - Math.cos(4 * degree)) * scale);
            points[i] = new Point(x + getWidth() / 2f, -y + getHeight() / 2f);
            degree = degree + interval;
        }
        // 连线
        Path path = new Path();
        path.moveTo(points[0].x, points[0].y);
        for (int i = 1; i < n; i++) {
            path.lineTo(points[i].x, points[i].y);
        }
        path.close();
        return path;
    }

思路1:使用Canvas的clipPath方法直接剪裁Canvas。

优点简单粗暴,可以适应于各种控件且不需要考虑绘制中的各种情形。缺点是剪裁之后,Canvas的绘制区域将被Path限制,任何的内容及后续扩展都局限在了这个Path之内。且clipPath方法不支持硬件加速,当应用开启了硬件加速时,设备在4.0.4与4.0.3这样的版本上使用图片剪裁功能的时候会crash。
重写onDraw()方法

@Override
    protected void onDraw(Canvas canvas) {
        // 调用父类的绘制方法之前直接根据模式裁剪Canvas
        canvas.clipPath(getHeartPath());
        super.onDraw(canvas);
    }

控件继承自ImageView,注意要在super.onDraw()之前剪裁Canvas。否则Canvas已经将图片绘制到了View中,这个时候再剪裁Canvas毫无意义

思路2:使用Paint.setShader方法,只给指定区域上色。

Shader在三维软件中称之为着色器,就是用来给空白图形上色用的。
关于Shader的详细介绍可以参考
https://blog.csdn.net/harvic880925/article/details/52039081
首先获取ImageView上的Drawable,将Drawable转化成Bitmap,然后给Paint设置BitmapShader着色器,Bitmap在指定Path中的区域将被上色,Path之外的区域会被忽略。这种方式不会改变Canvas,也不会对后续的绘制工作造成影响
重写onDraw()方法

@Override
    protected void onDraw(Canvas canvas) {
        if (mBitmap == null) {
            mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            Canvas drawCanvas = new Canvas(mBitmap);
            Drawable drawable = getDrawable();
            if (drawable != null) {
                drawable.draw(drawCanvas);
            }

            mPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
            canvas.drawPath(getHeartPath(), mPaint);
        }
    }

注意:使用这种方式,不需要再使用super.onDraw(canvas)

思路3:使用PorterDuffXfermode混合图层

分别绘制图片图层,与路径图层。然后将两个图层混合。

 @Override
    protected void onDraw(Canvas canvas) {
        // 图层混合绘制方式
        srcBmp = makeSrc();
        dstBmp = makeDst();
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(srcBmp, 0, 0, mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(dstBmp, 0, 0, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layerID);
    }
    
   /**
     * 源图层
     */
    private Bitmap makeSrc() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Drawable drawable = getDrawable();
        if (drawable != null) {
            drawable.draw(canvas);
        }
        return bitmap;
    }

    /**
     * 目标图层
     */
    private Bitmap makeDst() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        canvas.drawPath(getHeartPath(), paint);
        return bitmap;
    }

注意:

int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.restoreToCount(layerID);

如果不加如这段代码,在进行图层混合的时候,源图层将会被直接绘制在原始Canvas上。然后再绘制目标图层的时候,会与整个原始Canvas取交集,这时会将非交集区域的颜色全部清空,从而漏出Activity的底色。
在这里插入图片描述
在加入这段代码之后,Canvas.saveLayer(),会生成新的全透明的Bitmap,后续的绘制都是在这个新图层上完成的。混合之后再绘制在原始Canvas上。这时边缘区域显示的颜色为ImageView父布局的背景色。简单理解两者的区别如下
未使用saveLayer时,3 - 2 - 1 = 0
而使用saveLayer时,3 - (2 - 1) = 2
显然两者的计算结果不一样了,而后者才是我们真正需要的绘制流程。
关于saveLayer()的用法可以参考
https://blog.csdn.net/harvic880925/article/details/51317746

完整代码

public class ShapeImageView extends AppCompatImageView {

    private Paint mPaint;
    private Bitmap srcBmp;
    private Bitmap dstBmp;
    private Xfermode xfermode;

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

    private void initData() {
        mPaint = new Paint();
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        // 图层混合绘制方式
        srcBmp = makeSrc();
        dstBmp = makeDst();
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(srcBmp, 0, 0, mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(dstBmp, 0, 0, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layerID);


        // BitmapShader绘制方式
//        if (srcBmp == null) {
//            srcBmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
//            Canvas drawCanvas = new Canvas(srcBmp);
//            Drawable drawable = getDrawable();
//            if (drawable != null) {
//                drawable.draw(drawCanvas);
//            }
//
//            mPaint.setShader(new BitmapShader(srcBmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
//            canvas.drawPath(getHeartPath(), mPaint);
//        }
        // clipPath绘制方式
//        canvas.clipPath(getHeartPath());
//        super.onDraw(canvas);
    }

    /**
     * 源图层
     */
    private Bitmap makeSrc() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Drawable drawable = getDrawable();
        if (drawable != null) {
            drawable.draw(canvas);
        }
        return bitmap;
    }

    /**
     * 目标图层
     */
    private Bitmap makeDst() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        canvas.drawPath(getHeartPath(), paint);
        return bitmap;
    }


    /**
     * 画心形曲线
     */
    private Path getHeartPath() {
        int n = 100;
        // 计算缩放比例
        float scale = getWidth() / 17f / 2f;
        // 将360度平均分为 100份,每个弧度对应一个点
        float interval = (float) (2 * Math.PI / 100);
        // 定义初始弧度degree
        float degree = 0;
        Point[] points = new Point[n];
        for (int i = 0; i < n; i++) {
            // 根据心形曲线公式,计算出每个弧度对应的点的坐标
            // 当degree = 90度的时候,x取最大值16。当degree = 180度的时候,y取最小值 -17。
            // 即保证y * scale * 17 * 2 = height的时候,曲线的与控件相切,以此计算出scale的值
            float x = (float) ((16 * Math.pow(Math.sin(degree), 3)) * scale);
            float y = (float) ((13 * Math.cos(degree) - 5 * Math.cos(2 * degree) - 2 * Math.cos(3 * degree) - Math.cos(4 * degree)) * scale);
            points[i] = new Point(x + getWidth() / 2f, -y + getHeight() / 2f);
            degree = degree + interval;
        }
        // 连线
        Path path = new Path();
        path.moveTo(points[0].x, points[0].y);
        for (int i = 1; i < n; i++) {
            path.lineTo(points[i].x, points[i].y);
        }
        path.close();
        return path;
    }

    public static class Point {
        float x;
        float y;

        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值