之前在完成一个项目时要求用户可以从相册中选择一张图片,裁剪出自己喜欢的区域作为头像或是背景的功能,尝试了许多方法后发现其中有很多小细节,于是就记录了下来,主要包括UIBezierPath与CALayer的结合使用、设置裁剪区域的大小以及裁剪图片三个部分。
1.UIBezierPath与CAShapeLayer的结合使用,实现裁剪区域
使用CAShapeLayer与UIBezierPath可以实现不在view的drawRect方法中就画出一些想要的图形,UIBezierPath对象是CGPathRef数据类型的封装,可以创建基于矢量的路径,例如圆形、多边形等形状 ,而每个CAShapeLayer对象都代表着将要被渲染到屏幕上的一个任意的形状,具体的形状由其path属性指定。
当我们对某一个Layer进行裁剪的时候,可以通过绘制贝塞尔曲线画出裁剪的区域,由于裁剪区域相对于背景视图是“亮”的,如下图所示,因此需要通过反向裁剪的方法去绘制贝塞尔曲线,即- (UIBezierPath *)bezierPathByReversingPath,裁剪“自己”,保留以外的部分。
cropView.png
2.重置锚点,让缩放更灵活
当我们对一些UIView进行缩放或是旋转操作的时候,大多都是以该视图的中心点进行操作的,也可以通过改变UIView的锚点来改变“中心点的位置”,就如下面这样:
Display.gif
anchorPoint是属于CALayer的属性,举个例子来说,就好比把一张白纸用图钉订在书桌上,如果订得不是很紧的话,白纸就可以沿顺时针或逆时针方向围绕图钉旋转,这时候图钉就起着支点的作用,通过改变图钉的位置可以改变白纸旋转的状态,类似的还有平移、缩放,anchorPoint也具备同样的功能,它用一种相对bounds的比例值来确定的,中心点的anchorPoint为(0.5,0.5),如下图所示:
anchorPoint.png
在上图中,当anchorPoint有为(0.5,0.5)时,它在superLayer中的实际位置为(100, 100),并且和position的值相等,如果改变anchorPoint 的值,结果也是如此,因此可以说position就是anchorPoint在superLayer中的位置,position是layer相对superLayer坐标空间的位置,从而可以得到它们之间的关系:
position.x = frame.origin.x + anchorPoint.x \* bounds.size.width;
position.y = frame.origin.y + anchorPoint.y \* bounds.size.height;
通过上述公式我们可以知道在修改anchorPoint时会移动layer,因为position不受影响,只能是frame.origin做相应的改变,因而会移动layer,所以在实际情况中,我们常常是想修改anchorPoint,但又不想要移动layer,所以就需要对position做相应地修改:
positionNew.x = positionOld.x + (anchorPointNew.x - anchorPointOld.x) \* bounds.size.width
positionNew.y = positionOld.y + (anchorPointNew.y - anchorPointOld.y) \* bounds.size.height
因此通过对图片添加自定义手势,并判断用户操作的是哪一个位置,从而实时更新锚点来实现此效果:
//左上、左下、右上、右下四个位置的单指缩放操作,并对边界做相应的处理
\- (void)cornerScaleGes:(UPScaleGestureRecognizer *)gesture {
CGRect newBounds = self.cropAreaView.bounds;
CGFloat newW = newBounds.size.width;
CGFloat newH = newBounds.size.height;
CGFloat moveW = gesture.cornerMoveW;
CGFloat moveH = gesture.cornerMoveH;
switch (gesture.cornerLocation) {
case FingerPanCornerLocationWithLeftTop:
[self resetanchorPoint:self.cropAreaView AnchorPoint:CGPointMake(1.0, 1.0)];
newW = MIN(newW - moveW, CGRectGetMaxX(self.cropAreaView.frame));
newH = MIN(newH - moveH, CGRectGetMaxY(self.cropAreaView.frame));
break;
case FingerPanCornerLocationWithLeftBottom:
[self resetanchorPoint:self.cropAreaView AnchorPoint:CGPointMake(1.0, 0.0)];
newW = MIN(newW - moveW, CGRectGetMaxX(self.cropAreaView.frame));
newH = MIN(newH + moveH, self.bounds.size.height - CGRectGetMinY(self.cropAreaView.frame));
break;
case FingerPanCornerLocationWithRightTop:
[self resetanchorPoint:self.cropAreaView AnchorPoint:CGPointMake(0.0, 1.0)];
newW = MIN(newW + moveW, self.bounds.size.width - CGRectGetMinX(self.cropAreaView.frame));
newH = MIN(newH - moveH, CGRectGetMaxY(self.cropAreaView.frame));
break;
case FingerPanCornerLocationWithRightBottom:
[self resetanchorPoint:self.cropAreaView AnchorPoint:CGPointMake(0.0, 0.0)];
newW = MIN(newW + moveW, self.bounds.size.width - CGRectGetMinX(self.cropAreaView.frame));
newH = MIN(newH + moveH, self.bounds.size.height - CGRectGetMinY(self.cropAreaView.frame));
break;
default:
break;
}
self.cropAreaView.bounds = CGRectMake(0, 0, newW, newH);
}
同理,除了四个对角之外,上下左右四个方向的处理也类似,最后,还要对图片缩放的边界进行处理就可以了。
3.裁剪图片
在iOS中裁剪图片的方式可以通过截屏的方式处理,即使用UIGraphicsBeginImageContextWithOptions来获取裁剪的区域,也可以将UIImage转换成CGImageRef来处理,由于前者无法获取裁剪区域的x、y的坐标,因此选择后者,为了保证裁剪后的图片的像素点不变,所以在进行裁剪操作时需要将裁剪区域做相应的放大处理:
//裁剪图片
UIView \*cropAreaView = self.cutDownImgView.subviews.lastObject;
CGImageRef sourceImgeRef = [self.cutDownImgView.image CGImage];
//保证裁剪图片的大小与原比例相符合
CGFloat scaleW = self.cutDownImgView.image.size.width / self.cutDownImgView.frame.size.width;
CGFloat scaleH = self.cutDownImgView.image.size.height / self.cutDownImgView.frame.size.height;
CGRect scaleRect = CGRectMake(cropAreaView.frame.origin.x \* scaleW, cropAreaView.frame.origin.y \* scaleH, cropAreaView.bounds.size.width * scaleW, cropAreaView.bounds.size.height \* scaleH);
CGImageRef newImageRef = CGImageCreateWithImageInRect(sourceImgeRef, scaleRect);
UIImage \*newImage = [UIImage imageWithCGImage:newImageRef];