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"