他们主动布局(autolayout)环境的图像编辑器
hi,all:
在经过了一番犹豫之后。我决定将我自己做的这个小APP的源代码发布给大家:
其出发点是和大家一起学习iOS开发。仅供学习參考之用。
之前代码是托管与gitlab
上的,今天我将其pull到github上来了,大家能够自行下载:git clone git@github.com:lihux/twentyThousandTomatoes.git没有安装git或者不会用的童鞋,
请猛戳github地址:https://github.com/lihux/twentyThousandTomatoes。进去之后选
择download zip下载就可以。
在大部分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),而圆形框内部不进行填充(透明)。
详细实现代码例如以下:
<span style="font-size:18px;">-(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;
}
</span>
如今再来考虑怎样处理横竖屏的问题:我们的剪切框是直接通过UIView的drawRect
方法直接手绘上去的,因此无法通过自己主动布局(autolayout)对剪切框进行又一次布局。
解决的办法是在屏幕发生横竖屏切换的时候又一次绘制圆形剪切框。在iOS8中不再使
用willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration来获取屏幕旋转事件了。iOS8以后的使用新的
willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
来取代。
因此我们在这种方法中,强制裁剪框重绘(maskview):
<span style="font-size:18px;">#pragma mark - UIContentContainer protocol
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
[self.maskView setNeedsDisplay];
}
</span>
这样我们的剪切框就顺利完毕了,接下来我们来设置scrollview,使其满足我
们的交互预期。
2.3scrollview的设置
首先来看一下整个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的各项属性:
<span style="font-size:18px;">// TTPhotoMaskView.h
@protocol TTPhotoMaskViewDelegate <NSObject> - (void)pickingFieldRectChangedTo:(CGRect) rect; @end @interface TTPhotoMaskView : UIView @property (nonatomic, weak) id <TTPhotoMaskViewDelegate> delegate; @end</span>
在maskView的drawRect方法中加入:当中pickingFieldRect即为圆环剪切框
的“frame”,包括其相对于maskView的origin和size信息。
<span style="font-size:18px;"> if ([self.delegate respondsToSelector:@selector(pickingFieldRectChangedTo:)]) {
[self.delegate pickingFieldRectChangedTo:self.pickingFieldRect];
}
</span>
接下来就是在我们的viewController中实现pickingFieldRectChangedTo方法,
调整scrollView:
<span style="font-size:18px;">#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;
}
}
</span>
以下来具体解析一下上面每一步设置的作用,首先以一张苹果官方文档(Scroll
View Programming Guide for iOS)上的图片来简单看一下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对于放大缩小的支持很easy。你仅仅需设置放缩的最大和最小倍数,
然后在代理函数(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和其他财产。
版权声明:本文博客原创文章,博客,未经同意,不得转载。
他们主动布局(autolayout)环境的图像编辑器的更多相关文章
- IOS不用AutoLayout也能实现自己主动布局的类(3)----MyRelativeLayout横空出世
对于IOS开发人员来说,在自己主动布局出现前仅仅能通过计算和设置frame的值来处理.这样设置位置时就会出现非常多硬编码,同一时候在屏幕旋转和不同屏幕之间适配时须要编码又一次调整位置和尺寸,我们也能够 ...
- iOS 自己主动布局教程
springs和struts的问题 你肯定非常熟悉autosizing masks-也被觉得是springs&struts模式.autosizing mask决定了当一个视图的父视图大小改变时 ...
- Fragment为载体可自己主动布局的CardView(GitHub上写开源项目初体验)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 开篇废话: 前些天一直在看Android5.0 的Material Desgin,里面新增 ...
- ABBYY FineReader的图像编辑器功能使用方法
日常工作中,有时可能需要对图像进行编辑,可是正常情况下大家都知道图像是不能直接编辑的,需要借助工具.ABBYY FineReader 12 OCR文字识别软件可以实现图像的手动编辑,接下来就具体给大家 ...
- UITableViewHeaderFooterView的使用+自己主动布局
UITableViewHeaderFooterView的使用+自己主动布局 使用UITableView的header或footer复用时,假设採用自己主动布局,你会发现有约束冲突,以下这样写能够消除约 ...
- iOS_ScrollView的自己主动布局
ScrollView的自己主动布局稍显麻烦.但也是有规律可循, 下面就是仅竖向滑动的scrollView加入约束的固定做法 1.在控制器的view加入一个label.取名做anchor 2.给anch ...
- 【强大美观易用的图像编辑器】Pixelmator Pro 1.2 for Mac
图标 Icon 软件介绍 Description Pixelmator Pro是一个功能强大.美观.易于使用的图像编辑器,专为Mac设计.Pixelmator Pro 借助各种专业级的无损图像编辑 ...
- UNIX高级环境编程(8)进程环境(Process Environment)- 进程的启动和退出、内存布局、环境变量列表
在学习进程控制相关知识之前,我们需要了解一个单进程的运行环境. 本章我们将了解一下的内容: 程序运行时,main函数是如何被调用的: 命令行参数是如何被传入到程序中的: 一个典型的内存布局是怎样的: ...
- CSS3 网格布局(grid layout)基础知识 - 隐式网格自己主动布局(grid-auto-rows/grid-auto-columns/grid-auto-flow)
网格模板(grid-template)属性及其普通写法(longhands)定义了一个固定数量的轨道.构成显式网格. 当网格项目定位在这些界限之外.网格容器通过添加隐式网格线生成隐式网格轨道. 这些隐 ...
随机推荐
- storm-编程入门
一 编程接口 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhhbm ...
- hdu3038(并查集)
题目链接 题意:有n次询问,给出a到b区间的总和,问这n次给出的总和中有几次是和前面已近给出的是矛盾的 分析:sum数组维护着到根节点的距离(即区间和),每次合并x,y,s(a,b分别为x,y的根节点 ...
- Android入门之简单短信发送器
效果图: manifest.xml 文件中加入 <uses-permission android:name="android.permission.SEND_SMS"/&g ...
- Amazon S3数据一致性模型
左右Amazon S3有两种类型的数据的一致性模型的: 最后,一致性和读一致性. 有下面几种行为: 1 写一个新的object,然后開始读它.直到全部的变化都传播完(副本),你才干读到它,否则就是ke ...
- ActiveMQ源码架构解析第一节(转)
工作四年已久,也快到了而立之年,本人也酷爱技术,总是想找一些途径来提升自己,想着温故而知新所以就写起了博客,然而写博客这个想法也是酝酿了很久,近期也看到了有很多人在问关于ActiveMQ的相关问题,有 ...
- SWT的TitleAreaDialog详解
转自:http://www.cnblogs.com/AllenYoung/archive/2006/10/05/521805.html Dialog是SWT和JFace的一个重要的组成部分,我们在开发 ...
- evnetlet hub
hub 是 Eventlet's event loop的主要部分,用于分配I/O 事件 和调度绿色线程. Eventlet 有多种hub实现,现支持一下几种: epoll poll selects p ...
- java整合flex
java+flex项目整合 评论0 字号:大中小 订阅 第一种:javaproject和flexproject独立 这样的方式也是非常多人使用的方式.flex程序猿和java程序猿相互独立的工作. ...
- HDU 2451 Simple Addition Expression(组合数学)
主题链接:http://acm.hdu.edu.cn/showproblem.php?pid=2451 Problem Description A luxury yacht with 100 pass ...
- JDK8在Java转让Javascript脚本引擎动态地定义和运行代码
import java.lang.*; import java.util.Arrays; import java.util.List; import javax.script.Invocable; i ...