Recently, I was doing something with my friend @anotheren. I was going to make the picture picker of wechat. So we have the AnyImageKit framework, and now we’re done selecting and editing images. When doing the picture editing function, I did the cutting function for a long time. I thought of a way to do it, but when I found it was not good halfway through, I overturned and redid it. After going through this process for two or three times, I finally made it. There are a lot of holes in this function, and there is not much information about this piece on the Internet, so I want to write an article to record it.

We’re going to start with three small problems.

Problem 1: How to display the picture completely

First consider the horizontal image (the second image), set the image width to scrollView.bounds.width, and scale the image’s height equally.

width = scrollView.bounds.width
height = image.width * scrollView.bounds.height / scrollView.bounds.width
Copy the code

Next consider the case of the vertical graph (the first graph) and make a judgment based on the previous step.

// If the image is larger than scrollView.bounds.height is a vertical image, scale the image to scrollView.bounds.height and calculate the width according to the scale.
if height > scrollView.bounds.height {
    height = scrollView.bounds.height
    width = image.height * scrollView.bounds.width / scrollView.bounds.height
}
Copy the code

Finally, calculate the imageView.frame based on the size and the problem is solved.

Note: The gray part is scrollView

Question 2: How to show the part of the image beyond the scrollView after zooming

It’s natural to look at this problem and think, well, maybe the scrollView is full screen, so you can display it all. However, a full-screen scrollView has some problems that can’t be solved. The third problem will be covered below, and we will not consider this solution for now.

Scrollview.clipstobounds = false.

Question 3: How to make the picture pull without scaling

ContentSize < scrollView.bounds.size scrollView.contentInSet (); contentInset ();

In daily development, the contentInset API is rarely used. For those of you who are unfamiliar with this property, please note that. ContentInset is UIEdgeInsets that add an extra scroll area to the scrollView. An example of this is the MJRefresh drop-down refresh, which I’m sure you’ve all used before. As you refresh, you’ll notice that there’s a scrollable area at the top of the scrollView, which is implemented using the contentInset API.

Now that we know about contentInset, we need to correct the condition that scrollView is scrollable:

scrollView.contentSize + scrollView.contentInset > scrollView.bounds.size
Copy the code

Now we set the value of contentInset to 0.1.

scrollView.contentInset = UIEdgeInsets(top: 0.1.left: 0.1, bottom: 0.1.right: 0.1)
Copy the code

The width of the image is the same as the width of the scrollView, but the height is not, so we need to calculate the height:

let bottomInset = scrollView.bounds.height - cropRect.height + 0.1
Copy the code

To deal with the width problem for vertical diagrams, let’s put the code together:

let rightInset = scrollView.bounds.width - cropRect.width + 0.1
let bottomInset = scrollView.bounds.height - cropRect.height + 0.1
scrollView.contentInset = UIEdgeInsets(top: 0.1.left: 0.1, bottom: bottomInset, right: rightInset)
Copy the code

If we use a full-screen scrollView in the second problem, it is not easy to solve the third problem

tailoring

The four corners of the clipping box are drawn with UIView. Their level is the same as that of scrollView. Their positions can be described by a CGRect variable called cropRect.

The clipping core is how to move the picture to the correct position when the clipping box is moved, as shown in the following example.

According to the effects shown in the GIF, it can be concluded that:

  1. scrollViewThe scaling of the
  2. scrollViewThe offset of is changed
  3. The clipping box position has been moved

Let’s take a step-by-step look at how to solve these problems.

ZoomScale

In the driven figure, we can see that the scrollView will be scaled after the clipping box is moved, and there are two cases, one is horizontal and the other is vertical, so we need to calculate the scaling ratio of the two cases, and then choose one of them.

Let’s assume that the size of the image is ABCD, and we move point D to point G, that is, the clipping box is AEFG. When the user lets go, AEFG should be enlarged to the position of ABCD, from which we can get the scaling ratio: AB/AE = 375/187.5 = 2.0

But we’re not done yet. Imagine that when AEFG is scaled up to ABCD, point D is moved to point G again. This operation is equivalent to moving the image from G to J before scaling.

AB = scrollView.bounds.width AB = scrollView.bounds.width

  1. AEFGamplification2.0Times toABCD
  2. From the point ofDpoint-to-pointG, i.e.,CropRect. Width = 187.5
  3. AH = cropRect. Width/scrollView. ZoomScale = 187.5/2.0 = 93.75

Now we have the scaling formula for the horizontal image, and the same for the vertical image, as follows:

let zoomH = scrollView.bounds.width / (cropRect.width / scrollView.zoomScale)
let zoomV = scrollView.bounds.height / (cropRect.height / scrollView.zoomScale)
Copy the code

Next we need to analyze whether we should use a horizontal or vertical scale. Width is scaled to scrollView.bounds.width. According to the scaling ratio, croprect. height can be calculated. If croprect. height > scrollView.bounds.height means that the height is too high, we use the vertical scaling formula, and vice versa, the horizontal scaling formula, which looks like this:

let maxZoom = scrollView.maximumZoomScale
let zoomH = scrollView.bounds.width / (cropRect.width / scrollView.zoomScale)
let zoomV = scrollView.bounds.height / (cropRect.height / scrollView.zoomScale)
let isVertical = cropRect.height * (scrollView.bounds.width / cropRect.width) > scrollView.bounds.height
let zoom: CGFloat
if! isVertical { zoom = zoomH > maxZoom ? maxZoom : zoomH }else {
    zoom = zoomV > maxZoom ? maxZoom : zoomV
}
Copy the code

ContentOffset

Now let’s calculate contentOffset. Set the image as ABCD, move point A to point E, and enlarge EFGD by 2.0 to ABCD. It can be concluded that:

Note: ₁ indicates before scaling, ₂ indicates a scaling; CropStartPanRect gesture is cut out of the box before the start position (x) = CG ₂ = E CG ₁ * zoom = (cropRect. Origin. X-ray cropStartPanRect. Origin. X) x zoomCopy the code

The above formula is not the final one. Next, based on the current scaling ratio, move point A to point E again. This operation is equivalent to moving the picture from point E to point H before scaling.

Note: ₁ indicates before scaling, ₂ indicates after scaling once, or ₃ indicates after scaling twiceletZoomScale H(x) = CJ₃ = CG₃ + GJ₃ = CG₂ * ZOOM + GJ₂ * zoomScale = scrollView.contentOffset.x * zoomScale + (cropRect.origin.x - cropStartPanRect.origin.x) * zoomScaleCopy the code

Finally we calculate the final contentOffset based on the Angle of movement

let zoomScale = zoom / scrollView.zoomScale
let offsetX = (scrollView.contentOffset.x * zoomScale) + ((cropRect.origin.x - cropStartPanRect.origin.x) * zoomScale)
let offsetY = (scrollView.contentOffset.y * zoomScale) + ((cropRect.origin.y - cropStartPanRect.origin.y) * zoomScale)
let offset: CGPoint
switch position {  // An enumeration that marks the position of the Angle
case .topLeft:     // Move the upper left corner, where contentOffset X and y are changed
    offset = CGPoint(x: offsetX, y: offsetY)
case .topRight:    // Move the upper right corner, contentOffset y to change
    offset = CGPoint(x: scrollView.contentOffset.x * zoomScale, y: offsetY)
case .bottomLeft:  // Move the lower left corner, contentOffset X to change
    offset = CGPoint(x: offsetX, y: scrollView.contentOffset.y * zoomScale)
case .bottomRight: // Move the lower right corner, contentOffset is unchanged
    offset = CGPoint(x: scrollView.contentOffset.x * zoomScale, y: scrollView.contentOffset.y * zoomScale)
}
Copy the code

NewCropRect

Finally, after dragging the clipping frame to let go, we need to enlarge and center the clipping frame. This logic is the same as that used in calculating the scale of the picture in the first question, so I will not repeat it.

let newCropRect: CGRect
if(zoom == maxZoom && ! isVertical) || zoom == zoomH {let scale = scrollView.bounds.width / cropRect.width
    let height = cropRect.height * scale
    let y = (scrollView.bounds.height - height) / 2 + scrollView.frame.origin.y
    newCropRect = CGRect(x: scrollView.frame.origin.x, y: y, width: scrollView.bounds.width, height: height)
} else {
    let scale = scrollView.bounds.height / cropRect.height
    let width = cropRect.width * scale
    let x = (scrollView.bounds.width - width + scrollView.frame.origin.x) / 2
    newCropRect = CGRect(x: x, y: scrollView.frame.origin.y, width: width, height: scrollView.frame.height)
}
Copy the code

conclusion

There are a few things left to be said about clipping, such as the logic of completing the clipping and then re-entering the clipping. But the rest of the clipping logic is about as difficult as the above, and if you can understand the above, the rest of the logic shouldn’t be too difficult for you.

Finally, welcome everyone to point Star, mention Issue and PR~ for our project