离线下载
PDF版 ePub版

极客学院团队出品 · 更新于 2017-09-25 14:00:37

介绍用来选择轮廓图像的一个类——Will J Miller

译者:李鑫

原文:A Class for Selecting a Profile Image

本文为极客学院Wiki组织翻译,转载请注明出处。

时间:2016.3.14

文章介绍如何使用 UIImagePickerController 重制通讯录应用中常见的轮廓图像选择功能。其中涉及到了很多问题。该文展示了一个类如何使用 UIImagePickerController 来复制这种功能。

简介

如果想按照通讯录应用中那种功能来选择并编辑图像,似乎应该采用 UIImagePickerController 类(图像拾取器)。它似乎甚至还能完美地对编辑屏幕叠加层进行自定义。但遗憾的是,事实证明它是不可改变的。复制这些功能让我付出了常人难以想象的困难。

本文介绍了 MMSProfileImagePicker 类(轮廓图像拾取器),它可以进行图像选择和编辑,跟通讯录应用中的功能是等同的。

有些人已经解决了这个问题,并把他们的类放到了开发者社区中进行分享。所以你肯定会想,你的方案又有什么新意呢?很简单,我只想通过这篇文章来展示一些技巧,你有可能会在解决其他难题时用上它。

借助于图像拾取器,这种解决方案支持了这些功能。本文主要考虑与之集成的一些技术,以及如何在你自己的应用中使用 轮廓图像拾取器。可下载到的范例文件中已经实现了一个等同于通讯录应用中选择图像的功能。

范例应用

图 1 -范例应用

探索过程

在探索如何使用图像拾取器的叠加特性时,我遇到了一些令人困惑的问题,比如说,如何正确定位叠加并改变它的大小?如何在用相机选择图像时,让圆形只显示在编辑屏幕呢?以及如何按照 z 顺序正确定位圆形叠加层呢?当然,这些只是我现在能想到的一部分问题而已。

有些问题当时没有解决方案,有些则很复杂,有些则跟类的内部实现结合得过于紧密。比如说,stackoverflow 上的这个解法通过操纵图像拾取器创建的私有视图来显示圆形叠加。

这种方法无疑是很聪明,但它容易受到将来 iOS 版本更新的影响。而且,它只适用于选择一副图片的情况,而不适用于拍照并编辑一副图片的情况。换句话说就是:不可能在显示相机时拦截导航委托调用,从而在移动并缩放屏幕(图像编辑屏幕)出现之前插入叠加层。

要解决的问题太多,限于本文篇幅,就不赘述了。关于如何裁切位图,请参考我之前的文章裁切图像的 View 类。那里介绍到了这里所使用的 UIImage+Cropping 类别。

注意,我没有用到任何开源解决方案,如有雷同,实属巧合。

解决方法

我的策略就是,尽量发掘图像拾取器的功能,因为它包含着大部分我所需要的功能。不幸的是,有些类的功能必须重新实现才能完美复制通讯录应用中的相关功能。其中的一个挑战就是如何将圆形叠加层只显示在编辑屏幕上。在配置为显示相机时,叠加层会显示在获取图像屏幕和编辑屏幕上。结果,解决这个问题时,会需要对之前图像拾取器已经解决过的问题进行另一种考虑。

为了解决这种需求,我最终的结论是:最好是让图像拾取器负责呈现相机以及从相簿中选择图像这两种屏幕,让新的类负责创建并显示图像编辑屏幕。

这种方法还需要解决以下这些复杂问题:

  • 如何防止图像拾取器呈现图像编辑屏幕?
  • 如何从图像拾取器过渡到编辑屏幕?
  • 如何从图像拾取器抓取相机图像然后转换到编辑屏幕?

禁用图像拾取器的编辑屏幕

这种解决方法彻底重建了编辑屏幕功能,而不采用图像拾取器的编辑屏幕。当配置为从相机中选择图像时,图像拾取器会在获取图像和编辑屏幕上显示 cameraOverlay 属性。这一方法提供的解决方案可防止圆形叠加层显示在相机的图像获取屏幕上。

要想在“从相簿中选择”屏幕中显示“自定义编辑”屏幕,所用的配置十分简单。图像拾取器的一个属性可防止编辑屏幕的呈现。将 allowsEditing 属性设为 NO,就会返回用户的选择,但不会显示它。这就为显示自定义编辑屏幕提供了一种非常简单的方法。

遗憾的是,当配置为相机选择时,图像拾取器并不按照这一属性来做,依然显示编辑屏幕。这下事情就变得棘手了。我可不想重写一遍相机功能,但目前也没有任何现成的方法来禁用它,而且说归到底,我还是不想放弃这种方法。

如果我能找到一种方法让图像拾取器调用我的 action 方法(而不是它自己的),那么 轮廓图像拾取器就会防止编辑屏幕呈现出来。鉴于那个图像拾取器是一个导航控制器,那么通过支持 UINavigationControllerDelegate 接口,轮廓图像拾取器能能知道它所要过渡到的视图。但如果图像拾取器通过应用的视图控制器中显示,UIKit 会调用视图控制器上的方法。

所以,不能让应用来处理导航委托。轮廓图像拾取器创建一个代理视图控制器,它的责任就是处理导航委托,将调用转发给 轮廓图像拾取器。

在导航委托方法 navigationController:didShowViewController:animated: 中,轮廓图像拾取器搜索视图层级,查找拍照按钮。步骤如下:

检查是否相机将要显示视图:

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];

如此一来,当用户点击按钮拍照时,轮廓图像拾取器的 action 方法 takePhoto: 会被调用,图像拾取器就无法显示它的编辑屏幕了。

过渡到编辑屏幕

轮廓图像拾取器是负责呈现自定义编辑屏幕的视图控制器。它创建了视图和一些子视图,实现了圆形叠加层和图像,并处理屏幕的按钮事件。尽管布局这个屏幕的难度并不十分恐怖,但这其中仍然存在一些布局计算:显示图像并使之居中,使叠加层居中,以及创建滚动视图的 contentInset

因为本文主要想介绍的是如何整合这一对象与图像拾取器。所以关于细节方面的东西就留待读者下载源码去深入研究了。

过渡到编辑屏幕的方法有三种:

  • 应用显示一个已有图像,用于编辑。
  • 从相簿中选择一副图像。
  • 相机拍照,抓取一副图像。

显示已有图像

在这一用例下,应用请求 轮廓图像拾取器编辑已有图像,利用 presentEditScreen:withImage: 方法所体现的模式显示风格来显示编辑屏幕。

/* 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: 为使用的图像显示移动与缩放窗口。这一用例适用于万事俱备,只等裁切图像的情况,而不是还要先从相机或相簿中获取图像然后再裁切的情况。

 */

-(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];

}

从相簿中选择一副图像

当应用请求 轮廓图像拾取器显示相簿选择时,会将图像拾取器的 sourceType 属性设置为 UIImagePickerControllerSourceTypePhotoLibrary。轮廓图像拾取器并不依赖图像拾取器去显示自己的编辑屏幕,所以会将图像拾取器的 allowsEditing 属性设为 NO

当用户选择了一副图像时,图像拾取器会调用轮廓图像拾取器控制器上的委托方法 imagePickerController:didFinishPickingMediaWithInfo。轮廓图像拾取器引用图像,调用它的 editImage 方法来显示带有该图像的编辑屏幕。

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

    UIImage* tempImage = [info objectForKey:UIImagePickerControllerOriginalImage];

    [self editImage:tempImage];
}

因为图像拾取器是一个导航控制器,所以它会将编辑屏幕推入堆栈,以便支持取消导航。

[imagePicker pushViewController:self animated:NO]

除非导航栏被隐藏,否则它将显示在编辑屏幕上。在推动它之前,会设置属性来隐藏该栏。

[imagePicker setNavigationBarHidden:YES]

editImage 完整实现如下所示:

/* editImage: 利用输入图像对移动并缩放视图进行初始化,并将视图显示出来。只有当用户获取或选择图像后,才能从 `presentCamera` 和 `presentPhotoPicker` 中调用它。
 */

-(void)editImage:(UIImage*)image {    

    imageToEdit = image;

    self.modalPresentationStyle = UIModalPresentationFullScreen;

    [imagePicker setNavigationBarHidden:YES];

    [imagePicker pushViewController:self animated:NO];

}

从相机中选择一副图像

跟从相簿中选择图像相似,为了显示相机,将 sourceType 设为 UIImagePickerControllerSourceTypeCamera。当拍照后,轮廓图像拾取器就会调用 editImage 显示编辑屏幕。

抓取相机图像

为了防止图像拾取器显示编辑屏幕,轮廓图像拾取器取代了图像拾取器的 action 方法,但这样一来也就无法。轮廓图像拾取器应该将设备中的图像传输到相机视图中。
Profile picker has responsibility for transferring the image in the camera’s view from the device.

如果你们对其中的算法感兴趣的话,可以自己研究一下这些代码,如果发现其中有问题,请不吝赐教。具体来说,可以看看 MMSProfileImagePicker.m 文件中的下列方法:

  • prepareToCaptureStillImage: 用来创建 AVCaptureSession 并对其进行初始化。
  • captureStillImage: 负责将从相机视图中的图像到数据缓冲的传输过程进行初始化。
  • cameraConnection: 找到相机的 AVCaptureConnection 并返回它。
  • getCameraDevice: 返回iPhone 背盖后相机的 AVCaptureDevice。
  • captureInpute: 将设备的 AVCaptureDeviceInput 添加到会话中。

captureStillImage 方法获取相机图像,并利用收到的图像调用 editImage

在获取图像过程中出现了一个问题。在第一次实现时,图像质量很低:比起利用相机应用获取的图像而言,它显得很暗。

我上网查了查,有一个方法建议:在开启 AVCaptureSession 后,要延迟调用 captureStillImageAsynchronouslyFromConnection。插入一个 0.75 秒的延迟时间就能解决这个问题。我觉得肯定是因为缺少什么设置才导致了这种问题。虽然插入一个原因尚不清楚的延迟能把问题解决,但这很不可靠。我暂时还没有别的办法,它也暂时还能用,但如果你们找到了其他的办法,一定要给我留个言说一说。

使用 MMSProfileImagePicker

要想让你的应用模拟原生的通讯录应用上的某些功能,类就必须支持三个公共接口:

调用 presentEditScreen: 方法来编辑应用中已获得的图像。

-(void)presentEditScreen:(UIViewController* _Nonnull)vc withImage:(UIImage* _Nonnull)image;

调用 selectFromPhotoLibrary: 方法从相簿中选择图像:

-(void)selectFromPhotoLibrary:(UIViewController* _Nonnull)vc;

调用 selectFromCamera: 方法通过拍照来选择图像。

-(void)selectFromCamera:(UIViewController* _Nonnull)vc;

为了接收并编辑图像,应用必须在显示profile拾取器的视图控制器上实现 MMSProfileImagePickerDelegate(轮廓图像拾取器委托)委托方法。在调用预期方法来选择图像前,先要设置 轮廓图像拾取器的 delegate 属性。

- (IBAction)moveAndScale{

    profilePicker = [[MMSProfileImagePicker alloc] init];

    profilePicker.delegate = self;

    [profilePicker presentEditScreen:self withImage:originalImage];

}

轮廓图像拾取器委托是一个跟 UIImagePickerControllerDelegate (图像拾取器委托)类似的接口,目的也比较相像:接收选择并被编辑的照片。在退出该方法之前,应用应该先清除轮廓图像拾取器。

-(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];

} 

字典参数支持与图像拾取器委托相同的实现中所定义的所有编辑信息键。关于编辑信息键,请参看相关的iOS 开发者文档

另一方面,用户可能已经选择图像,然后决定退出操作。轮廓图像拾取器调用委托方法 mmsImagePickerControllerDidCancel:。应用应该调用 dismissModalViewControllerAnimated: 来解除 轮廓图像拾取器。

-(void)mmsImagePickerControllerDidCancel:(MMSProfileImagePicker *)picker {

    [self dismissViewControllerAnimated:YES completion:nil];

}

总结

在软件开发中,解决问题的最佳办法几乎是不存在的。的确,我们都有各自的强有力的观点。然而,还是存在着好办法、更好的办法、不同的办法、糟糕的办法这些差别。如果你能看到应用的核心源码,你就会从中看到上述所有这些办法的样板。竞争约束、团队文化、开发经验以及个人风格……这些因素都能影响我们的解决方案。

写作本文的目的在于展示一个问题的不同解决方案。在开发一个应用时,我力图首先挖掘架构本身的能力,写最少的代码,在创建可用的组件或工具时,注重业务逻辑的开发,在可行的情况下使用第三方库。

我原打算 Cocoa Touch 架构可以为这个例子提供一些方案,但我发现它并不支持现有的功能。但是,它似乎可以扩展叠加层支持功能。所以为了少写代码的目的,我还是研究了一下那个方法。

结果我发现这种方法很不完善,而且不灵活。由于为此花了不少时间,我觉得自己应该能写出自己的编辑屏幕,然后只要将它配置好即可。但由于没有经验,还是一头撞了南墙,我发现根本不可能那么简单。当我解决好一个问题之后,就像剥洋葱似的,另一个新问题马上蹦了出来,最终我找到本文所说的这种方法。

希望这篇文章能让你知道这种问题的解决方法,以及一些在其他项目中可能会用到的一些技术。如果你非常喜欢这个功能,完全可以把它用到你自己的应用中,那就太好不过了。当然,如果能为这个小组件提出改善方案就更好了。

cocoapods.org上可以找到该类。找到 MMSProfileImagePicker 并使用它吧。如果你想找到代码中的缺陷或加以改进,可以在 Github 仓库上找到这些代码。

祝你编程愉快!