在大部分APP(尤为是社交类的,如qq)常常会有更换头像的场景:点击用户加载头像,加载出系统图片,用户点击选中某张图片以后,能够对图片进行放缩和拖动,已更改圆形裁剪框圈定的图片部分。以下图即为qq的头像选取编辑界面:app
图1.qq照片编辑界面编辑器
界面中能够对图片进行放大、缩小,拖动,白色圆环区域表示点击肯定时将要裁剪的范围。留意上图的动画,qq老是可以确保圆环彻底被图片所覆盖,若是拖动或者放缩使得图片之外的黑色区域进入了圆环,图片会自动弹回恰好可以彻底覆盖的状态,鉴于CSDN上传图片2M的限制,上面的gif图很短,感兴趣的同窗能够打开QQ本身体验一把(在修改我的头像功能中)。ide
如今咱们也要实现一个相似功能的界面,而且是在autolayout环境下,同时支持横竖屏,这比QQ的图片选取页面又复杂了一些:QQ只支持竖屏的状况,不须要考虑横屏时的状况和横竖屏切换的问题。下面详细讨论。函数
1、预期效果布局
用户从相册或者相机中选取/拍摄一张照片,加载到图片编辑界面,用户能够拖动、放缩照片,使圆形选取框中截图到合适的图像做为用户头像。效果图以下图所示:动画
用户在拖动、放缩时要保证圆环区域所有被图片所覆盖,这样才能确保裁剪出来的照片恰好可以撑满整个圆形区域。同时,由于咱们支持横屏布局,所以还要确保竖屏切换横屏(或者反之)以后,圆环仍在正确的区域。ui
图2.竖屏效果 atom
图3.横屏效果spa
整个界面知足了上述用户交互需求以外,还要在用户点击肯定的时候,将圆形区域的图片裁剪下来,实现图片编辑的功能。代理
2、实现细节
2.1基本思路
在实现上,这个页面能够分为两大块:一块是scrollview的设置:contentSize、contentInset、zoomScale等等;另外一块是剪切框的实现(白色圆环、外围半透明蒙层),以及横竖屏切换时剪切框如何变化等;而这两块又不是彻底独立的:scrollview的不少交互都依赖于剪切框:最小放缩不能小于剪切框、移动不能超出剪切框的范围等。能够认为,scrollview的属性依赖于剪切框的属性。而剪切框在横屏或者竖屏的时候大小位置是保持不变的,所以,咱们很天然的获得这样一个思路:先肯定剪切框,横竖屏都没问题了,再经过剪切框肯定scrollview。
2.2剪切框的实现
从图二中能够看出剪切框是一个比较特殊的界面:圆形虚线框内部是彻底透明的(clearColor or alpha = 0),而外围的填充部分则是半透明效果(blackColor and alpha = 0.2),常规的经过view的嵌套设置alpha、backgroundColor和layer.cornerRadius是不行的,由于view的alpha属性具备“遗传性”:父view的alpha将直接做用于全部的子view上去,这时咱们就要考虑经过更底层的绘图方式直接在一个view上完成剪切框的绘制工做。
咱们在storyboard中添加一个view(称之为:maskView),添加约束使其和scrollview大小、尺寸彻底保持一致。将这个view的class改成TTPhotoMaskView:一个咱们定制的view,在其drawRect方法中,绘制剪切框,绘制示意图以下:
图4.剪切框绘制
1.绘制两条封闭的线,一条是方形的,恰好覆盖整个view的边界,还一条是圆形的虚线裁剪框;
2.使用奇偶原则对这两条封闭曲线进行色彩填充,使得方框和圆形框之间的区域填充(黑色,alpha=0.2),而圆形框内部不进行填充(透明)。
具体实现代码以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
-(void)drawRect:(CGRect)rect
{
CGFloat width = rect.size.width;
CGFloat height = rect.size.height;
//pickingFieldWidth:圆形框的直径
CGFloat pickingFieldWidth = width < height ? (width - kWidthGap) : (height - kHeightGap);
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGContextSaveGState(contextRef);
CGContextSetRGBFillColor(contextRef, 0, 0, 0, 0.35);
CGContextSetLineWidth(contextRef, 3);
//计算圆形框的外切正方形的frame:
self.pickingFieldRect = CGRectMake((width - pickingFieldWidth) / 2, (height - pickingFieldWidth) / 2, pickingFieldWidth, pickingFieldWidth);
//建立圆形框UIBezierPath:
UIBezierPath *pickingFieldPath = [UIBezierPath bezierPathWithOvalInRect:self.pickingFieldRect];
//建立外围大方框UIBezierPath:
UIBezierPath *bezierPathRect = [UIBezierPath bezierPathWithRect:rect];
//将圆形框path添加到大方框path上去,以便下面用奇偶填充法则进行区域填充:
[bezierPathRect appendPath:pickingFieldPath];
//填充使用奇偶法则
bezierPathRect.usesEvenOddFillRule = YES;
[bezierPathRect fill];
CGContextSetLineWidth(contextRef, 2);
CGContextSetRGBStrokeColor(contextRef, 255, 255, 255, 1);
CGFloat dash[2] = {4,4};
[pickingFieldPath setLineDash:dash count:2 phase:0];
[pickingFieldPath stroke];
CGContextRestoreGState(contextRef);
self.layer.contentsGravity = kCAGravityCenter;
}
|
如今再来考虑如何处理横竖屏的问题:咱们的剪切框是直接经过UIView的drawRect方法直接手绘上去的,所以没法经过自动布局(autolayout)对剪切框进行从新布局。
解决的办法是在屏幕发生横竖屏切换的时候从新绘制圆形剪切框。在iOS8中再也不使用willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration来获取屏幕旋转事件了,iOS8之后的使用新的willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id)coordinator来代替。
所以咱们在这个方法中,强制裁剪框重绘(maskview):
1
2
3
4
5
6
|
#pragma mark - UIContentContainer protocol
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id)coordinator
{
[
super
willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
[self.maskView setNeedsDisplay];
}
|
这样咱们的剪切框就顺利完成了,接下来咱们来设置scrollview,使其知足咱们的交互预期。
2.3 scrollview的设置
首先来看一下整个view的层级结构:scrollview有一个撑满整个scrollview的imageView做为scrollview的content view,在scrollView之上盖着一个剪切框的view(mask view),这三个view都经过约束保持和根view的bounds一致。
图5.view的层级结构
上面提到,scrollview的各类属性的设置都要依赖于手绘出的剪切框。而圆形剪切框的位置、大小在每次转屏以后可能发生变化,所以咱们必需要在每次maskView的drawRect方法调用以后都从新调整一下scrollview的属性。所以咱们在maskView中添加一个代理,将这个代理设置为maskview所在的viewController,每次当重绘发生后就经过代理方法通知viewcontroller调整scrollview的各项属性:
1
2
3
4
5
6
7
8
9
10
11
12
|
// TTPhotoMaskView.h
@protocol TTPhotoMaskViewDelegate
- (void)pickingFieldRectChangedTo:(CGRect) rect;
@end
@interface TTPhotoMaskView : UIView
@property (nonatomic, weak) id delegate;
@end
|
在maskView的drawRect方法中添加:其中pickingFieldRect即为圆环剪切框的“frame”,包含其相对于maskView的origin和size信息。
1
2
3
|
if
([self.delegate respondsToSelector:@selector(pickingFieldRectChangedTo:)]) {
[self.delegate pickingFieldRectChangedTo:self.pickingFieldRect];
}
|
接下来就是在咱们的viewController中实现pickingFieldRectChangedTo方法,调整scrollView:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#pragma mark - TTPhotoMaskViewDelegate
- (void)pickingFieldRectChangedTo:(CGRect)rect
{
self.pickingFieldRect = rect;
CGFloat topGap = rect.origin.y;
CGFloat leftGap = rect.origin.x;
self.scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(topGap, leftGap, topGap, leftGap);
//step 1: setup contentInset
self.scrollView.contentInset = UIEdgeInsetsMake(topGap, leftGap, topGap, leftGap);
CGFloat maskCircleWidth = rect.size.width;
CGSize imageSize = self.originImage.size;
//setp 2: setup contentSize:
self.scrollView.contentSize = imageSize;
CGFloat minimunZoomScale = imageSize.width < imageSize.height ? maskCircleWidth / imageSize.width : maskCircleWidth / imageSize.height;
CGFloat maximumZoomScale = 5;
//step 3: setup minimum and maximum zoomScale
self.scrollView.minimumZoomScale = minimunZoomScale;
self.scrollView.maximumZoomScale = maximumZoomScale;
self.scrollView.zoomScale = self.scrollView.zoomScale < minimunZoomScale ? minimunZoomScale : self.scrollView.zoomScale;
//step 4: setup current zoom scale if needed:
if
(self.needAdjustScrollViewZoomScale) {
CGFloat temp = self.view.bounds.size.width < self.view.bounds.size.height ? self.view.bounds.size.width : self.view.bounds.size.height;
minimunZoomScale = imageSize.width < imageSize.height ? temp / imageSize.width : temp / imageSize.height;
self.scrollView.zoomScale = minimunZoomScale;
self.needAdjustScrollViewZoomScale = NO;
}
}
|
下面来详细解析一下上面每一步设置的做用,首先以一张苹果官方文档(Scroll View Programming Guide for iOS)上的图片来简单看一下contentSize和contentInset的意义和做用:
图6.UIScrollView的contentSize和contentInset属性示意图
contentSize是你在scrollView中要展现的内容(content)的大小,具体值要根据content的尺寸而定,咱们这里是要完整的无压缩的展现一个图片的内容,所以这里在step 2中将contentSize设为图片(image.size)的size同等大小。
contentInset能够理解为展现内容的上下左右“留白”的间距,默认值为(0,0,0,0),contentInset所标示的留白加上contentSize才是一个scrollView所能滑动的所有区域。这里咱们不想让content(图片)的滑动区域超出圆形剪切框的位置,能够经过巧妙的讲剪切框圆环和view的上下左右边缘的间距做为scrollView的contentInset,这就是step 1作的事情,它确保了手指在图片上拖动的时候圆形剪切框总能填满图片的内容。
scrollView对于放大缩小的支持很是简单,你只需设置放缩的最大和最小倍数,而后在代理函数(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView中返回要缩放的view便可。这里主要须要肯定的时scrollview的最小缩放尺寸,以知足当放缩到最小时恰好图片较短的一个维度(长或者宽)和圆形剪切框相切,这是可以放缩的最小值,由于若是再缩小图片就没法填满剪切框了:
图7.放缩到最小时,剪切框必需要和较短的一边相切
step 4只在viewDidLoad的时候执行,也即第一次进入图片编辑页面的时候,须要强制调整一下scrollview的当前zoomScale,使得图片在一个合适的尺寸显示出来。
至此,整个功能完成,运行一下程序,看一下效果,达到了预期:
图8.转屏效果
图9.拖动和缩放
3、总结
将图片加载进scrollview,对其放缩、拖动而后裁剪其中一部分是图片编辑器的主要功能,看似简单的功能需求,细究起来却到处是坑,必需要深刻的思考其中的每个细节,利用好UIView的drawRect方法,结合使用scrollview的特性方能得以实现。
本示例主要有如下两点值得关注:
1.圆形剪切框的实现,以及在autolayout环境下旋转屏后剪切框的处理;
2.scrollView的属性设置,必需要结合所加载图片的实际尺寸、圆形剪切框的位置和大小信息来动态的调整scrollView的contentSize、contentInset等属性。