Android development skills-custom image-like cropping controls
Taking a photo-cropping, or selecting an image-cropping is a group of operations that are often required when we set an Avatar or upload an image. In the previous article, I talked about the use of Camera. In this article, I will talk about how to crop images.
The following requirements come from products. To crop an image like that, drag and zoom in the image without moving the cropping frame. The content outside the cropping frame must have a translucent Black Mask. A line of prompt text will be displayed under the cropping box (I still have some reserved opinions ).
In Android, there are still a lot of control libraries for image cropping, especially the popular ones on github, which have evolved to a relatively stable stage, but unfortunately, their cropping process is to drag or scale the cropping box, so I had to find them myself to see if there are ready-made or semi-finished products of the wheel, you don't have to start from scratch.
Great God's implementation process
First, let's take a look at the implementation process of the preceding high-imitation cropping control. It is not difficult to say, mainly the following points:
1. RewriteImageView
And listen to gesture events, including two-point, two-point scaling and dragging, to make it a control for scaling and dragging the image.
2. DefineMatrix
Member variable, which maintains matrix data such as scaling and translation of the image.
3. When dragging or scaling, the area of the intersection between the image and the cropping box must be equal to that of the cropping box. That is, the image cannot be dragged from the cropping box.
3. When setting an image, first initialize the scaling and translation operations based on the image size to make the third condition piece as small as possible.
4. Each time a gesture event is received, the corresponding matrix is calculated and the calculation result is passedImageView
OfsetImageMatrix
The method is applied to the image.
5. the cropping box is a separate controlImageView
It is also large and displayed on top of it.
6. UseXXXLayout
Encapsulate the cropping box and scaling.
7. Create an empty Bitmap and use it to createCanvas
, Draw the scaled and translated image to this Bitmap, and createBitmap
(By callingBitmap.createBitmap
Method ).
My custom content
The code I got was changed after the version of the great god of Hong Yang, and the code was a bit messy (although it was implemented by the function ). In the original functions, I want to make the following changes:
Merge the content of the cropping box to the ImageView. The cropping box can be any aspect ratio of the left and right margins of the rectangular cropping box. You can set the mask layer color. You can set prompt text under the cropping box (your own product requirements ).) the next product adds the maximum size attribute definition of a cropped image.
In the above functional requirements, I have defined the following attributes:
<code class="language-xml hljs "><declare-styleable name="ClipImageView"> <attr name="civHeight" format="integer"> <attr name="civWidth" format="integer"> <attr name="civTipText" format="string"> <attr name="civTipTextSize" format="dimension"> <attr name="civMaskColor" format="color"> <attr name="civClipPadding" format="dimension"></attr></attr></attr></attr></attr></attr></declare-styleable></code>
Where:
civHeight
And
civWidth
Is the width and height ratio of the cropping frame.
civTipText
Prompt text content
civTipTextSize
Size of prompt text
civMaskColor
Color value of the Mask Layer
civClipPadding
Crop the padding. Since the cropping box is inside the control, I finally chose to use padding to describe the distance between the cropping box and the edge of our control. Member variables
I made some changes to the member variables, removed the horizontal margin variables originally used to define the cropping box and other useless variables, and added some of my member variables, the final result is as follows:
Private final int mMaskColor; // mask layer color private final Paint mPaint; // brush private final int mWidth; // crop the size of the frame width (integer value read from the attribute) private final int mHeight; // the size of the height of the cropping box (same as above) private final String mTipText; // The prompt text private final int mClipPadding; // The cropping box is private float mScaleMax = 4.0f relative to the padding of the control; // The maximum size of the image is private float mScaleMin = 2.0f; // minimum image scale/*** initial scale */private float mInitScale = 1.0f; /*** used to store the matrix */private final float [] mMatrixValues = new float [9];/*** zoom gesture check */private ScaleGestureDetector mScaleGestureDetector = null; private final Matrix mScaleMatrix = new Matrix ();/*** double-click */private GestureDetector mGestureDetector; private boolean isAutoScale; private float mLastX; private float mLastY; private boolean isCanDrag; private int lastPointerCount; private Rect mClipBorder = new Rect (); // cropping box private int mMaxOutputWidth = 0; // maximum output width of the cropped Image
Constructor
The constructor mainly involves reading some custom attributes:
public ClipImageView(Context context) { this(context, null); } public ClipImageView(Context context, AttributeSet attrs) { super(context, attrs); setScaleType(ScaleType.MATRIX); mGestureDetector = new GestureDetector(context, new SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { if (isAutoScale) return true; float x = e.getX(); float y = e.getY(); if (getScale() < mScaleMin) { ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16); } else { ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16); } isAutoScale = true; return true; } }); mScaleGestureDetector = new ScaleGestureDetector(context, this); this.setOnTouchListener(this); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.WHITE); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView); mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1); mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1); mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0); mTipText = ta.getString(R.styleable.ClipImageView_civTipText); mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000); final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24); mPaint.setTextSize(textSize); ta.recycle(); mPaint.setDither(true); }
Defines the position of the cropping frame.
The cropping box is in the middle of the control. First, we read the ratio of width to height and the left and right margins from the attribute. However, in the constructor, since the control has not been drawn, the width and height of the widget cannot be obtained, so the size and position of the cropping box cannot be calculated. Therefore, I have rewritten the onLayout method and calculated the position of the cropping box here:
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); final int width = getWidth(); final int height = getHeight(); mClipBorder.left = mClipPadding; mClipBorder.right = width - mClipPadding; final int borderHeight = mClipBorder.width() * mHeight / mWidth; mClipBorder.top = (height - borderHeight) / 2; mClipBorder.bottom = mClipBorder.top + borderHeight; }
Crop a box
The code for drawing prompt text is also provided here, all in the same method. Easy to rewriteonDraw
Method. There are two ways to draw a cropping box. One is to draw a mask layer with full screen, and then extract a rectangle from the middle. However, when I use it, I find that a rectangle cannot be drawn, so I use the following one:
First draw the upper and lower rectangles, and then draw the Left and Right rectangles. The undrawn section enclosed in the middle is our cropping box.
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int width = getWidth(); final int height = getHeight(); mPaint.setColor(mMaskColor); mPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0, 0, width, mClipBorder.top, mPaint); canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint); canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint); canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint); mPaint.setColor(Color.WHITE); mPaint.setStrokeWidth(1); mPaint.setStyle(Paint.Style.STROKE); canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint); if (mTipText != null) { final float textWidth = mPaint.measureText(mTipText); final float startX = (width - textWidth) / 2; final Paint.FontMetrics fm = mPaint.getFontMetrics(); final float startY = mClipBorder.bottom + mClipBorder.top / 2 - (fm.descent - fm.ascent) / 2; mPaint.setStyle(Paint.Style.FILL); canvas.drawText(mTipText, startX, startY, mPaint); } }
Modify the initial display of an image
Here, I do not use the global layout listener (throughgetViewTreeObserver
Add callback), but directly rewrite several methods to set the image, and set the initial display after setting the image:
@Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); postResetImageMatrix(); } @Override public void setImageResource(int resId) { super.setImageResource(resId); postResetImageMatrix(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); postResetImageMatrix(); } private void postResetImageMatrix() { post(new Runnable() { @Override public void run() { resetImageMatrix(); } }); }
resetImageMatrix()
The method is to set the initial scaling and translation of the image. The calculation is based on the image size, the size of the control, and the size of the cropping box:
/*** Edge moment of the vertical direction and View */public void resetImageMatrix () {final Drawable d = getDrawable (); if (d = null) {return ;} final int dWidth = d. getIntrinsicWidth (); final int dHeight = d. getIntrinsicHeight (); final int cWidth = mClipBorder. width (); final int cHeight = mClipBorder. height (); final int vWidth = getWidth (); final int vHeight = getHeight (); final float scale; final float dx; final float dy; if (dWidth * cHeight> cWidth * dHeight) {scale = cHeight/(float) dHeight;} else {scale = cWidth/(float) dWidth ;} dx = (vWidth-dWidth * scale) * 0.5f; dy = (vHeight-dHeight * scale) * 0.5f; mScaleMatrix. setScale (scale, scale); mScaleMatrix. postTranslate (int) (dx + 0.5f), (int) (dy + 0.5f); setImageMatrix (mScaleMatrix); mInitScale = scale; mScaleMin = mInitScale * 2; mScaleMax = mInitScale * 4 ;}
Note:: There is a pitfall. SetBitmap
SetImageView
IsImageView
ObtainedDrawable
Object and its width and height, insteadBitmap
Object.Drawable
The object may beBitmap
To zoom in or out the display, resulting in its width or height andBitmap
The width and height are different.
A little more attention: To obtain the width and height of a widget, you must obtain the width and height of the widget after it is drawn.post
OneRunnable
Object To the main threadLooper
To ensure that it is called after the interface is drawn.
Scaling and dragging
When scaling or dragging, determine whether the boundary is exceeded. If the boundary is exceeded, take the allowed final value. I am not sure about the code here. Please refer to the source code later.
Crop
Here is another transformation focus.
First, the great god of Hong Yang creates an emptyBitmap
And createCanvas
Object, and thendraw
Method to draw the scaled image to this Bitmap and then callBitmap.createBitmap
Get the content of the cropping box. But we have already rewrittenonDraw
Method to draw a crop box, so we will not consider it here.
In addition, this method has another problem: it draws a Drawable object. If we set a large Bitmap, it may be scaled. Here we crop the scaled Bitmap, which is not used to crop the source image.
Here I refer to other cropping image libraries and saveMatrix
Calculate the member variables, obtain the corresponding range of the cropping box, and obtain the final image according to the final requirement (we need to limit the maximum size of the product). The Code is as follows:
public Bitmap clip() { final Drawable drawable = getDrawable(); final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap(); final float[] matrixValues = new float[9]; mScaleMatrix.getValues(matrixValues); final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth(); final float transX = matrixValues[Matrix.MTRANS_X]; final float transY = matrixValues[Matrix.MTRANS_Y]; final float cropX = (-transX + mClipBorder.left) / scale; final float cropY = (-transY + mClipBorder.top) / scale; final float cropWidth = mClipBorder.width() / scale; final float cropHeight = mClipBorder.height() / scale; Matrix outputMatrix = null; if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) { final float outputScale = mMaxOutputWidth / cropWidth; outputMatrix = new Matrix(); outputMatrix.setScale(outputScale, outputScale); } return Bitmap.createBitmap(originalBitmap, (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight, outputMatrix, false); }
Because we areBitmap
First obtainBitmap
:
final Drawable drawable = getDrawable(); final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
Then, we can use a matrix value that contains nine elementsfloat
Array reading:
final float[] matrixValues = new float[9]; mScaleMatrix.getValues(matrixValues);
For example, to read the zoom value on X, the code ismatrixValues[Matrix.MSCALE_X]
.
Pay special attention to the fact that, as mentioned above, the scale here isDrawable
Object, butBitmap
If the image is too largeDrawable
So the zoom size is calculated as follows:
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
Then obtain the image translation volume:
final float transX = matrixValues[Matrix.MTRANS_X]; final float transY = matrixValues[Matrix.MTRANS_Y];
Calculate the starting point and width of the crop frame on the image:
final float cropX = (-transX + mClipBorder.left) / scale; final float cropY = (-transY + mClipBorder.top) / scale; final float cropWidth = mClipBorder.width() / scale; final float cropHeight = mClipBorder.height() / scale;
The above is the final result we want to crop.
However, as I mentioned earlier, the maximum output size should be limited according to product requirements. Since the aspect ratio of the cropped image is, I only use the width (you can also use the height) Here, so the following code is added, when the cropped width exceeds our maximum width, scale the image.
Matrix outputMatrix = null; if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) { final float outputScale = mMaxOutputWidth / cropWidth; outputMatrix = new Matrix(); outputMatrix.setScale(outputScale, outputScale); }
Finally, the cropped Bitmap is created based on the value calculated above:
Bitmap.createBitmap(originalBitmap, (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight, outputMatrix, false);
In this way, the image cropping control is complete.
Effect
For the subsequent code, see https://github.com/msdx/clip-imageand demo. I added an interface to the control.
getClipMatrixValues
To obtain the matrix value of the image during cropping. It can be used to crop a large image. I will write another article about cropping a large image later. The code for cropping a large image is also in the demo above. You can set the aspect ratio of the cropping frame to determine whether the cropping frame is square or a cropping frame with other proportions.