iOS自動佈局(autolayout)下圖片編輯器的實現

在大部分APP(尤爲是社交類的,如qq)常常會有更換頭像的場景:點擊用戶加載頭像,加載出系統圖片,用戶點擊選中某張圖片以後,能夠對圖片進行放縮和拖動,已更改圓形裁剪框圈定的圖片部分。以下圖即爲qq的頭像選取編輯界面:app

20141012114535375.gif

圖1.qq照片編輯界面編輯器

界面中能夠對圖片進行放大、縮小,拖動,白色圓環區域表示點擊肯定時將要裁剪的範圍。留意上圖的動畫,qq老是可以確保圓環徹底被圖片所覆蓋,若是拖動或者放縮使得圖片之外的黑色區域進入了圓環,圖片會自動彈回恰好可以徹底覆蓋的狀態,鑑於CSDN上傳圖片2M的限制,上面的gif圖很短,感興趣的同窗能夠打開QQ本身體驗一把(在修改我的頭像功能中)。ide

如今咱們也要實現一個相似功能的界面,而且是在autolayout環境下,同時支持橫豎屏,這比QQ的圖片選取頁面又複雜了一些:QQ只支持豎屏的狀況,不須要考慮橫屏時的狀況和橫豎屏切換的問題。下面詳細討論。函數

1、預期效果佈局

用戶從相冊或者相機中選取/拍攝一張照片,加載到圖片編輯界面,用戶能夠拖動、放縮照片,使圓形選取框中截圖到合適的圖像做爲用戶頭像。效果圖以下圖所示:動畫

用戶在拖動、放縮時要保證圓環區域所有被圖片所覆蓋,這樣才能確保裁剪出來的照片恰好可以撐滿整個圓形區域。同時,由於咱們支持橫屏佈局,所以還要確保豎屏切換橫屏(或者反之)以後,圓環仍在正確的區域。ui

20141012135048671.png

圖2.豎屏效果 
20141012135124296.pngatom

圖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方法中,繪製剪切框,繪製示意圖以下:

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.拖動和縮放

3、總結

將圖片加載進scrollview,對其放縮、拖動而後裁剪其中一部分是圖片編輯器的主要功能,看似簡單的功能需求,細究起來卻到處是坑,必需要深刻的思考其中的每個細節,利用好UIView的drawRect方法,結合使用scrollview的特性方能得以實現。

本示例主要有如下兩點值得關注:

1.圓形剪切框的實現,以及在autolayout環境下旋轉屏後剪切框的處理;

2.scrollView的屬性設置,必需要結合所加載圖片的實際尺寸、圓形剪切框的位置和大小信息來動態的調整scrollView的contentSize、contentInset等屬性。

相關文章
相關標籤/搜索