(原创)手写一个图片裁剪工具

前言及效果展示

最近项目里要用到图片裁剪的功能,于是乎写了一个支持裁剪的自定义View
然后做了一个简单的demo项目。在这里分享出来
先来看下具体的效果吧
首先是一个入口页面,就是一个按钮,支持跳转到裁剪页面
当然这里会传一张固定的图片进去
在这里插入图片描述
裁剪界面我做成了一个Activity
长这样:
在这里插入图片描述
最后,裁剪完了之后把图片带回去
在这里插入图片描述
下面看下具体的实现介绍
文末会放上demo代码

核心工具类

现在安装裁剪的流程来介绍核心的一些实现
完整的内容可以参看文末的源码
首先看首页Mainactivity是如何跳转的:

        findViewById(R.id.clipimgBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, ClipPictureActivity.class);
                String imgpath = "/storage/emulated/0/DCIM/test.PNG";
                ClipPictureStrategy clipPictureStrategy = new ClipPictureStrategy(imgpath);
                clipPictureStrategy.setClipmsg("裁剪图片");
                intent.putExtra("strategy", clipPictureStrategy);
                startActivityForResult(intent, requestCode);
            }
        });

这边跳转裁剪页面时,把参数列表做成了一个实体类ClipPictureStrategy
这个类可以用来定制我们要裁剪的一些参数
其中图片路径是必须传的
其他的有默认值
这块提一下,其实这里可以用kotlin实现
或者采用建造者模式
但因为是demo,就不过分炫技了
这个类里面目前有下面这些属性
相关属性的作用都在下面注释了

    private String imagePath = "";//图片路径
    private String clipmsg = "";//裁剪框底部文案
    private float screenHeightFloat = 0.3f;//裁剪框距离顶部占据屏幕高度的百分比(默认距离屏幕顶部的距离为屏幕高度的30%)
    private float screenWidthFloat = 0.6f;//裁剪框的宽占据屏幕宽度的百分比(默认宽度为屏幕宽度的60%)

有了这些属性,我们在绘制页面时,就可以方便地处理裁剪框的位置和大小了
当然,这边裁剪框是正方形的,也可以改成矩形
下面来到裁剪页面
这个页面的功能,其实都是由QRCropImageView这个自定义View来实现的
它里面支持对图片的旋转,缩放,平移
然后根据裁剪框的大小来裁剪图片
这边也把源码贴上来了

/**
 * @introduce : 裁剪图片控件
 * <p>
 * creattime : 2021/11/25
 * <p>
 * author : xiongyp
 **/
@SuppressLint("AppCompatCustomView")
public class QRCropImageView extends ImageView {

    private float x_down = 0;
    private float y_down = 0;
    private PointF mid = new PointF();
    private float oldDist = 1f;
    private float oldRotation = 0;
    private Matrix matrix;
    private Matrix matrix1 = new Matrix();
    private Matrix savedMatrix = new Matrix();

    private static final int NONE = 0;
    private static final int DRAG = 1;
    private static final int ZOOM = 2;
    private int mode = NONE;

    private boolean matrixCheck = false;

    private int widthScreen;
    private int heightScreen;

    private Bitmap gintama;
    private Bitmap drawBitmap;


    private Paint paintLine;
    private Paint paintShade;
    private Paint paintCorner;

    private Rect rectTop;
    private Rect rectBottom;
    private Rect rectLeft;
    private Rect rectRight;
    private Rect cropRect;

    private int lineWidth;
    private int cornerWidth;
    private int cornerLength;
    private int paddingH;
    private Activity activity;

    private int left, top, right, bottom;

    private int rectHeight = 0;

    private int marginTop = 0;
    private int width2;
    private int width;
    private int height;

    public QRCropImageView(Activity activity, Bitmap bitmap) {
        super(activity);
        gintama = bitmap;
        this.activity = activity;
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        widthScreen = dm.widthPixels;
        heightScreen = dm.heightPixels;
        matrix = new Matrix();

        paintLine = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintLine.setColor(Color.WHITE);
        paintLine.setStyle(Paint.Style.STROKE);//设置空心
        lineWidth = (int) dp2px(activity, 0.5f);
        paintLine.setStrokeWidth(lineWidth);

        paintShade = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintShade.setColor(Color.parseColor("#CC000000"));

        cornerWidth = (int) dp2px(activity, 4);
        cornerLength = (int) dp2px(activity, 25);
        paintCorner = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintCorner.setColor(Color.WHITE);
        paintCorner.setStrokeWidth(cornerWidth);

        paddingH = (int) dp2px(activity, 30);

        // 初始化居中缩放
        int bw = gintama.getWidth();
        int bh = gintama.getHeight();
        float scale = Math.min(1f * widthScreen / bw, 1f * heightScreen / bh);
        Log.e("scale==", "" + scale);
        drawBitmap = scaleBitmap(gintama, scale);
        matrix.postTranslate(0, 1f * heightScreen / 2 - (bh * scale / 2));
    }


    protected void onDraw(Canvas canvas) {
        width = getWidth();
        height = getHeight();

        canvas.save();
        canvas.drawBitmap(drawBitmap, matrix, null);
        canvas.restore();

        paddingH = (width - rectHeight) / 2;
        // 四边线
        left = paddingH;
        top = marginTop;
        right = width - paddingH;
//        bottom = (int) (height * 0.51);
        bottom = top + rectHeight;
        if (rectTop == null) {
            rectTop = new Rect(0, 0, width, top);
            rectBottom = new Rect(0, bottom, width, height);
            rectLeft = new Rect(0, top, left, bottom);
            rectRight = new Rect(right, top, width, bottom);
        }
        //绘制上下蒙层
        canvas.drawRect(rectTop, paintShade);
        canvas.drawRect(rectBottom, paintShade);
        //绘制左右蒙层
        canvas.drawRect(rectLeft, paintShade);
        canvas.drawRect(rectRight, paintShade);

        //  裁切区域矩形
        if (cropRect == null) {
            cropRect = new Rect(left, top, right, bottom);
        }
        canvas.drawRect(cropRect, paintLine);

        width2 = cornerWidth / 2;
        // 绘制四角
        canvas.drawLine(left, top + width2, left + cornerLength, top + width2, paintCorner);
        canvas.drawLine(left + width2, top, left + width2, top + cornerLength, paintCorner);

        canvas.drawLine(right, top + width2, right - cornerLength, top + width2, paintCorner);
        canvas.drawLine(right - width2, top, right - width2, top + cornerLength, paintCorner);

        canvas.drawLine(left, bottom - width2, left + cornerLength, bottom - width2, paintCorner);
        canvas.drawLine(left + width2, bottom, left + width2, bottom - cornerLength, paintCorner);

        canvas.drawLine(right, bottom - width2, right - cornerLength, bottom - width2, paintCorner);
        canvas.drawLine(right - width2, bottom, right - width2, bottom - cornerLength, paintCorner);

    }


    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mode = DRAG;
                x_down = event.getX();
                y_down = event.getY();
                savedMatrix.set(matrix);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                mode = ZOOM;
                oldDist = spacing(event);
                oldRotation = rotation(event);
                savedMatrix.set(matrix);
                midPoint(mid, event);
                break;
            case MotionEvent.ACTION_MOVE:
                if (mode == ZOOM) {
                    matrix1.set(savedMatrix);
                    float rotation = rotation(event) - oldRotation;
                    float newDist = spacing(event);
                    float scale = newDist / oldDist;
                    matrix1.postScale(scale, scale, mid.x, mid.y);// 縮放
                    matrix1.postRotate(rotation, mid.x, mid.y);// 旋轉
                    matrixCheck = matrixCheck();
                    if (matrixCheck == false) {
                        matrix.set(matrix1);
                        invalidate();
                    }
                } else if (mode == DRAG) {
                    matrix1.set(savedMatrix);
                    matrix1.postTranslate(event.getX() - x_down, event.getY()
                            - y_down);// 平移
                    matrixCheck = matrixCheck();
                    matrixCheck = matrixCheck();
                    if (matrixCheck == false) {
                        matrix.set(matrix1);
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                break;
        }
        return true;
    }

    private boolean matrixCheck() {
        float[] f = new float[9];
        matrix1.getValues(f);
        // 图片4个顶点的坐标
        float x1 = f[0] * 0 + f[1] * 0 + f[2];
        float y1 = f[3] * 0 + f[4] * 0 + f[5];
        float x2 = f[0] * drawBitmap.getWidth() + f[1] * 0 + f[2];
        float y2 = f[3] * drawBitmap.getWidth() + f[4] * 0 + f[5];
        float x3 = f[0] * 0 + f[1] * drawBitmap.getHeight() + f[2];
        float y3 = f[3] * 0 + f[4] * drawBitmap.getHeight() + f[5];
        float x4 = f[0] * drawBitmap.getWidth() + f[1] * drawBitmap.getHeight() + f[2];
        float y4 = f[3] * drawBitmap.getWidth() + f[4] * drawBitmap.getHeight() + f[5];
        // 图片现宽度
        double width = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        // 缩放比率判断
        if (width < widthScreen / 3 || width > widthScreen * 3) {
            return true;
        }
        // 出界判断
        if ((x1 < widthScreen / 3 && x2 < widthScreen / 3
                && x3 < widthScreen / 3 && x4 < widthScreen / 3)
                || (x1 > widthScreen * 2 / 3 && x2 > widthScreen * 2 / 3
                && x3 > widthScreen * 2 / 3 && x4 > widthScreen * 2 / 3)
                || (y1 < heightScreen / 3 && y2 < heightScreen / 3
                && y3 < heightScreen / 3 && y4 < heightScreen / 3)
                || (y1 > heightScreen * 2 / 3 && y2 > heightScreen * 2 / 3
                && y3 > heightScreen * 2 / 3 && y4 > heightScreen * 2 / 3)) {
            return true;
        }
        return false;
    }

    // 旋转
    public void drawRotation(int rotation) {
        matrix.preRotate(rotation, (float) drawBitmap.getWidth() / 2, (float) drawBitmap.getHeight() / 2);  //要旋转的角度
        invalidate();
    }

    // 触碰两点间距离
    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    // 取手势中心点
    private void midPoint(PointF point, MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        point.set(x / 2, y / 2);
    }

    // 取旋转角度
    private float rotation(MotionEvent event) {
        double delta_x = (event.getX(0) - event.getX(1));
        double delta_y = (event.getY(0) - event.getY(1));
        double radians = Math.atan2(delta_y, delta_x);
        return (float) Math.toDegrees(radians);
    }

    // 将移动,缩放以及旋转后的图层保存为新图片
    // 本例中沒有用到該方法,需要保存圖片的可以參考
    public Bitmap createCropBitmap() {
        // 创建旋转缩放样本
        Bitmap bitmap = Bitmap.createBitmap(widthScreen, heightScreen,
                Bitmap.Config.ARGB_8888); // 背景图片
        Canvas canvas = new Canvas(bitmap); // 新建画布
        canvas.drawBitmap(drawBitmap, matrix, null); // 画图片
        canvas.save(); // 保存画布
        canvas.restore();
        // 返回裁切样本
        return Bitmap.createBitmap(bitmap, cropRect.left, cropRect.bottom - (cropRect.bottom - cropRect.top)
                , cropRect.right - cropRect.left, cropRect.bottom - cropRect.top);
    }

    /**
     * 根据给定的宽和高进行拉伸
     *
     * @param origin 原图
     * @param scale  缩放比例
     * @return new Bitmap
     */
    private Bitmap scaleBitmap(Bitmap origin, float scale) {
        if (origin == null) {
            return null;
        }
        int height = origin.getHeight();
        int width = origin.getWidth();
        Matrix matrix = new Matrix();
        matrix.postScale(scale, scale);// 使用后乘
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        return newBM;
    }

    public int getRectHeight() {
        return rectHeight;
    }

    //矩形的宽高
    public void setRectHeight(int rectHeight) {
        this.rectHeight = rectHeight;
        invalidate();
    }

    //距离顶部的高度
    public void setMarginTop(int marginTop) {
        this.marginTop = marginTop;
        invalidate();
    }

    public int getMarginTop() {
        return marginTop;
    }


    /**
     * 将dp单位转成px
     *
     * @param context context {@link Context}
     * @param value   value
     * @return 转化之后的值
     */
    public float dp2px(@Nullable Context context, float value) {
        if (context == null) {
            return Float.MIN_EXPONENT;
        }
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics);
    }
}

裁剪页面这边还有一些对于图片的压缩功能,也做成了一个工具类

/**
 * @introduce :
 *
 * creattime : 2021/12/13
 *
 * author : xiongyp
 *
**/
public class StreamUtil {

    /**
     * 根据路径获得图片并压缩,返回bitmap(票小秘压缩方案)
     *
     * @param filePath filePath
     * @return Bitmap
     */
    public static Bitmap getSmallBitmap(String filePath, int srcWidth, int srcHeight) {
        try {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(filePath, options);
            // Calculate inSampleSize
            options.inSampleSize = computeSize(srcWidth, srcHeight);
            // Decode bitmap with inSampleSize set
            options.inJustDecodeBounds = false;

            Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
            return bitmap;
        } catch (Exception e) {
            Log.e("CassStreamUtil", e.getMessage());
        }
        return null;
    }


    /**
     * 这里的算法是以短边压缩到1000~2000之间为目标,通过计算到1000的比值,然后需要将采样率控制为2的倍数
     * 所以需要使用方法{@link #calInSampleSize(int)}进行计算
     * (票小秘压缩方案)
     *
     * @return 采样率
     */
    public static int computeSize(int srcWidth, int srcHeight) {
        srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
        srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

        int shortSide = Math.min(srcWidth, srcHeight);

        int rate = (int) Math.floor(shortSide / 1000.0);

        return calInSampleSize(rate);
    }

    /**
     * 通过移位操作计算采样率,是某个整数对应的二进制数保留最高位为1,其他位置为0的结果
     * (票小秘压缩方案)
     *
     * @param rate 比例
     * @return 采样率
     */
    private static int calInSampleSize(int rate) {
        int i = 0;
        while ((rate >> (++i)) != 0) ;
        return 1 << --i;
    }

}

当然,如果你想和demo里一样要保存图片,记得申请存储权限

一些异常

在运行demo时,发现了系统的一个API的问题

Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);

这个decodeFile返回的bitmap为null
查阅了相关资料,解决方法如下:
在配置清单的Application标签里加入如下内容即可:

android:requestLegacyExternalStorage="true"

源码放送

百度网盘如下:
链接:https://pan.baidu.com/s/1rkeWDYOwSNoFcENPOahQHA
提取码:wks2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值