Android development skills-custom fake WeChat image cropping controls

Source: Internet
Author: User

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. RewriteImageViewAnd listen to gesture events, including two-point, two-point scaling and dragging, to make it a control for scaling and dragging the image.
2. DefineMatrixMember 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 passedImageViewOfsetImageMatrixThe method is applied to the image.
5. the cropping box is a separate controlImageViewIt is also large and displayed on top of it.
6. UseXXXLayoutEncapsulate 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.createBitmapMethod ).

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:

civHeightAnd civWidthIs the width and height ratio of the cropping frame. civTipTextPrompt text content civTipTextSizeSize of prompt text civMaskColorColor value of the Mask Layer civClipPaddingCrop 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 rewriteonDrawMethod. 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 (throughgetViewTreeObserverAdd 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. SetBitmapSetImageViewIsImageViewObtainedDrawableObject and its width and height, insteadBitmapObject.DrawableThe object may beBitmapTo zoom in or out the display, resulting in its width or height andBitmapThe 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.postOneRunnableObject To the main threadLooperTo 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 emptyBitmapAnd createCanvasObject, and thendrawMethod to draw the scaled image to this Bitmap and then callBitmap.createBitmapGet the content of the cropping box. But we have already rewrittenonDrawMethod 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 saveMatrixCalculate 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 areBitmapFirst obtainBitmap:

        final Drawable drawable = getDrawable();        final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

Then, we can use a matrix value that contains nine elementsfloatArray 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 isDrawableObject, butBitmapIf the image is too largeDrawableSo 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. getClipMatrixValuesTo 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.

 

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.