在大部分APP(尤其是社交类的,如qq)经常会有更换头像的场景:点击用户加载头像,加载出系统图片,用户点击选中某张图片之后,可以对图片进行放缩和拖动,已更改圆形裁剪框圈定的图片部分。如下图即为qq的头像选取编辑界面:

图1.qq照片编辑界面

界面中可以对图片进行放大、缩小,拖动,白色圆环区域表示点击确定时将要裁剪的范围。留意上图的动画,qq总是能够确保圆环完全被图片所覆盖,如果拖动或者放缩使得图片以外的黑色区域进入了圆环,图片会自动弹回刚好能够完全覆盖的状态,鉴于CSDN上传图片2M的限制,上面的gif图很短,感兴趣的同学可以打开QQ自己体验一把(在修改个人头像功能中)。

现在我们也要实现一个类似功能的界面,并且是在autolayout环境下,同时支持横竖屏,这比QQ的图片选取页面又复杂了一些:QQ只支持竖屏的情况,不需要考虑横屏时的情况和横竖屏切换的问题。下面详细讨论。

一、预期效果

用户从相册或者相机中选取/拍摄一张照片,加载到图片编辑界面,用户可以拖动、放缩照片,使圆形选取框中截图到合适的图像作为用户头像。效果图如下图所示:

用户在拖动、放缩时要保证圆环区域全部被图片所覆盖,这样才能确保裁剪出来的照片刚好能够撑满整个圆形区域。同时,因为我们支持横屏布局,因此还要确保竖屏切换横屏(或者反之)之后,圆环仍在正确的区域。

图2.竖屏效果

图3.横屏效果

整个界面满足了上述用户交互需求之外,还要在用户点击确定的时候,将圆形区域的图片裁剪下来,实现图片编辑的功能。

二、实现细节

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),而圆形框内部不进行填充(透明)。

具体实现代码如下:

-(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):

#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的各项属性:

//  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信息。

    if ([self.delegate respondsToSelector:@selector(pickingFieldRectChangedTo:)]) {  [self.delegate pickingFieldRectChangedTo:self.pickingFieldRect];  }

接下来就是在我们的viewController中实现pickingFieldRectChangedTo方法,调整scrollView:

#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.拖动和缩放

三、总结

将图片加载进scrollview,对其放缩、拖动然后裁剪其中一部分是图片编辑器的主要功能,看似简单的功能需求,细究起来却处处是坑,必须要深入的思考其中的每一个细节,利用好UIView的drawRect方法,结合使用scrollview的特性方能得以实现。

本示例主要有以下两点值得关注:

1.圆形剪切框的实现,以及在autolayout环境下旋转屏后剪切框的处理;

2.scrollView的属性设置,必须要结合所加载图片的实际尺寸、圆形剪切框的位置和大小信息来动态的调整scrollView的contentSize、contentInset等属性。

iOS疯狂详解之自动布局(autolayout)下图片编辑器的实现相关推荐

  1. iOS疯狂详解之开源库

    youtube下载神器:https://github.com/rg3/youtube-dl vim插件:https://github.com/Valloric/YouCompleteMe vim插件配 ...

  2. iOS疯狂详解之AFNetworking图片缓存问题

    AFNetworking网络库已经提供了很好的图片缓存机制,效率是比较高的,但是我发现没有直接提供清除缓存的功能,可项目通常都需要添加 清除功能的功能,因此,在这里我以UIImageView+AFNe ...

  3. iOS疯狂详解之启动分层引导动画

    一. 为什么要写这篇文章? 这是一个很古老的话题,从两年前新浪微博开始使用多层动画制作iOS App的启动引导页让人眼前一亮(当然,微博是不是历史第一个这个问题值得商榷)之后,各种类型的引导页层出不穷 ...

  4. iOS疯狂详解之NSFileHandle

    // 创建一个文件 - (void)addField {NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocument ...

  5. IOS UIView详解

    文章目录 IOS UIView详解 1.官方类分析 2. UIView 常用的属性 2.1 UIView的圆角加阴影效果的实现 2.2 UIView 属性 2.2.1 UIView 几何属性 2.2. ...

  6. iOS绘图详解-多种绘图方式、裁剪、滤镜、移动、CTM

    iOS绘图详解 摘要: Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎.它提供了低级别.轻量级.高保真度的2D渲染.该框架可以用于基于路径的 绘 ...

  7. iOS多线程详解:实践篇

    iOS多线程实践中,常用的就是子线程执行耗时操作,然后回到主线程刷新UI.在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程.由于在iOS中除了主线程,其他子线程是独立 ...

  8. [iOS] 国际化详解

    PS:修改设备系统语言方法 设置 -> 通用 -> 语言与地区 -> iPhone 语言 Settings -> General -> Language & Re ...

  9. python命令提示符窗口在哪里_详解python命令提示符窗口下如何运行python脚本

    以arcgispro的python脚本为例在arcgispro自带的python窗口下运行python脚本 需求: 将arcgispro的.aprx项目包中gdb的数据源路径更换为sde数据源路径. ...

最新文章

  1. es6与java的相似度,特斯拉Model Y对比蔚来ES6!这次对比结果出乎意料
  2. POJ 3104 Drying 二分
  3. java颜色gui_Java gui颜色不加载
  4. React开发(254):react项目理解 ant design 注意参数传递格式
  5. inc指令是什么意思_西门子PLC一些指令
  6. Docker 遇到swapon failed Operation not permitted
  7. 嵌入式操作系统内核原理和开发(基础)
  8. c语言实验分支程序设计二,C语言程序实验报告分支结构的程序设计(0页).doc
  9. 开发规范 - UML图
  10. 配置×××服务器使用L2TP/IPSEC协议
  11. 设计模式---组合模式(C++实现)
  12. xshell使用隧道
  13. tomcat编码设置
  14. Asp.net core 中实现AOP面向切面编程
  15. 计算机sci论文怎么写,SCI论文从写作到发表步骤攻略
  16. php如何将时间戳,PHP如何将时间戳转换日期
  17. matlab脉冲压缩,雷达线性调频脉冲压缩的原理及其matlab仿真
  18. csv文件中文乱码转换
  19. Python+Django+Mysql实现购物商城推荐系统 基于用户、项目的协同过滤推荐购物商城系统 网络购物推荐系统 代码实现 源代码下载
  20. 有关积分的不等式证明

热门文章

  1. 区块链社区是什么?有哪些常见的区块链社区?
  2. eclipse中使用git提交时忽略不必要的文件
  3. 微信支付apiV3编程实例php,PHP 微信小程序 微信支付 v3
  4. 地下水位监测仪 原理
  5. 新浪云node加mysql_通过新浪云部署NideShop微信小程序商城(基于Node.js+MySQL+ThinkJS)...
  6. Access、Hybrid和Trunk三种模式的理解
  7. 概率论第二章知识点+错题总结
  8. 微电子技术和计算机技术的区别,信息技术和计算机技术的区别是什么?
  9. 基于Labview平台的滚动轴承故障分析与噪声评价系统
  10. 解决Nvivo自动编码问题的语言包