zoukankan      html  css  js  c++  java
  • iOS自动布局(autolayout)下图片编辑器的实现

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

    20141012114535375.gif

    图1.qq照片编辑界面

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

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

    一、预期效果

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

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

    20141012135048671.png

    图2.竖屏效果 
    20141012135124296.png

    图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方法中,绘制剪切框,绘制示意图如下:

    360桌面截图20141014103117.jpg

    图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一致。

    20141013142547328.png

    图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的意义和作用:

    20141013143855807.jpg

    图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的最小缩放尺寸,以满足当放缩到最小时刚好图片较短的一个维度(长或者宽)和圆形剪切框相切,这是能够放缩的最小值,因为如果再缩小图片就无法填满剪切框了:

    360桌面截图20141014104104.jpg

    图7.放缩到最小时,剪切框必须要和较短的一边相切

    step 4只在viewDidLoad的时候执行,也即第一次进入图片编辑页面的时候,需要强制调整一下scrollview的当前zoomScale,使得图片在一个合适的尺寸显示出来。

    至此,整个功能完成,运行一下程序,看一下效果,达到了预期:

    20141013152324598.gif

    图8.转屏效果

    20141013152747427.gif

    图9.拖动和缩放

    三、总结

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

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

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

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

     
     
  • 相关阅读:
    171 01 Android 零基础入门 03 Java常用工具类02 Java包装类 01 包装类简介 01 Java包装类内容简介
    170 01 Android 零基础入门 03 Java常用工具类01 Java异常 08 Java异常总结 01 异常总结
    169 01 Android 零基础入门 03 Java常用工具类01 Java异常 07 异常链 01 异常链简介
    168 01 Android 零基础入门 03 Java常用工具类01 Java异常 06 自定义异常 01 自定义异常类
    167 01 Android 零基础入门 03 Java常用工具类01 Java异常 05 使用throw和throws实现异常处理 02 使用throw抛出异常对象
    166 01 Android 零基础入门 03 Java常用工具类01 Java异常 05 使用throw和throws实现异常处理 01 使用throws声明异常类型
    165 01 Android 零基础入门 03 Java常用工具类01 Java异常 04 使用try…catch…finally实现异常处理 05 return关键字在异常处理中的使用
    DevExpress WPF v20.2版本亮点放送:全新升级的PDF Viewer
    界面控件DevExpress使用教程:Dashboard – 自定义导出
    DevExpress WinForms帮助文档:表单控件
  • 原文地址:https://www.cnblogs.com/hanzhuzi/p/4060516.html
Copyright © 2011-2022 走看看