IOS的UIImagePickerController可以讓用戶通過相機或者相冊獲取想要的圖片,並且通過設置allowsEditing屬性允許用戶在選擇了圖片以后對圖片進行裁剪。不過在某些時候會出現正方形的裁剪框沒有適配圖片的情況,如下圖:
這時候裁剪得到的是一張長方形圖片,並且圖片尺寸與UIImagePickerController設置的maxWidth和maxHeight尺寸並不符合。例如一個高和寬比例為1:2的圖片,設置裁剪的maxWidth和maxHeight均為100,裁剪框的范圍類似於上面右邊的圖片,上下留空左右框住圖片的邊界。最終獲取到的裁剪結果為一張寬為100高為200的長方形圖片。
現在需要一個圖片裁剪器,能夠自適應圖片最窄邊的裁剪,保證最終得到的圖片為正方形。綜合網上查詢結果,決定做一個類似WP7系統自帶的裁剪方案:一個固定的透明框在屏幕中央,周圍留黑色遮罩,允許用戶隨意縮放和移動圖片,不過圖片的邊界不會超出屏幕中的透明框。
首先考慮裁剪框,需要固定在屏幕中間不動,並且周圍是透明的遮罩,決定采用UIImageView顯示一張圖片在屏幕的最頂層。裁剪器在我這里實際的用途為裁剪頭像,定義的標准為兩種,720X720的高清頭像和300X300的普通頭像。因此透明框的大小我選擇了300X300,在屏幕中居中。
為了實現圖片的滑動和縮放,選擇UIScrollView作為容器來裝顯示用戶圖片的ImageView。為了保證圖片邊界不超出裁剪框范圍,需要根據圖片的長寬來定義ScrollView的ContentSize,並適應縮放。代碼中我重寫了SourceImage的Set方法,在Set方法中適配好圖片顯示和ContentSize的大小。並且在縮放倍數上作限制,如果圖片本身的最短邊就不足720則禁止縮放,如果超過720則最大允許縮放到裁剪框內畫面的實際大小為720大小。
ContentView要根據image的大小調整,保證圖片不會超出裁剪框。ContentView過大和過小都會影響裁剪。

1 - (void)setSourceImage:(UIImage *)image{ 2 if (sourceImage) { 3 [sourceImage release]; 4 sourceImage = nil; 5 } 6 sourceImage = [image retain]; 7 [_imageview setImage:self.sourceImage]; 8 CGFloat wh = sourceImage.size.width/sourceImage.size.height; 9 CGSize displaySize; 10 if (wh > 1) {//寬圖 11 _imageContainer.maximumZoomScale = ((sourceImage.size.height / DEF_CUTSIZE > 1)&&(sourceImage.size.height / DEF_CUTSIZE)*(DEF_CUTSIZE/DEF_HDSIZE) > 1) ? (sourceImage.size.height / DEF_CUTSIZE)*(DEF_CUTSIZE/720) : 1;//設置放大倍數 12 isImgAvailable = (sourceImage.size.height*2 < DEF_CUTSIZE) ? NO : YES;//檢查圖片是否可用 13 displaySize = CGSizeMake(sourceImage.size.width*(DEF_CUTSIZE/sourceImage.size.height), DEF_CUTSIZE); 14 }else{//高圖 15 _imageContainer.maximumZoomScale = ((sourceImage.size.width / DEF_CUTSIZE > 1)&&(sourceImage.size.width / DEF_CUTSIZE)*(DEF_CUTSIZE/DEF_HDSIZE) > 1) ? (sourceImage.size.width / DEF_CUTSIZE)*(DEF_CUTSIZE/720) : 1;//設置放大倍數 16 isImgAvailable = (sourceImage.size.width*2 < DEF_CUTSIZE) ? NO : YES;//檢查圖片是否可用 17 displaySize = CGSizeMake(DEF_CUTSIZE, sourceImage.size.height*(DEF_CUTSIZE/sourceImage.size.width)); 18 } 19 _imageview.frame = CGRectMake(0, 0, displaySize.width, displaySize.height); 20 _imageContainer.contentSize = _imageview.frame.size; 21 _imageContainer.contentInset = UIEdgeInsetsMake((SCREEN_HEIGHT - DEF_CUTSIZE)/2, (SCREEN_WIDTH - DEF_CUTSIZE)/2, (SCREEN_HEIGHT - DEF_CUTSIZE)/2, (SCREEN_WIDTH - DEF_CUTSIZE)/2); 22 23 //讓圖片居中顯示 24 _imageContainer.contentOffset = (wh>1) ? CGPointMake((displaySize.width - SCREEN_WIDTH)/2, _imageContainer.contentOffset.y) : CGPointMake(_imageContainer.contentOffset.x, (displaySize.height - SCREEN_HEIGHT)/2); 25 }
圖片的拖動和縮放做好以后,剩下的就是裁剪了。裁剪方法是從網上抄來的代碼,全網都在轉載不知具體出處了。定義好裁剪區域的大小和起始坐標就可以得到裁剪完成的圖片了。

1 CGPoint point = CGPointMake(_imageContainer.contentOffset.x + (SCREEN_WIDTH - DEF_CUTSIZE)/2, _imageContainer.contentOffset.y + (SCREEN_HEIGHT - DEF_CUTSIZE)/2); 2 CGRect imageRect = CGRectMake(point.x * (self.sourceImage.size.width / _imageview.frame.size.width), point.y * (self.sourceImage.size.height / _imageview.frame.size.height), DEF_CUTSIZE * (self.sourceImage.size.width / _imageview.frame.size.width), DEF_CUTSIZE * (self.sourceImage.size.height / _imageview.frame.size.height)); 3 subImage = [self getImageFromImage:self.sourceImage subImageSize:imageRect.size subImageRect:imageRect]; 4 5 6 //圖片裁剪 7 -(UIImage *)getImageFromImage:(UIImage*) superImage subImageSize:(CGSize)subImageSize subImageRect:(CGRect)subImageRect { 8 // CGSize subImageSize = CGSizeMake(WIDTH, HEIGHT); //定義裁剪的區域相對於原圖片的位置 9 // CGRect subImageRect = CGRectMake(START_X, START_Y, WIDTH, HEIGHT); 10 CGImageRef imageRef = superImage.CGImage; 11 CGImageRef subImageRef = CGImageCreateWithImageInRect(imageRef, subImageRect); 12 UIGraphicsBeginImageContext(subImageSize); 13 CGContextRef context = UIGraphicsGetCurrentContext(); 14 CGContextDrawImage(context, subImageRect, subImageRef); 15 UIImage* returnImage = [UIImage imageWithCGImage:subImageRef]; 16 UIGraphicsEndImageContext(); //返回裁剪的部分圖像 17 return returnImage; 18 }
最后添加一個動畫效果,模仿Path軟件中看圖片的動畫,將裁剪框周圍的遮罩漸變為全黑,只保留裁剪好的圖片,最后裁剪好的圖片逐漸縮小到顯示頭像的地方。為了實現這個效果,在裁剪器中添加一個CGRect屬性,生成裁剪器時設置好返回頭像位置的坐標和大小,采用UIView的animateWithDuration方法實現動畫效果即可。
在裁剪好了以后,頭像位置顯示的為300X300的小頭像,因此增加一個查看720X720高清頭像的方法。模仿Path查看圖片的漸變動畫效果,頁面中頭像以外的其他元素漸變至全黑,同時將頭像放大到屏幕大小。實現方法為用一個新的Controller來顯示頭像變大變小的動畫和展示大圖,原頁面的小頭像僅響應點擊。在presentViewController到新的Controller時會擋住原頁面,為了實現半透明的漸變效果,需要設置原頁面Controler的modalPresentionStyle屬性為UIModalPresentationCurrentContext。新的Controller將實現進入和退出動畫,在大圖顯示的時候點擊屏幕執行退出動畫。

1 // 2 // AvatarHDViewController.h 3 // CutPicTest 4 // 5 // Created by liulu on 12-12-21. 6 // Copyright (c) 2012年 liulu. All rights reserved. 7 // 8 9 #import "AvatarHDViewController.h" 10 #import "AppDelegate.h" 11 #import <QuartzCore/QuartzCore.h> 12 13 #define SCREEN_WIDTH 320 14 #define SCREEN_HEIGHT 480 15 16 @interface AvatarHDViewController () 17 18 @end 19 20 @implementation AvatarHDViewController 21 @synthesize avatarImg; 22 @synthesize beginRect; 23 @synthesize delegate; 24 25 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 26 { 27 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 28 if (self) { 29 // Custom initialization 30 isShowHDImg = NO; 31 self.view.backgroundColor = [UIColor clearColor]; 32 33 _viewBg = [[UIView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)]; 34 [self.view addSubview:_viewBg]; 35 [self.view sendSubviewToBack:_viewBg]; 36 _viewBg.backgroundColor = [UIColor blackColor]; 37 _viewBg.alpha = 0; 38 } 39 return self; 40 } 41 42 - (void)viewDidLoad 43 { 44 [super viewDidLoad]; 45 // Do any additional setup after loading the view. 46 _avatarImgV = [[UIImageView alloc]init]; 47 [self.view addSubview:_avatarImgV]; 48 [_avatarImgV.layer setMasksToBounds:YES]; 49 // [_avatarImgV.layer setCornerRadius:6.0]; 50 51 _avatarImgV.contentMode = UIViewContentModeScaleAspectFill; 52 53 } 54 55 -(void)viewDidAppear:(BOOL)animated{ 56 [super viewDidAppear:animated]; 57 [self enterAnimation]; 58 } 59 60 - (void)dealloc{ 61 [_avatarImgV release]; 62 [super dealloc]; 63 } 64 65 #pragma mark - 66 #pragma mark set 67 - (void)setAvatarImg:(UIImage *)img{ 68 avatarImg = img; 69 [_avatarImgV setImage:self.avatarImg]; 70 } 71 72 - (void)setBeginRect:(CGRect)rect{ 73 beginRect = rect; 74 _avatarImgV.frame = self.beginRect; 75 } 76 77 #pragma mark - 78 #pragma mark Animation 79 - (void)enterAnimation{ 80 // [UIView animateWithDuration:0.2 animations:^{ 81 // _viewBg.alpha = 1; 82 // }completion:^(BOOL finished){ 83 // if (finished) { 84 [UIView animateWithDuration:0.5 animations:^{ 85 _avatarImgV.frame = CGRectMake(0, (SCREEN_HEIGHT - SCREEN_WIDTH)/2, SCREEN_WIDTH, SCREEN_WIDTH); 86 _viewBg.alpha = 1; 87 }completion:^(BOOL finished){ 88 if (finished) { 89 //添加手勢 90 if (!_recognizer) { 91 _recognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleSwipeFromDownToUp)]; 92 } 93 [_recognizer setNumberOfTapsRequired:1]; 94 [_recognizer setNumberOfTouchesRequired:1]; 95 [self.view addGestureRecognizer:_recognizer]; 96 } 97 }]; 98 // } 99 // }]; 100 } 101 102 - (void)exitAnimation{ 103 // [UIView animateWithDuration:0.4 animations:^{ 104 // _avatarImgV.frame = self.beginRect; 105 // }completion:^(BOOL finished){ 106 // if (finished) { 107 [UIView animateWithDuration:0.5 animations:^{ 108 _viewBg.alpha = 0; 109 _avatarImgV.frame = self.beginRect; 110 }completion:^(BOOL finished){ 111 if (self.delegate&&[self.delegate respondsToSelector:@selector(hiddenHDUserImg)]) { 112 [self.delegate hiddenHDUserImg]; 113 } 114 }]; 115 // } 116 // }]; 117 } 118 119 - (void)handleSwipeFromDownToUp{ 120 //移除手勢 121 for (UITapGestureRecognizer* recognizer in self.view.gestureRecognizers) { 122 if (recognizer==_recognizer) { 123 [self.view removeGestureRecognizer:recognizer]; 124 } 125 } 126 [self exitAnimation]; 127 } 128 129 - (void)didReceiveMemoryWarning 130 { 131 [super didReceiveMemoryWarning]; 132 // Dispose of any resources that can be recreated. 133 } 134 135 @end
最終效果:
因為設計的時候考慮不足,在真機上拍照以后可能出現裁剪得到的圖片與裁剪框中不同的問題,這是因為ios的相機並沒有根據拍照時的重力方向來將圖片實際旋轉,而是采用了寫入圖片EXIF信息的方式確保圖片顯示方向正確。因此在裁剪圖片時還需要根據從相冊獲取到的UIImage對象的imageOrientation來重新計算正確的裁剪坐標和區域才能得到正確的圖像。
最終我的這個裁剪器還是沒有在實際當中使用,原因是為了適配高清圖片,在圖片最小邊不足720時我直接禁止用戶放大了,導致用戶體驗非常不好。而應用在設置頭像的場景中時很多時候對於一張照片用戶確實就只想截取其中的某個區域作為頭像,而用戶很少會在意頭像是否是絕對的高清。並且300的尺寸和720的尺寸在大部分手機屏幕上實際上看起來差別並不大。設計時更應該全面考慮實際應用情況,720的分辨率只應該作為高清頭像的一個上限標准,而不該強制用戶使用720分辨率頭像。