Make writing a habit together! This is the fifth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Before doing a custom View, the effect is somewhat similar to the poster factory, as a custom View to learn ~ first look at the effect picture:

Is a background image, dug in the middle of a number of different shapes of the “hole”, each “hole” put an image, you can drag, scale, rotate the image, and the current image when ready to operate with a red highlighted border. When you select an image, a menu bar pops up at the bottom, with three buttons: rotate the image 90 degrees, flip the image symmetrically, and save the entire poster to the phone’s built-in SD card root directory.

This is similar to the poster factory effect. Select several pictures and the bottom template (the position and shape of the background picture and the hollowed out part), and then change the size, position and Angle of the selected picture by touching to make a poster you like.

This is basically a custom View, done in the project called JigsawView. Its basic structure is to draw the operable picture at the bottom layer, the background picture at the second layer, and the hollow part at the third layer. The hollow part is realized by PorterDuffXfermode, and the shape of the hollow part is determined by the SVG file of the corresponding mobile phone directory.

When drawing with Canvas in Android, we can use PorterDuffXfermode to mix the pixel of the drawing graph with the pixel of the corresponding position in the Canvas according to certain rules to form a new pixel value, so as to update the final pixel color value in the Canvas. This creates a lot of interesting effects. For details about PorterDuffXfermode, please refer to Android Canvas drawing PorterDuffXfermode and its working principle

The first thing to do here is turn off hardware acceleration, because hardware acceleration can cause the effect to be lost. Called in the initialization statement of the View

setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Copy the code

Can.

Because JigsawView has a lot of code, only the most important parts are shown here. See the GitHub link at the end of this article for the full code.

First you need two brushes:

Paint mMaimPaint = new Paint(paint.anti_alias_flag); MSelectPaint = new Paint(paint.anti_alias_flag);Copy the code

The model shown here is the PictureModel. PictureModel is mainly a HollowModel that contains location and scaling information as well as hollow parts. The specific position and size of images are determined by the HollowModel. The hollow parts are filled with CenterCrop similar to ImageView. This is handled in the makePicFillHollow method of JigsawView.

HollowModel holds a collection of Path objects derived from parsing an SVG file, which can represent the path represented by an SVG file. The parsing is handled by the custom SvgParseUtil class, which reads the corresponding SVG file from the phone’s built-in SD card (the Path can be flexibly configured, of course) and parses it into a drawable Path collection object. SvgParseUtil essentially parses an XML file (thinking SVG is an XML file) and copies the SVG path directly from the system’s PathParser, leaving the other round rectangles and polygons to handle themselves. Here the specific code is not shown here, please see the source code on GitHub in detail.

Here is the complete onDraw method:

@Override protected void onDraw(Canvas canvas) { if (mPictureModels ! = null && mPictureModels.size() > 0 && mBitmapBackGround ! = null) {// loop over the image to be processed for (PictureModel PictureModel: mPictureModels) { Bitmap bitmapPicture = pictureModel.getBitmapPicture(); int pictureX = pictureModel.getPictureX(); int pictureY = pictureModel.getPictureY(); float scaleX = pictureModel.getScaleX(); float scaleY = pictureModel.getScaleY(); float rotateDelta = pictureModel.getRotate(); HollowModel hollowModel = pictureModel.getHollowModel(); ArrayList<Path> paths = hollowModel.getPathList(); if (paths ! = null && paths.size() > 0) { for (Path tempPath : paths) { mPath.addPath(tempPath); } drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, mPath); } else { drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, null); }} // Create a new layer and place the new layer on top of the canvas default layer. After we execute Canvas.savelayer (), all our drawing operations will be drawn on our new layer instead of the canvas default layer. int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG); drawBackGround(canvas); PictureModel: PictureModel: mPictureModels) { int hollowX = pictureModel.getHollowModel().getHollowX(); int hollowY = pictureModel.getHollowModel().getHollowY(); int hollowWidth = pictureModel.getHollowModel().getWidth(); int hollowHeight = pictureModel.getHollowModel().getHeight(); ArrayList<Path> paths = pictureModel.getHollowModel().getPathList(); if (paths ! = null && paths.size() > 0) { for (Path tempPath : paths) { mPath.addPath(tempPath); } drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, mPath); mPath.reset(); } else { drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, null); }} // Draw this layer to canvas default layer canvas.restoreToCount(layerId); // Draw select image highlight border for (PictureModel PictureModel: mPictureModels) { if (pictureModel.isSelect() && mIsNeedHighlight) { canvas.drawRect(getSelectRect(pictureModel), mSelectPaint); }}}}Copy the code

The train of thought is still quite clear. Lines 3 through 22 draw an operable image. DrawPicture in line 19 is to draw all the operable pictures. When the hollow part corresponding to the picture does not have corresponding SVG, draw the rectangle corresponding to the position and size of the HollowModel as the hollow part, i.e. DrawPicture in line 20.

Take a look at the drawPicture method:

private void drawPicture(Canvas canvas, Bitmap bitmapPicture, int coordinateX, int coordinateY, float scaleX, float scaleY, float rotateDelta , HollowModel hollowModel, Path path) { int picCenterWidth = bitmapPicture.getWidth() / 2; int picCenterHeight = bitmapPicture.getHeight() / 2; mMatrix.postTranslate(coordinateX, coordinateY); mMatrix.postScale(scaleX, scaleY, coordinateX + picCenterWidth, coordinateY + picCenterHeight); mMatrix.postRotate(rotateDelta, coordinateX + picCenterWidth, coordinateY + picCenterHeight); canvas.save(); // The following is the corresponding hollow part intersection processing, need to improve if (path! = null) { Matrix matrix1 = new Matrix(); RectF rect = new RectF(); path.computeBounds(rect, true); int width = (int) rect.width(); int height = (int) rect.height(); float hollowScaleX = hollowModel.getWidth() / (float) width; float hollowScaleY = hollowModel.getHeight() / (float) height; matrix1.postScale(hollowScaleX, hollowScaleY); path.transform(matrix1); // Shift path path.offset(HollowModel.gethollowx (), hollowModel.gethollowy ()); // Make the image only draw inside the hollow to prevent sliding into another jigsaw area canvas.clippath (path); path.reset(); } else { int hollowX = hollowModel.getHollowX(); int hollowY = hollowModel.getHollowY(); int hollowWidth = hollowModel.getWidth(); int hollowHeight = hollowModel.getHeight(); ClipRect (hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight); } canvas.drawBitmap(bitmapPicture, mMatrix, null); canvas.restore(); mMatrix.reset(); }Copy the code

Here, Matrix is mainly used to process various changes of pictures. In the onTouchEvent method, the position, scale and Angle of the PictureModel object being operated will be assigned according to the different touch events. Therefore, the assignment parameters after each touch will be taken out in the drawPicture and handed to the Matrix object for processing, and finally passed

canvas.drawBitmap(bitmapPicture, mMatrix, null);
Copy the code

I can display the picture of the change after touch. Also on line 26, Canvas.clippath (path); Is to limit the drawable area of the image to the hollowed out area to prevent the image from sliding into other hollowed out areas.

Notice line 25 of onDraw

int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
Copy the code

In order to properly display the PorterDuffXfermode effect, you need to create a new layer, as referenced in the blog post above.

The drawBackGround method on line 26 of onDraw is simply to draw the background.

Lines 28 to 44 draw the hollow part, mainly by taking out the Path set stored in the HollowModel, and then passing the Path data to the mPath object through addPath method, and then drawing the hollow part by drawHollow method.

private void drawHollow(Canvas canvas, int hollowX, int hollowY, int hollowWidth, int hollowHeight, Path path) { mMaimPaint.setXfermode(mPorterDuffXfermodeClear); // If (path! = null) { canvas.save(); canvas.translate(hollowX, hollowY); // Scale the hollow part so that the hollow part fills the HollowModel's corresponding rectangular region scalePathRegion(Canvas, hollowWidth, hollowHeight, Path); canvas.drawPath(path, mMaimPaint); canvas.restore(); mMaimPaint.setXfermode(null); } else { Rect rect = new Rect(hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight); canvas.save(); canvas.drawRect(rect, mMaimPaint); canvas.restore(); mMaimPaint.setXfermode(null); }}Copy the code

Here’s the first step to setting the brush’s PorterDuffXfermode:

mMaimPaint.setXfermode(mPorterDuffXfermodeClear);
Copy the code

Here for hollow out effect, PorterDuffXfermode use porterduff.mode. CLEAR.

The canvas is then translated, and the Path object representing the hollowout Path is scaled using the scalePathRegion method, so that the hollowout Path fills the rectangular region corresponding to the HollowModel. Then use the

canvas.drawRect(rect, mMaimPaint);
Copy the code

Draw in the hollow path.

And finally don’t forget

canvas.restore();
mMaimPaint.setXfermode(null);
Copy the code

Restores the state of the canvas and brush.

Then line 47 of onDraw draws this layer onto the canvas default layer:

 canvas.restoreToCount(layerId);
Copy the code

Ontouch last

// Draw select image highlight border for (PictureModel PictureModel: mPictureModels) { if (pictureModel.isSelect() && mIsNeedHighlight) { canvas.drawRect(getSelectRect(pictureModel), mSelectPaint); }}Copy the code

In onTouchEvent, the touch event determines which image is currently selected, and then in onDraw the currently selected image draws the border of the corresponding HollowModel.

That’s the end of onDraw.

Look again at the onTouchEvent method:

@Override public boolean onTouchEvent(MotionEvent event) { if (mPictureModels == null || mPictureModels.size() == 0) { return true; } switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: If (event.getPoInterCount () == 2) {//mPicModelTouch = getHandlePicModel(event); //mPicModelTouch = getHandlePicModel(event); if (mPicModelTouch ! = null) { // mPicModelTouch.setSelect(true); // resetNoTouchPicsState(); mPicModelTouch.setSelect(true); MLastFingerDistance = distanceBetweenFingers(event); MLastDegree = rotation(event); mIsDoubleFinger = true; invalidate(); } } break; ACTION_DOWN: // Record the location of the last event mLastX = event.getx (); mLastY = event.getY(); MDownX = event.getx (); mDownY = event.getY(); MPicModelTouch = getHandlePicModel(event); if (mPicModelTouch ! = null) {resetNoTouchPicsState(); mPicModelTouch.setSelect(true); invalidate(); } break; ACTION_MOVE: switch (event.getPointerCount()) {case 1: if (! mIsDoubleFinger) { if (mPicModelTouch ! Dx = (int) (event.getx () -mlastx); int dy = (int) (event.getY() - mLastY); int tempX = mPicModelTouch.getPictureX() + dx; int tempY = mPicModelTouch.getPictureY() + dy; if (checkPictureLocation(mPicModelTouch, tempX, TempY)) {/ / check to the hollow out some truly assignment to mPicModelTouch mPicModelTouch. SetPictureX (tempX); mPicModelTouch.setPictureY(tempY); MLastX = event.getx (); // Save the previous position for the next event to calculate the relative displacement. mLastY = event.getY(); // Update View invalidate() after modifying the position of mPicModelTouch; } } } break; Case 2: if (mPicModelTouch! Double fingerDistance = distanceBetweenFingers(event); Double currentDegree = rotation(event); Float scaleRatioDelta = (float) (fingerDistance/mLastFingerDistance); float scaleRatioDelta = (float) (fingerDistance/mLastFingerDistance); float rotateDelta = (float) (currentDegree - mLastDegree); float tempScaleX = scaleRatioDelta * mPicModelTouch.getScaleX(); float tempScaleY = scaleRatioDelta * mPicModelTouch.getScaleY(); If (Math. Abs (tempScaleX) < 3 && Math. Abs (tempScaleX) > 0.3 && Math. Abs (tempScaleY) < 3 && Math. > 0.3) {mPicModelTouch. SetScaleX (tempScaleX); mPicModelTouch.setScaleY(tempScaleY); mPicModelTouch.setRotate(mPicModelTouch.getRotate() + rotateDelta); // Refresh View invalidate() after modifying the model; MLastFingerDistance = fingerDistance; mLastFingerDistance = fingerDistance; }} mLastDegree = currentDegree; } break; } break; ACTION_UP: // for (PictureModel PictureModel: mPictureModels) { // pictureModel.setSelect(false); // } mIsDoubleFinger = false; double distance = getDisBetweenPoints(event); if (mPicModelTouch ! = null) {// Whether to slide, Non-slip, change the selected the if (short < ViewConfiguration. GetTouchSlop ()) {if (mPicModelTouch. IsLastSelect ()) { mPicModelTouch.setSelect(false); mPicModelTouch.setLastSelect(false); if (mPictureCancelSelectListner ! = null) { mPictureCancelSelectListner.onPictureCancelSelect(); } } else { mPicModelTouch.setSelect(true); mPicModelTouch.setLastSelect(true); // Selected callback if (mPictureSelectListener! = null) { mPictureSelectListener.onPictureSelect(mPicModelTouch); } } invalidate(); } else {// Slide to cancel all selected states mpicModelTouch.setSelect (false); mPicModelTouch.setLastSelect(false); // Refresh View invalidate() after canceling the status; }} else {// If no image is selected, uncheck all image status for (PictureModel PictureModel: mPictureModels) { pictureModel.setLastSelect(false); } // No jigsaw is selected by the callback if (mPictureNoSelectListener! = null) { mPictureNoSelectListener.onPictureNoSelect(); } // Update View invalidate(); } break; Case MotionEvent.ACTION_POINTER_UP: if (mPicModelTouch! = null) { mPicModelTouch.setSelect(false); invalidate(); } } return true; }Copy the code

Although it is long, it is not difficult to understand. It is basically a routine thing, which can be understood by reading the notes.

The overall process is as follows: First in the Down event: whether in one-handed or two-handed mode, the currently clicked image model will be selected so that the selected image model can be modified in future events to change the image display in onDraw.

In a Move event: in one-handed mode, the PictureModel position is assigned for each Move event, and invalidate is called to refresh the interface.

In two-handed mode, the Angle and scaling ratio of the PictureModel are assigned based on the Angle change caused by each MOVE event and the distance change between the two fingers, and then invalidate is called to refresh the interface.

Up event: single mode, first determine whether has been sliding (sliding distance less than ViewConfiguration. GetTouchSlop () is considered not sliding but click), not sliding is to change the picture of the current selected processing, switch state. If it is, uncheck all images.

Uncheck all images in two-finger state.

In order to make the zoom and rotation experience better, so as long as the finger DOWN event falls in the hollow part, in the case of no Up event, even if you slide out of the hollow part, you can still continue to operate the selected picture, to avoid the operation inconvenience caused by the small hollow part, which is consistent with the effect of the poster factory.

Source code address: github.com/yanyinan/Ji…

Original is not easy, if you feel that this article is helpful to yourself, don’t forget to click on the likes and attention, but also to the author’s affirmation ~