標籤:http io color os ar 使用 for sp strong
在大部分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),而圓形框內部不進行填充(透明)。
具體實現代碼如下:
| 1234567891011121314151617181920212223242526272829 |
-(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):
| 123456 |
#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的各項屬性:
| 123456789101112 |
// 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資訊。
| 123 |
if ([self.delegate respondsToSelector:@selector(pickingFieldRectChangedTo:)]) { [self.delegate pickingFieldRectChangedTo:self.pickingFieldRect]; } |
接下來就是在我們的viewController中實現pickingFieldRectChangedTo方法,調整scrollView:
| 1234567891011121314151617181920212223242526272829 |
#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)片編輯器的實現