譯者:李鑫
原文:A Class for Selecting a Profile Image
本文為極客學(xué)院Wiki組織翻譯,轉(zhuǎn)載請(qǐng)注明出處。
時(shí)間:2016.3.14
文章介紹如何使用 UIImagePickerController 重制通訊錄應(yīng)用中常見(jiàn)的輪廓圖像選擇功能。其中涉及到了很多問(wèn)題。該文展示了一個(gè)類如何使用 UIImagePickerController 來(lái)復(fù)制這種功能。
如果想按照通訊錄應(yīng)用中那種功能來(lái)選擇并編輯圖像,似乎應(yīng)該采用 UIImagePickerController 類(圖像拾取器)。它似乎甚至還能完美地對(duì)編輯屏幕疊加層進(jìn)行自定義。但遺憾的是,事實(shí)證明它是不可改變的。復(fù)制這些功能讓我付出了常人難以想象的困難。
本文介紹了 MMSProfileImagePicker 類(輪廓圖像拾取器),它可以進(jìn)行圖像選擇和編輯,跟通訊錄應(yīng)用中的功能是等同的。
有些人已經(jīng)解決了這個(gè)問(wèn)題,并把他們的類放到了開(kāi)發(fā)者社區(qū)中進(jìn)行分享。所以你肯定會(huì)想,你的方案又有什么新意呢?很簡(jiǎn)單,我只想通過(guò)這篇文章來(lái)展示一些技巧,你有可能會(huì)在解決其他難題時(shí)用上它。
借助于圖像拾取器,這種解決方案支持了這些功能。本文主要考慮與之集成的一些技術(shù),以及如何在你自己的應(yīng)用中使用 輪廓圖像拾取器。可下載到的范例文件中已經(jīng)實(shí)現(xiàn)了一個(gè)等同于通訊錄應(yīng)用中選擇圖像的功能。
圖 1 -范例應(yīng)用
在探索如何使用圖像拾取器的疊加特性時(shí),我遇到了一些令人困惑的問(wèn)題,比如說(shuō),如何正確定位疊加并改變它的大?。咳绾卧谟孟鄼C(jī)選擇圖像時(shí),讓圓形只顯示在編輯屏幕呢?以及如何按照 z 順序正確定位圓形疊加層呢?當(dāng)然,這些只是我現(xiàn)在能想到的一部分問(wèn)題而已。
有些問(wèn)題當(dāng)時(shí)沒(méi)有解決方案,有些則很復(fù)雜,有些則跟類的內(nèi)部實(shí)現(xiàn)結(jié)合得過(guò)于緊密。比如說(shuō),stackoverflow 上的這個(gè)解法通過(guò)操縱圖像拾取器創(chuàng)建的私有視圖來(lái)顯示圓形疊加。
這種方法無(wú)疑是很聰明,但它容易受到將來(lái) iOS 版本更新的影響。而且,它只適用于選擇一副圖片的情況,而不適用于拍照并編輯一副圖片的情況。換句話說(shuō)就是:不可能在顯示相機(jī)時(shí)攔截導(dǎo)航委托調(diào)用,從而在移動(dòng)并縮放屏幕(圖像編輯屏幕)出現(xiàn)之前插入疊加層。
要解決的問(wèn)題太多,限于本文篇幅,就不贅述了。關(guān)于如何裁切位圖,請(qǐng)參考我之前的文章裁切圖像的 View 類。那里介紹到了這里所使用的 UIImage+Cropping 類別。
注意,我沒(méi)有用到任何開(kāi)源解決方案,如有雷同,實(shí)屬巧合。
我的策略就是,盡量發(fā)掘圖像拾取器的功能,因?yàn)樗蟛糠治宜枰墓δ?。不幸的是,有些類的功能必須重新?shí)現(xiàn)才能完美復(fù)制通訊錄應(yīng)用中的相關(guān)功能。其中的一個(gè)挑戰(zhàn)就是如何將圓形疊加層只顯示在編輯屏幕上。在配置為顯示相機(jī)時(shí),疊加層會(huì)顯示在獲取圖像屏幕和編輯屏幕上。結(jié)果,解決這個(gè)問(wèn)題時(shí),會(huì)需要對(duì)之前圖像拾取器已經(jīng)解決過(guò)的問(wèn)題進(jìn)行另一種考慮。
為了解決這種需求,我最終的結(jié)論是:最好是讓圖像拾取器負(fù)責(zé)呈現(xiàn)相機(jī)以及從相簿中選擇圖像這兩種屏幕,讓新的類負(fù)責(zé)創(chuàng)建并顯示圖像編輯屏幕。
這種方法還需要解決以下這些復(fù)雜問(wèn)題:
這種解決方法徹底重建了編輯屏幕功能,而不采用圖像拾取器的編輯屏幕。當(dāng)配置為從相機(jī)中選擇圖像時(shí),圖像拾取器會(huì)在獲取圖像和編輯屏幕上顯示 cameraOverlay 屬性。這一方法提供的解決方案可防止圓形疊加層顯示在相機(jī)的圖像獲取屏幕上。
要想在“從相簿中選擇”屏幕中顯示“自定義編輯”屏幕,所用的配置十分簡(jiǎn)單。圖像拾取器的一個(gè)屬性可防止編輯屏幕的呈現(xiàn)。將 allowsEditing 屬性設(shè)為 NO,就會(huì)返回用戶的選擇,但不會(huì)顯示它。這就為顯示自定義編輯屏幕提供了一種非常簡(jiǎn)單的方法。
遺憾的是,當(dāng)配置為相機(jī)選擇時(shí),圖像拾取器并不按照這一屬性來(lái)做,依然顯示編輯屏幕。這下事情就變得棘手了。我可不想重寫(xiě)一遍相機(jī)功能,但目前也沒(méi)有任何現(xiàn)成的方法來(lái)禁用它,而且說(shuō)歸到底,我還是不想放棄這種方法。
如果我能找到一種方法讓圖像拾取器調(diào)用我的 action 方法(而不是它自己的),那么 輪廓圖像拾取器就會(huì)防止編輯屏幕呈現(xiàn)出來(lái)。鑒于那個(gè)圖像拾取器是一個(gè)導(dǎo)航控制器,那么通過(guò)支持 UINavigationControllerDelegate 接口,輪廓圖像拾取器能能知道它所要過(guò)渡到的視圖。但如果圖像拾取器通過(guò)應(yīng)用的視圖控制器中顯示,UIKit 會(huì)調(diào)用視圖控制器上的方法。
所以,不能讓?xiě)?yīng)用來(lái)處理導(dǎo)航委托。輪廓圖像拾取器創(chuàng)建一個(gè)代理視圖控制器,它的責(zé)任就是處理導(dǎo)航委托,將調(diào)用轉(zhuǎn)發(fā)給 輪廓圖像拾取器。
在導(dǎo)航委托方法 navigationController:didShowViewController:animated: 中,輪廓圖像拾取器搜索視圖層級(jí),查找拍照按鈕。步驟如下:
檢查是否相機(jī)將要顯示視圖:
if (imagePicker.sourceType == UIImagePickerControllerSourceTypeCamera && !isSnapPhotoTargetAdded && isPresentingCamera)
在底部視圖欄的子視圖列表中的索引 8 處,找到了拍照按鈕視圖:
UIView* bottomBarView = [viewController.view.subviews objectAtIndex:2];
UIButton* buttonView = [bottomBarView.subviews objectAtIndex:8];
刪除處理 UIControlEventTouchUpInside 事件所用的 action 方法:
[buttonView removeTarget:viewController.view action:NULL forControlEvents:UIControlEventTouchUpInside];
為該按鈕添加自定義處理器:
[buttonView addTarget:self action:@selector(takePhoto:) forControlEvents:UIControlEventTouchUpInside];
如此一來(lái),當(dāng)用戶點(diǎn)擊按鈕拍照時(shí),輪廓圖像拾取器的 action 方法 takePhoto: 會(huì)被調(diào)用,圖像拾取器就無(wú)法顯示它的編輯屏幕了。
輪廓圖像拾取器是負(fù)責(zé)呈現(xiàn)自定義編輯屏幕的視圖控制器。它創(chuàng)建了視圖和一些子視圖,實(shí)現(xiàn)了圓形疊加層和圖像,并處理屏幕的按鈕事件。盡管布局這個(gè)屏幕的難度并不十分恐怖,但這其中仍然存在一些布局計(jì)算:顯示圖像并使之居中,使疊加層居中,以及創(chuàng)建滾動(dòng)視圖的 contentInset。
因?yàn)楸疚闹饕虢榻B的是如何整合這一對(duì)象與圖像拾取器。所以關(guān)于細(xì)節(jié)方面的東西就留待讀者下載源碼去深入研究了。
過(guò)渡到編輯屏幕的方法有三種:
在這一用例下,應(yīng)用請(qǐng)求 輪廓圖像拾取器編輯已有圖像,利用 presentEditScreen:withImage: 方法所體現(xiàn)的模式顯示風(fēng)格來(lái)顯示編輯屏幕。
/* presentEditScreen: presents the move and scale window for a supplied image. This use case is for when all that's required is to crop an image not to select one from the camera or photo album before cropping.
*/
/* 注釋解釋——presentEditScreen: 為使用的圖像顯示移動(dòng)與縮放窗口。這一用例適用于萬(wàn)事俱備,只等裁切圖像的情況,而不是還要先從相機(jī)或相簿中獲取圖像然后再裁切的情況。
*/
-(void)presentEditScreen:(UIViewController* _Nonnull)vc withImage:(UIImage* _Nonnull)image{
isDisplayFromPicker = isPresentingCamera = NO;
imageToEdit = image;
presentingVC = vc;
self.modalPresentationStyle = UIModalPresentationFullScreen;
[presentingVC presentViewController:self animated:YES completion:nil];
}
當(dāng)應(yīng)用請(qǐng)求 輪廓圖像拾取器顯示相簿選擇時(shí),會(huì)將圖像拾取器的 sourceType 屬性設(shè)置為 UIImagePickerControllerSourceTypePhotoLibrary。輪廓圖像拾取器并不依賴圖像拾取器去顯示自己的編輯屏幕,所以會(huì)將圖像拾取器的 allowsEditing 屬性設(shè)為 NO。
當(dāng)用戶選擇了一副圖像時(shí),圖像拾取器會(huì)調(diào)用輪廓圖像拾取器控制器上的委托方法 imagePickerController:didFinishPickingMediaWithInfo。輪廓圖像拾取器引用圖像,調(diào)用它的 editImage 方法來(lái)顯示帶有該圖像的編輯屏幕。
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage* tempImage = [info objectForKey:UIImagePickerControllerOriginalImage];
[self editImage:tempImage];
}
因?yàn)閳D像拾取器是一個(gè)導(dǎo)航控制器,所以它會(huì)將編輯屏幕推入堆棧,以便支持取消導(dǎo)航。
[imagePicker pushViewController:self animated:NO]
除非導(dǎo)航欄被隱藏,否則它將顯示在編輯屏幕上。在推動(dòng)它之前,會(huì)設(shè)置屬性來(lái)隱藏該欄。
[imagePicker setNavigationBarHidden:YES]
editImage 完整實(shí)現(xiàn)如下所示:
/* editImage: 利用輸入圖像對(duì)移動(dòng)并縮放視圖進(jìn)行初始化,并將視圖顯示出來(lái)。只有當(dāng)用戶獲取或選擇圖像后,才能從 `presentCamera` 和 `presentPhotoPicker` 中調(diào)用它。
*/
-(void)editImage:(UIImage*)image {
imageToEdit = image;
self.modalPresentationStyle = UIModalPresentationFullScreen;
[imagePicker setNavigationBarHidden:YES];
[imagePicker pushViewController:self animated:NO];
}
跟從相簿中選擇圖像相似,為了顯示相機(jī),將 sourceType 設(shè)為 UIImagePickerControllerSourceTypeCamera。當(dāng)拍照后,輪廓圖像拾取器就會(huì)調(diào)用 editImage 顯示編輯屏幕。
為了防止圖像拾取器顯示編輯屏幕,輪廓圖像拾取器取代了圖像拾取器的 action 方法,但這樣一來(lái)也就無(wú)法。輪廓圖像拾取器應(yīng)該將設(shè)備中的圖像傳輸?shù)较鄼C(jī)視圖中。
Profile picker has responsibility for transferring the image in the camera’s view from the device.
如果你們對(duì)其中的算法感興趣的話,可以自己研究一下這些代碼,如果發(fā)現(xiàn)其中有問(wèn)題,請(qǐng)不吝賜教。具體來(lái)說(shuō),可以看看 MMSProfileImagePicker.m 文件中的下列方法:
prepareToCaptureStillImage: 用來(lái)創(chuàng)建 AVCaptureSession 并對(duì)其進(jìn)行初始化。 captureStillImage: 負(fù)責(zé)將從相機(jī)視圖中的圖像到數(shù)據(jù)緩沖的傳輸過(guò)程進(jìn)行初始化。 cameraConnection: 找到相機(jī)的 AVCaptureConnection 并返回它。 getCameraDevice: 返回iPhone 背蓋后相機(jī)的 AVCaptureDevice。 captureInpute: 將設(shè)備的 AVCaptureDeviceInput 添加到會(huì)話中。 captureStillImage 方法獲取相機(jī)圖像,并利用收到的圖像調(diào)用 editImage。
在獲取圖像過(guò)程中出現(xiàn)了一個(gè)問(wèn)題。在第一次實(shí)現(xiàn)時(shí),圖像質(zhì)量很低:比起利用相機(jī)應(yīng)用獲取的圖像而言,它顯得很暗。
我上網(wǎng)查了查,有一個(gè)方法建議:在開(kāi)啟 AVCaptureSession 后,要延遲調(diào)用 captureStillImageAsynchronouslyFromConnection。插入一個(gè) 0.75 秒的延遲時(shí)間就能解決這個(gè)問(wèn)題。我覺(jué)得肯定是因?yàn)槿鄙偈裁丛O(shè)置才導(dǎo)致了這種問(wèn)題。雖然插入一個(gè)原因尚不清楚的延遲能把問(wèn)題解決,但這很不可靠。我暫時(shí)還沒(méi)有別的辦法,它也暫時(shí)還能用,但如果你們找到了其他的辦法,一定要給我留個(gè)言說(shuō)一說(shuō)。
要想讓你的應(yīng)用模擬原生的通訊錄應(yīng)用上的某些功能,類就必須支持三個(gè)公共接口:
調(diào)用 presentEditScreen: 方法來(lái)編輯應(yīng)用中已獲得的圖像。
-(void)presentEditScreen:(UIViewController* _Nonnull)vc withImage:(UIImage* _Nonnull)image;
調(diào)用 selectFromPhotoLibrary: 方法從相簿中選擇圖像:
-(void)selectFromPhotoLibrary:(UIViewController* _Nonnull)vc;
調(diào)用 selectFromCamera: 方法通過(guò)拍照來(lái)選擇圖像。
-(void)selectFromCamera:(UIViewController* _Nonnull)vc;
為了接收并編輯圖像,應(yīng)用必須在顯示profile拾取器的視圖控制器上實(shí)現(xiàn) MMSProfileImagePickerDelegate(輪廓圖像拾取器委托)委托方法。在調(diào)用預(yù)期方法來(lái)選擇圖像前,先要設(shè)置 輪廓圖像拾取器的 delegate 屬性。
- (IBAction)moveAndScale{
profilePicker = [[MMSProfileImagePicker alloc] init];
profilePicker.delegate = self;
[profilePicker presentEditScreen:self withImage:originalImage];
}
輪廓圖像拾取器委托是一個(gè)跟 UIImagePickerControllerDelegate (圖像拾取器委托)類似的接口,目的也比較相像:接收選擇并被編輯的照片。在退出該方法之前,應(yīng)用應(yīng)該先清除輪廓圖像拾取器。
-(void)mmsImagePickerController:(MMSProfileImagePicker *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
cropImage = [info objectForKey:UIImagePickerControllerEditedImage];
originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
self.circleImageView.image = cropImage;
self.squareImageView.image = cropImage;
self.btnAdd.hidden = YES;
self.circleImageView.hidden = NO;
self.btnEdit.hidden = NO;
[picker dismissViewControllerAnimated:YES completion:nil];
}
字典參數(shù)支持與圖像拾取器委托相同的實(shí)現(xiàn)中所定義的所有編輯信息鍵。關(guān)于編輯信息鍵,請(qǐng)參看相關(guān)的iOS 開(kāi)發(fā)者文檔。
另一方面,用戶可能已經(jīng)選擇圖像,然后決定退出操作。輪廓圖像拾取器調(diào)用委托方法 mmsImagePickerControllerDidCancel:。應(yīng)用應(yīng)該調(diào)用 dismissModalViewControllerAnimated: 來(lái)解除 輪廓圖像拾取器。
-(void)mmsImagePickerControllerDidCancel:(MMSProfileImagePicker *)picker {
[self dismissViewControllerAnimated:YES completion:nil];
}
在軟件開(kāi)發(fā)中,解決問(wèn)題的最佳辦法幾乎是不存在的。的確,我們都有各自的強(qiáng)有力的觀點(diǎn)。然而,還是存在著好辦法、更好的辦法、不同的辦法、糟糕的辦法這些差別。如果你能看到應(yīng)用的核心源碼,你就會(huì)從中看到上述所有這些辦法的樣板。競(jìng)爭(zhēng)約束、團(tuán)隊(duì)文化、開(kāi)發(fā)經(jīng)驗(yàn)以及個(gè)人風(fēng)格……這些因素都能影響我們的解決方案。
寫(xiě)作本文的目的在于展示一個(gè)問(wèn)題的不同解決方案。在開(kāi)發(fā)一個(gè)應(yīng)用時(shí),我力圖首先挖掘架構(gòu)本身的能力,寫(xiě)最少的代碼,在創(chuàng)建可用的組件或工具時(shí),注重業(yè)務(wù)邏輯的開(kāi)發(fā),在可行的情況下使用第三方庫(kù)。
我原打算 Cocoa Touch 架構(gòu)可以為這個(gè)例子提供一些方案,但我發(fā)現(xiàn)它并不支持現(xiàn)有的功能。但是,它似乎可以擴(kuò)展疊加層支持功能。所以為了少寫(xiě)代碼的目的,我還是研究了一下那個(gè)方法。
結(jié)果我發(fā)現(xiàn)這種方法很不完善,而且不靈活。由于為此花了不少時(shí)間,我覺(jué)得自己應(yīng)該能寫(xiě)出自己的編輯屏幕,然后只要將它配置好即可。但由于沒(méi)有經(jīng)驗(yàn),還是一頭撞了南墻,我發(fā)現(xiàn)根本不可能那么簡(jiǎn)單。當(dāng)我解決好一個(gè)問(wèn)題之后,就像剝洋蔥似的,另一個(gè)新問(wèn)題馬上蹦了出來(lái),最終我找到本文所說(shuō)的這種方法。
希望這篇文章能讓你知道這種問(wèn)題的解決方法,以及一些在其他項(xiàng)目中可能會(huì)用到的一些技術(shù)。如果你非常喜歡這個(gè)功能,完全可以把它用到你自己的應(yīng)用中,那就太好不過(guò)了。當(dāng)然,如果能為這個(gè)小組件提出改善方案就更好了。
在 cocoapods.org上可以找到該類。找到 MMSProfileImagePicker 并使用它吧。如果你想找到代碼中的缺陷或加以改進(jìn),可以在 Github 倉(cāng)庫(kù)上找到這些代碼。
祝你編程愉快!