使 ZCImagePickerController 支持预览功能

来源:互联网 发布:mysql if 编辑:程序博客网 时间:2024/05/10 14:25

需求

ZCImagePickerController 支持图片多选,但无法在选图时预览大图。我们需要修改这个框架,让它支持预览大图功能。

准备图片资源

我们需要修改4张图片:

你可以打开这些图片看看。你会发现这是用于覆盖在 cell 上的透明图片,右下角有一个 checkmark 标志表示 cell 的选中状态。但它的大小占据了整个 cell:

这会导致整个 cell 都被这张透明图片遮住。这不是我们需要的。我们需要的是让它只保留右下角的1/4 的内容(即 checkmark 所在的位置),其余 3/4 我们需要截去。
请使用图片处理软件解决这个问题。将这4张图片做同样的处理,修改画布,将它们除了右下角之外的 3/4 面积裁切掉。

然后编辑每张图片,使用滤镜中的 color controls 功能,将颜色饱和度设为0,亮度调整为 0.43,如下图所示;

将编辑结果另存为其它名字,比如 SelectionOverlay1@2x.png,然后将图片拖到项目导航窗口中。4张图片处理完,将在项目中增加4张新图片,分别名为(我们在原文件名的结尾加上一个1):

修改 ZCAsset 类

修改接口

打开 ZCAsset.h,在ZCAssetProtocal协议声明中增加一个方法:

- (void)assetDidTouchDown:(ZCAsset*)asset;// MARK: yhy added

原来的协议方法 assetDidSelect: 方法用于处理当用户在 cell 右下角 1/4 区域内的点击,而新增的协议方法则用于处理用户在 cell 除右下角 1/4 区域之外的点击。

在类的 interface 中增加一个实例变量:

@interface ZCAsset : UIControl{    BOOL isSelect;// MARK: yhy added}

修改 initWithAsset:selected: 方法

打开 ZCAsst.m。在 initWithAsset: selected: 方法中,将 _selectionOverlayLayer.frame = thumbFrame; 一句修改为:

_selectionOverlayLayer.frame = CGRectMake(CGRectGetMidX(thumbFrame), CGRectGetMidY(thumbFrame), CGRectGetWidth(thumbFrame)/2.0, CGRectGetHeight(thumbFrame)/2.0);// MARK: yhy edit was:thumbFrame;

这样,使得原来的 checkmark 图片覆盖 cell 右下角 1/4 的位置。

然后注释后面的一段代码,这些代码我们将移植到 toggleSelection 方法中:

//        NSString *imageName = [ZCHelper isiOS7orLater] ? @"SelectionOverlay~iOS71" : @"SelectionOverlay1";//        if (![NSThread isMainThread]) {//            dispatch_async(dispatch_get_main_queue(), ^{//                _selectionOverlayLayer.contents = (id)[UIImage imageNamed:imageName].CGImage;//            });//        }//        else {//            _selectionOverlayLayer.contents = (id)[UIImage imageNamed:imageName].CGImage;//        }//        _selectionOverlayLayer.hidden = !selected;

再注释这一句(我们会用手势识别器来代替 action):

//[self addTarget:self action:@selector(toggleSelection) forControlEvents:UIControlEventTouchUpInside];

并添加如下代码:

[self setSelected:selected];UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapHandler:)];[self addGestureRecognizer:tapGesture];

先初始化 selected 属性,然后再用我们的 tap 手势识别器取代原来的 action。

实现 selected 属性

首先需要实现 selected 属性的两个指定的 setter/getter 方法:

- (BOOL)isSelected {    return isSelect;}- (void)setSelected:(BOOL)selected {    isSelect = !selected;    [self toggleSelection];}

在 setter/getter 方法中,我们使用了实例变量 isSelect 来保存属性的值。同时当属性值被修改时,我们会调用 toggleSelection 方法。这里你可能会奇怪,在调用 toggleSelection 方法之前为什么会是 isSelect = !selected 而不是 isSelect = selected。

这是因为 toggleSelection 方法中也有一句 isSelect = !selected,这样就会把原来的 isSelect 值又变回 selected 而不是 !select 了。

toggleSelection 方法

接下来看 toggleSelection 方法的实现:

- (void)toggleSelection {    isSelect = !isSelect; // 1    NSString *normalImage = [ZCHelper isiOS7orLater] ? @"SelectionOverlay~iOS71" : @"SelectionOverlay1"; // 2    NSString *selImage = [ZCHelper isiOS7orLater] ? @"SelectionOverlay~iOS7" : @"SelectionOverlay"; // 3    NSString* imageName = isSelect ? selImage : normalImage; //4    if (![NSThread isMainThread]) { //5        dispatch_async(dispatch_get_main_queue(), ^{            _selectionOverlayLayer.contents = (id)[UIImage imageNamed:imageName].CGImage;        });    }else {        _selectionOverlayLayer.contents = (id)[UIImage imageNamed:imageName].CGImage;    }    if ([self.delegate respondsToSelector:@selector(assetDidSelect:)]) { //6        [self.delegate assetDidSelect:self];    }}
  1. 如方法名所暗示的,这个方法会将 isSelect 布尔值来回切换,如果原来是 YES,将它改成 NO,如果原来是 NO 的则改为 YES。
  2. 根据 iOS 版本选择相应的图片,用于表示 cell 未选中的状态。
  3. 根据 iOS 版本选择相应的图片,用于表示 cell 选中时的状态。
  4. 根据 isSelect 值判断当前需要显示的是选中状态,还是未选中状态。
  5. 在主线程中将 _selectionOverlayLayer 中的图片内容应用为指定图片。
  6. 调用委托对象的 assetDidSelect: 方法,表明用户在点击 cell 右下角的 1/4 区域。

tapHandler 方法

手势识别器会触发 tapHandler 方法。tapHandler 方法实现如下:

-(void)tapHandler:(UITapGestureRecognizer*)gesture{    CALayer* layerThatWasTapped = [_selectionOverlayLayer hitTest:[gesture locationInView:self]]; // 1    if(layerThatWasTapped == _selectionOverlayLayer){ // 2        [self toggleSelection];    }else{ // 3        if([self.delegate respondsToSelector:@selector(assetDidTouchDown:)]){            [self.delegate assetDidTouchDown:self];        }    }}

在这个方法中,我们会判断用户点击的区域是 cell 右下角的 1/4 区域(即 _selectionOverlayLayer 这个 CALayer),还是其他地方。

  1. CALayer 的 hitTest 方法用于测试 UIView 中的某个点是否位于 layer 的 subLayer 中,并返回最远的那个 subLayer。但我们知道,_selectionOverlayLayer 是一个单层的 CALayer,它没有任何 subLayer。因此,如果触摸点位于 _selectionOverlayLayer 中,则返回结果只会是 _selectionOverlayLayer。
  2. 如果触摸点切实位于 _selectionOverlayLayer(通过 hitTest 方法返回值判断),则我们出发 toggleSelection 方法,表示用户想要的是一个选择/清除选择的动作。
  3. 否则,用户是想要一个预览全屏大图的动作,因此我们会调用委托方法 assetDidTouchDown:,将处理过程转移到委托对象(ZCAssetTablePicker)来处理。

修改 ZCAssetTablePicker 类

修改接口

打开 ZCAssetTablePicker.m,增加两个实例变量声明:

@interface ZCAssetTablePicker (){    UIView *_vDimmed; // MARK: yhy added    UIImageView *_ivPreview; // MARK: yhy added}

_vDimmed 是一个黑色半透明的 UIView,我们用于显示全屏预览图时作为背景的遮罩层。
_ivPreview 是一个 UIImageView,用于显示全屏预览图。

实现 ZCAssetProtocal 协议中的新方法

我们需要实现先前我们在 ZCAssetProtocal 协议中增加的新方法,这个方法负责处理当用户想查看大图而不是选择/反选 cell 的点击:

- (void)assetDidTouchDown:(ZCAsset *)asset{    if (_vDimmed == nil) { // 1        _vDimmed = [[UIView alloc]initWithFrame:CGRectZero];        _vDimmed.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin |            UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin |            UIViewAutoresizingFlexibleWidth;        _vDimmed.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.7];        [self.view addSubview:_vDimmed];         _vDimmed.alpha = 0.0;        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapOnDimmedView:)];        [_vDimmed addGestureRecognizer:tap];    }    // _vDimmed 的位置必须跟随 tableView 滚动而滚动    _vDimmed.frame = CGRectMake(0, self.tableView.contentOffset.y, CGRectGetWidth(self.tableView.frame), CGRectGetHeight(self.tableView.frame));    [self.view bringSubviewToFront:_vDimmed]; // 2    // NSLog(@"view.frame = %@",[self printFrame:self.view.frame]);     // NSLog(@"_vDimmed.frame = %@",[self printFrame:_vDimmed.frame]);    _ivPreview = [[UIImageView alloc] initWithFrame:_vDimmed.bounds]; // 3    _ivPreview.contentMode = UIViewContentModeScaleAspectFit;    _ivPreview.autoresizingMask = _vDimmed.autoresizingMask;    [_vDimmed addSubview:_ivPreview];    CGImageRef ref=[asset.asset.defaultRepresentation fullResolutionImage]; // 4    UIImage* img=[[UIImage alloc]initWithCGImage:ref]; // 5    _ivPreview.image = img; // 6    // add gesture for close preview    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPanToClosePreview:)]; // 7    [_vDimmed addGestureRecognizer:pan];    [UIView animateWithDuration:0.2 animations:^(void) { // 8        _vDimmed.alpha = 1.0;    }]; }
  1. 如果 _vDimmed 还没初始化,则初始化 _vDimmed,并设置它的属性,为它添加一个单击手势识别器(这样当用户点击 _vDimmed 时它会隐藏)。
  2. 将 _vDimmed 放到 view 最上层。
  3. 初始化 _ivPreview,并设置它的属性,使它根据能根据自己的内容自动调整大小(保持宽高比)。
  4. 通过 ALAsset 的 defaultRepresentation 属性获得全屏图片,这是一个 CGImageRef 对象,我们需要转换成 UIImage。
  5. CGImageRef 转换成 UIImage。
  6. 将 UIImage 赋给 _ivPreview 的 image 属性,以显示图片的内容。
  7. 然后在 _vDimmed 上添加一个滑动手势识别器,并调用 onPanToClosePreview: 方法进行处理。
  8. 增加一个淡出动画。

onTapOnDimView 方法

这个方法在用户点击 _vDimmed 时触发,这表明用户想退出图片的预览,因此我们简单调用了 hidePreview (后面实现)来隐藏 _vDimmed:

- (void)onTapOnDimmedView:(UITapGestureRecognizer *)tap{    if (tap.state == UIGestureRecognizerStateEnded)    {        if (_ivPreview != nil)            [self hidePreview];    }}

onPanToClosePreview 方法

这个方法当用户在 _vDimmed 上滑动手指时触发,这时我们可以给用户一个更炫的“飞出”效果,并隐藏 _vDimmed:

- (void)onPanToClosePreview:(UIPanGestureRecognizer *)gestureRecognizer{    if(gestureRecognizer.numberOfTouches <= 1){        CGPoint translation = [gestureRecognizer translationInView:self.view];        CGPoint center = CGPointMake(CGRectGetMidX(_vDimmed.bounds),CGRectGetMidY(_vDimmed.bounds));        if (gestureRecognizer.state == UIGestureRecognizerStateEnded) // 1        {            [UIView animateWithDuration:0.2 animations:^(void) {                if (_vDimmed.alpha < 0.6)   // 2                {                    CGPoint pt = _ivPreview.center;                    if (_ivPreview.center.y > _vDimmed.center.y) // 3                        pt.y = self.view.frame.size.height * 1.5;                    else if (_ivPreview.center.y < _vDimmed.center.y) // 4                        pt.y = -self.view.frame.size.height * 1.5;                    _ivPreview.center = pt;                    [self hidePreview];                }                else // 5                {                    _vDimmed.alpha = 1.0;                    _ivPreview.center = center;                    NSLog(@"_vDimmed.center.y:%f",_vDimmed.center.y);                }            }];        }        else // 6        {            _ivPreview.center = CGPointMake(_ivPreview.center.x, _ivPreview.center.y + translation.y);            _vDimmed.alpha = 1 - ABS(_ivPreview.center.y-center.y) / (center.y);            NSLog(@"_vDimmed's alpha= 1-ABS(%f-%f)/(%f/2.0)=%f",_ivPreview.center.y,_vDimmed.center.y,_vDimmed.frame.size.height,_vDimmed.alpha);            [gestureRecognizer setTranslation:CGPointMake(0, 0) inView:self.view];        }    }}

这个动画的效果是,先让 _ivPreview.center 向上或向下移动,同时 _vDimmed 渐渐变透明,直至最后 _ivPreview 飞出屏幕,_vDimmed 完全透明。

  1. 如果滑动已经结束,我们就判断 vDimmed.alpha 值是否已经降低到 0.7 以下了。
  2. 如果是,判断 _ivPreview 移动的方向,并根据方向来移出屏幕。
  3. 如果 _ivPreview 是向下运动的,则向下移出屏幕。
  4. 如果 _ivPreview 是向上运动的,则向上移出屏幕。
  5. 如果 vDimmed.alpha 的值在手指滑动结束时还大于 0.7,表明用户滑动手指的距离不够长,可能用户不想关闭预览,因此我们将 _vDimmed 和 _ivPreview 恢复原样。
  6. 如果滑动未结束,则我们需要让 _ivPreview 根据手指的移动来运动(但是只考虑手势 y 坐标,表明只能对上滑、下滑进行反应,向左、右滑动并不会触发动作)。同时让 _vDimmed 的 alpha 值逐渐减小(即淡出效果)。

hidePreview 方法

这个方法很简单,隐藏 _vDimmed 而已:

- (void)hidePreview{    [_ivPreview removeFromSuperview];//1    _ivPreview = nil;    [self.view sendSubviewToBack:_vDimmed];// 2    _vDimmed.alpha = 0.0;    [_vDimmed removeGestureRecognizer:[_vDimmed.gestureRecognizers lastObject]];// 3}
  1. 移除 _ivPreview 并销毁内存。
  2. 将 _vDimmed 放到 view 的下层并置为透明。
  3. 将 pan 手势识别器从 _vDimmed 中移除而 tap 手势识别器仍然保留。因为后者只会添加一次,而前者每次显示 _vDimmed 时都会添加。
0 0
原创粉丝点击