(original) handwriting a picture cutting tool

Introduction and effect display

Recently, the function of image clipping is used in the project, so a custom View supporting clipping is written
Then I made a simple demo project. Share it here
Let's look at the specific effect first
The first is an entry page, which is a button that supports jumping to the clipping page
Of course, a fixed picture will be sent here

I made an Activity by cutting the interface
It looks like this:

Finally, take the picture back after cutting

Let's take a look at the specific implementation
The demo code will be placed at the end of the text

Core tools

Now install the tailoring process to introduce some implementations of the core
See the source code at the end of the article for the complete content
First, let's look at how Mainactivity jumps on the homepage:

        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("Crop picture");
                intent.putExtra("strategy", clipPictureStrategy);
                startActivityForResult(intent, requestCode);
            }
        });

When you jump to the clipping page, make the parameter list into an entity class ClipPictureStrategy
This class can be used to customize some parameters we want to tailor
The picture path must be transmitted
Others have default values
In fact, it can be implemented with kotlin
Or adopt the builder mode
But because it's a demo, it's not too showy
This class currently has the following properties
The functions of related attributes are explained in the following comments

    private String imagePath = "";//Picture path
    private String clipmsg = "";//Copy at the bottom of the clipping box
    private float screenHeightFloat = 0.3f;//The percentage of the clipping box from the top to the screen height (the default distance from the top of the screen is 30% of the screen height)
    private float screenWidthFloat = 0.6f;//The width of the clipping box as a percentage of the screen width (the default width is 60% of the screen width)

With these properties, we can easily handle the position and size of the clipping box when drawing the page
Of course, the clipping box here is square, or it can be changed into a rectangle
Now come to the clipping page
The functions of this page are actually implemented by the custom View QRCropImageView
It supports the rotation, zoom and translation of pictures
Then crop the picture according to the size of the crop box
The source code is also posted here

/**
 * @introduce : Crop picture control
 * <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);//Set void
        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);

        // Initialize centered zoom
        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;
        // Quadrilateral line
        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);
        }
        //Draw upper and lower masks
        canvas.drawRect(rectTop, paintShade);
        canvas.drawRect(rectBottom, paintShade);
        //Draw left and right masks
        canvas.drawRect(rectLeft, paintShade);
        canvas.drawRect(rectRight, paintShade);

        //  Cut area rectangle
        if (cropRect == null) {
            cropRect = new Rect(left, top, right, bottom);
        }
        canvas.drawRect(cropRect, paintLine);

        width2 = cornerWidth / 2;
        // Draw four corners
        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);// Zoom
                    matrix1.postRotate(rotation, mid.x, mid.y);// Spin
                    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);// translation
                    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);
        // Coordinates of 4 vertices in the picture
        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];
        // Current width of picture
        double width = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        // Scaling ratio judgment
        if (width < widthScreen / 3 || width > widthScreen * 3) {
            return true;
        }
        // Out of bounds judgment
        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;
    }

    // rotate
    public void drawRotation(int rotation) {
        matrix.preRotate(rotation, (float) drawBitmap.getWidth() / 2, (float) drawBitmap.getHeight() / 2);  //Angle to rotate
        invalidate();
    }

    // Distance between two touching points
    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);
    }

    // Take the center point of the gesture
    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);
    }

    // Take rotation angle
    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);
    }

    // Save the moved, scaled, and rotated layers as new pictures
    // This method is not used in this example. If you need to save pictures, you can refer to it
    public Bitmap createCropBitmap() {
        // Create rotation scale Swatch
        Bitmap bitmap = Bitmap.createBitmap(widthScreen, heightScreen,
                Bitmap.Config.ARGB_8888); // Background picture
        Canvas canvas = new Canvas(bitmap); // New canvas
        canvas.drawBitmap(drawBitmap, matrix, null); // Draw pictures
        canvas.save(); // Save canvas
        canvas.restore();
        // Return cut sample
        return Bitmap.createBitmap(bitmap, cropRect.left, cropRect.bottom - (cropRect.bottom - cropRect.top)
                , cropRect.right - cropRect.left, cropRect.bottom - cropRect.top);
    }

    /**
     * Stretch according to the given width and height
     *
     * @param origin Original drawing
     * @param scale  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);// Use post multiplication
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        return newBM;
    }

    public int getRectHeight() {
        return rectHeight;
    }

    //Width and height of rectangle
    public void setRectHeight(int rectHeight) {
        this.rectHeight = rectHeight;
        invalidate();
    }

    //Height from top
    public void setMarginTop(int marginTop) {
        this.marginTop = marginTop;
        invalidate();
    }

    public int getMarginTop() {
        return marginTop;
    }


    /**
     * Convert dp units to px
     *
     * @param context context {@link Context}
     * @param value   value
     * @return Value after conversion
     */
    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);
    }
}

There are also some image compression functions on the clipping page, which are also made into a tool class

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

    /**
     * Get the picture according to the path and compress it, and return to bitmap (ticket secret compression scheme)
     *
     * @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;
    }


    /**
     * The algorithm here aims to compress the short edge to 1000 ~ 2000. By calculating the ratio to 1000, it is necessary to control the sampling rate to a multiple of 2
     * Therefore, you need to use the method {@ link #calInSampleSize(int)} for calculation
     * (Ticket Secretary (compression scheme)
     *
     * @return sampling rate
     */
    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);
    }

    /**
     * The sampling rate is calculated through the shift operation. It is the result that the highest bit of the binary number corresponding to an integer is 1 and other positions are 0
     * (Ticket Secretary (compression scheme)
     *
     * @param rate proportion
     * @return sampling rate
     */
    private static int calInSampleSize(int rate) {
        int i = 0;
        while ((rate >> (++i)) != 0) ;
        return 1 << --i;
    }

}

Of course, if you want to save pictures as in the demo, remember to apply for storage permission

Some anomalies

When running the demo, an API problem of the system was found

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

The bitmap returned by this decodeFile is null
After consulting relevant materials, the solutions are as follows:
Add the following contents to the Application tab of the configuration list:

android:requestLegacyExternalStorage="true"

Source code broadcast

Keywords: Android

Added by blackthunder on Fri, 17 Dec 2021 08:47:02 +0200