最近和朋友@anotheren合作,打算做一套微信图片选择器。于是就有了AnyImageKit框架,至此图片选择和编辑功能已经完成。在做图像编辑功能的时候,我在裁剪功能上做了很长时间。我想到了一个主意。做到一半发现不行,就推翻重做。经历了两三遍这个过程,我终于做到了。这个函数的坑还是蛮多的,网上资料也不是很多,所以想写一篇文章记录一下。首先,我们要解决三个小问题。问题一:如何完整显示图片首先考虑横屏(第二张图)的情况,将图片的宽度设置为scrollView.bounds.width,然后按比例缩放图片的高度。width=scrollView.bounds.widthheight=image.width*scrollView.bounds.height/scrollView.bounds.width接下来考虑竖图(第一张图)的情况,在上一步的基础上判断。//如果图片高度超过scrollView.bounds.height,则为竖版图片,将图片高度缩放到scrollView.bounds.height,然后根据比例计算宽度。ifheight>scrollView.bounds.height{height=scrollView.bounds.heightwidth=image.height*scrollView.bounds.width/scrollView.bounds.height}最后根据size计算imageView.frame,问题解决。注:灰色部分为scrollView问题2:图片缩放后,超出scrollView的部分如何显示?看到这个问题,自然会想到scrollView可能是全屏的,所以才可以全屏显示。但是全屏的scrollView会有一些无法解决的问题。下面会提到第三个问题,暂时不考虑这个方案。第二种方案就简单多了,设置scrollView.clipsToBounds=false就可以解决这个问题。问题三:在没有缩放比例的情况下,如何让图片可以拉动?众所周知,当scrollView.contentSizescrollView.bounds.size接下来我们设置contentInset的值为0.1(肉眼不可见)scrollView.contentInset=UIEdgeInsets(top:0.1,left:0.1,bottom:0.1,right:0.1)这样设置后图片可以左右滚动,但是不能上下滚动,因为图片的宽度和scrollView相等,但是height不是,所以需要调整高度计算:letbottomInset=scrollView.bounds.height-cropRect.height+0.1是处理竖图的宽度,整合代码:letrightInset=scrollView.bounds.width-cropRect.width+0.1letbottomInset=scrollView.bounds。height-cropRect.height+0.1scrollView.contentInset=UIEdgeInsets(top:0.1,left:0.1,bottom:bottomInset,right:rightInset)到这里问题3就解决了,现在我们反过来看问题2,如果在问题2中的话使用全屏scrollView,那么第三个问题不好解决~裁剪裁剪的UI部分这里就不展开了,主要说明裁剪框的四个角是用UIView绘制的,它们的层级是和scrollView一样,它们的位置可以用一个CGRect变量cropRect来描述。裁剪的核心内容是如何在裁剪框移动时将图片移动到正确的位置。根据动画显示的效果可以得出:scrollView的缩放发生了变化,scrollView的偏移量发生了变化,裁剪框的位置发生了移动。让我们看看如何逐步解决这些问题。ZoomScale从动图中可以看出,scrollView移动裁剪框后需要进行缩放,有两种情况,一种是横图,一种是竖图,所以我们需要计算缩放比例两种情况,然后选择使用其中一种。我们假设图片的大小为ABCD,将D点移动到G点的位置,即裁剪框的位置为AEFG。当用户放手时,AEFG需要放大到ABCD的位置,所以我们可以得到缩放比例:AB/AE=375/187.5=2.0但是这还没完。想象一下,当AEFG放大到ABCD时,再次将D指向G点的位置,这个操作相当于在图片缩放之前从G点移动到J点。根据前面的结论,我们可以知道变焦比为:AB/AH。实际代码中AB=scrollView.bounds.width,下面需要AH的值。AEFG从D点到G点放大2.0倍到ABCD,即cropRect.width=187.5AH=cropRect.width/scrollView.zoomScale=187.5/2.0=93.75现在我们得到了图像水平缩放的公式,和竖图也是一样的,代码如下:,我们需要分析一下水平图像的缩放比例仍然是垂直的。我们将裁剪框的宽度即cropRect.width缩放为scrollView.bounds.width,根据缩放比例计算缩放后的cropRect.height。如果cropRect.height>scrollView.bounds.height,说明高度过高。我们将使用垂直图像的缩放公式,反之亦然,代码如下:(cropRect.height/scrollView.zoomScale)letisVertical=cropRect.height*(scrollView.bounds.width/cropRect.width)>scrollView.bounds.heightletzoom:CGFloatif!isVertical{zoom=zoomH>maxZoom?maxZoom:zoomH}else{zoom=zoomV>maxZoom?maxZoom:zoomV}ContentOffset现在我们来计算contentOffset,设图片为ABCD,将A点移动到E点,将EFGD放大2.0倍到ABCD。由此我们可以得到:(scrollView.bounds.width/cropRect.width)>scrollView.bounds.heightletzoom:CGFloatif!isVertical{zoom=zoomH>maxZoom?maxZoom:zoomH}else{zoom=zoomV>maxZoom?maxZoom:zoomV}以上公式不是最终的公式,然后根据当前的缩放比例,再次将A点移动到E点。这个操作相当于在图片未缩放前将图片从E点移动到H点。由此可知:注:?表示缩放前,?表示缩放一次后;cropStartPanRect是手势开始前裁剪框的位置E(x)=CG?=CG?*zoom=(cropRect.origin.x-cropStartPanRect.origin.x)*zoom最后我们根据移动计算出最终的contentOffsetletzoomScale=角度缩放/scrollView.zoomScaleletoffsetX=(scrollView.contentOffset.x*zoomScale)+((cropRect.origin.x-cropStartPanRect.origin.x)*zoomScale)letoffsetY=(scrollView.contentOffset.y*zoomScale)+((cropRect.origin.y-cropStartPanRect.origin.y)*zoomScale)letoffset:CGPointswitchposition{//枚举,标志角的位置case.topLeft://移动左上角,contentOffsetx和y都要改offset=CGPoint(x:offsetX,y:offsetY)case.topRight://移动右上角,contentOffsety要改offset=CGPoint(x:scrollView.contentOffset.x*zoomScale,y:offsetY)case.bottomLeft://移动左下角,contentOffsetx需要改变contentOffset保持不变offset=CGPoint(x:scrollView.contentOffset.x*zoomScale,y:scrollView.contentOffset.y*zoomScale)}NewCropRect最后,拖动裁剪框松手后,我们需要将裁剪框放大居中.这个逻辑和第一题计算图片缩放比例的逻辑是一样的,使用横竖图的计算逻辑是一样的,这里不再赘述。letnewCropRect:CGRectif(zoom==maxZoom&&!isVertical)||zoom==zoomH{letscale=scrollView.bounds.width/cropRect.widthleheight=cropRect.height*scalelety=(scrollView.bounds.height-height)/2+scrollView。frame.origin.ynewCropRect=CGRect(x:scrollView.frame.origin.x,y:y,width:scrollView.bounds.width,height:height)}else{letscale=scrollView.bounds.height/cropRect.heightletwidth=cropRect.width*scaleletx=(scrollView.bounds.width-width+scrollView.frame.origin.x)/2newCropRect=CGRect(x:x,y:scrollView.frame.origin.y,width:width,height:scrollView.frame.height)}结语关于裁剪的内容还有一些,比如裁剪完成,裁剪后重新进入裁剪的逻辑等等,但是剩下的裁剪逻辑的难点和上面的内容差不多。如果你能看懂上面的内容,相信剩下的逻辑对你来说并不难。最后,欢迎大家给我们的项目打个star,issue和PR~