iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

  • 内容
  • 评论
  • 相关

DEMO的github地址:https://github.com/YYProgrammer/YYPhotoBrowserLikeWX


效果如下图


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

效果图.gif


  • 实现图片组的浏览,包含捏合缩放、双击缩放、单击退出、向下拖拽退出等。

  • 重点是“向下拖拽退出”的实现。


架构设计


下文称下图中左边的界面为界面A,右边为界面B


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

A-B.jpeg


界面A只是一个用来测试的界面,界面B才是图片浏览器,架构设计主要针对界面B。主要需要考虑以下几个点:


  • 界面B的结构:N张图片需要左右滑动、图片本身需要缩放、N种手势交互、后期额外控件的添加等。

  • A到B、B到A的转场动画。

  • 重点:向下拖拽的交互怎么实现


界面B的结构


  • N张图片需要左右滑动:必然需要UIScrollView或其子类(UICollectionView),来放所有图片。

  • 图片本身需要缩放:所以图片本身需要一个UIScrollView包装起来用于做缩放。

  • N种手势交互:UIScrollView本身带有很多手势,再往上添加手势不妥,所以应当创建个UIView,UIView里有UIScrollView,手势加在UIView上,例如单击、双击等。

  • 后期额外控件的添加:


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

额外控件.jpeg


例如上图红圈的控件,明显不能添加到UIScrollView中否则就跟着滑走了。


最终设计如下图:


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

结构示意.jpeg


  • 蓝色是个scrollview,里面放图片,这样可以缩放图片。(demo中我把它封装成了YYPhotoBrowserSubScrollView)

  • 绿色是个scrollview,里面放蓝色scrollview,这样可以实现左右滑动翻页。(demo中我把它封装成了YYPhotoBrowserMainScrollView)

  • 红色是个控制器的view,这样做可以随意添加额外控件,而且控制器的话,也方便做界面A-界面B的转场动画(demo中对应YYPhotoBrowserViewController)。

  • 消息传递我采用的代理模式。


转场动画


这里B是modal出的控制器,我把转场效果交给了转场代理transitioningDelegate,转场动画具体做法可以参考文章:http://www.jianshu.com/p/a65d3463f4bc


值得一提的是,拖拽时背景颜色透明,出现A的界面。换句话说,B被present之后,A并没有消失。这需要设置一个属性


B.modalPresentationStyle = UIModalPresentationOverCurrentContext;


重点:向下拖拽的交互的实现


控件简介


结合上图,拖拽事件发生在蓝色这一层。


蓝色是个UIView(YYPhotoBrowserSubScrollView),添加了子控件UIScrollview(demo中对象命名为mainScrollView),mainScrollView的宽高占满了YYPhotoBrowserSubScrollView。mainScrollView中有UIImageView。


双击手势添加给YYPhotoBrowserSubScrollView,双击后改变mainScrollView的zoomScale(缩放比例系数)来实现缩放,单击手势也添加给YYPhotoBrowserSubScrollView,单击后通知代理-绿色控件,绿色再通知红色控制器,控制器退回。


需求介绍


用户向上拖拽时,图片向上移动(即正常的scrollview的滚动效果,结合demo中第一张长图片查看)。


向下拖拽到最顶部并持续向下拖拽时,图片遂手势移动,并变小,背景逐渐透明。松手瞬间,如果手势是向下移动的,B页退出,如果手势是向上移动的,图片回到原来的位置


解决方案分析


那么问题来了,拖拽的交互理所当然动用手势-UIPanGestureRecognizer,添加给谁呢?


首先我们来看一下一个手势发生时,发生了哪些事情:


1、生成一个UIEvent事件;


2、通过事件响应链查找最合适的事件执行者;


3、调用事件执行者绑定的手势事件。


所以,手势所绑定的事件在执行前,会先通过逐级查找的方式,找到最适合响应手势的控件,再执行其方法,流程如下图(白色原点处发生点击)


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

事件响应链.jpeg


图中控件与上面的结构图的控件一致,蓝色线是向下询问过程,橙色是返回结果的过程。


询问过程:


1、application:window你好,我收到一个事件(UIEvent),是在点你身上的,你看一看具体是你哪个孩子(subview)来响应,问好了告诉我。


2、window:我确认了一下,我是可见的(hidden != NO && alpha >= 0.01),我是可点击的(userInteractionEnabled != NO),而且点击的点确实在我身上([self pointInside:point withEvent:event] == YES),那么,我来遍历一下我的孩子们(subview),看看谁最合适,如果没有的话,那就是我了。


3、4、5、同上。


返回过程:


1、UIImageView:我不能被点击。


2、UIScrollview:我的孩子都不能响应,那就是我了。


3、YYPhotoBrowserMainScrollView:UIScrollview可以。


4、5、同上。


最后蓝色那个UIScrollview就成了事件的响应者。


其实这个询问和返回的过程,就是UIView里的方法


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event


按系统流程重写的话,内部过程如下:


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

    // 1.判断当前控件能否接收事件

    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

     

    // 2. 判断点在不在当前控件

    if ([self pointInside:point withEvent:event] == NO) return nil;

     

    // 3.从后往前遍历自己的子控件(后添加的视图在上面,在上面优先响应)

    NSInteger count = self.subviews.count;

    for (NSInteger i = count - 1; i >= 0; i--)

    {

        UIView *childView = self.subviews[i];

        //把当前控件上的坐标系转换成子控件上的坐标系

        CGPoint childP = [self convertPoint:point toView:childView];

        //让subview继续找它的subview

        UIView *fitView = [childView hitTest:childP withEvent:event];

        if (fitView)//寻找到最合适的view

        {

            return fitView;

        }

    }

     

    // 循环结束,表示没有比自己更合适的view

    return self;

}


然后,再调用响应者绑定的对应方法去执行,并且在调用时会将本次触摸相关的等信息装进UIGestureRecognizer里作为参数。


现在,我们来看一下这几个不可行的方案:


  • 添加一个pan手势给UIImageView,在手势事件中判断手势方向,如果使向上拖拽,把事件传递给scrollview,如果是向下,做“向下拖拽退出”交互效果。

    不可行原因:

    要判断手势方向,只能在手势事件中进行(hitTest中无法通过携带的UIEvent参数判断方向),通过方向来改变响应者,而要改变手势响应者,只能在hitTest方法中,但hitTest是在手势事件执行前,所以已经确定了UIImageView为响应者,再想改变响应者,UIImageView就会中断对事件的响应,本次触摸事件就宣告结束,需要再次抬起手指,重新下拉,生成新的事件,这个时候才能让scrollview来响应。

  • 重写scroolview的pan手势的绑定事件。如果是向上拖动,就按系统的交互写,如果是向下,就做“向下拖拽退出”交互效果。

    无论是新建一个pan手势覆盖掉scroolview自带的,还是找到它自带的pan手势打印出它绑定的方法(没记错的话叫handlePan:)然后重写,还是自己用UIView的子类实现一个自定义Scrollview,都算作重写,因为都要自己实现系统的交互效果。

    不可行原因:

    理论上不是不可行,是非常难。因为系统交互上,不只是单纯的手指移动,内容跟着动,还有:例如,快速滑动页面后松手,页面会持续滑动并以一个加速度做系数来减速直到停止,这个加速度怎么算?又如:内容滚到顶部后持续下拉再松手,会像弹簧一样弹回去,回去的速度是变化的,这里的加速度又是多少?


可行方案


思考题做到这里,其实答案已经很接近了。


所有拖动的交互都离不开pan手势UIPanGestureRecognizer,所以UIScrollview既然能在手指移动时做事儿,那它也离不开UIPanGestureRecognizer。


翻看UIScrollview的.h文件不难发现,它其实已经暴露了它的pan手势(不暴露就runtime遍历属性,总能找到想要的)。


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

h文件.jpeg


上文说到,调用手势绑定事件时,会把触摸相关的信息放进手势对象里,所以手指在scrollview里移动时,就能从它的panGestureRecognizer拿到我们想要的信息:手指移动路径,就能根据这些数据,做想要的效果。


所以方案如下:当scrollview在顶部并向下拖拽时,隐藏scrollview中本来的UIImageview,造一个一模一样的用来移动的imageview。获取到scrollview的panGestureRecognizer的手指位置信息,移动并缩放图片,通知代理设置控制器背景透明度。手指松开时,根据手指瞬间方向判断是否需要退出页面,并执行相应操作。


难点解决


  • 手势绑定事件的方法在父类中,怎么实时监控手势发生并移动了呢?怎么监听手势结束呢?

    当然是用到UIScrollview的代理方法。


/** scrollview正在滚动 */

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;   

/** scrollview即将结束拖拽 */

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset;


这里需要注意的是,如果图片没有屏幕大,那么scrollview的contentSize是小于frame的,这个时候并不能拖拽,代理方法自然也不执行。需要设置scrollview的属性:


/** 总是有弹簧效果 */

_mainScrollView.alwaysBounceVertical = YES;

_mainScrollView.alwaysBounceHorizontal = YES;//这是为了左右滑时能够及时回调scrollViewDidScroll代理


注意左右的弹簧属性也要设置,否则向下拖动交互进行中,如果用户开始左右拖动,而mainScrollView不能左右拖动,那代理方法不会执行,会造成图片卡住不动的感觉。


  • 造跟着手指动的imageView时,初始frame的计算

    其实不算很难,但是要场景要考虑全面,因为在拖动时,图片可能已经是放大状态。


- (void)saveFrameBeginPan

{

    imageWidthBeforeDrag = self.mainImageView.yy_width;//开始时的高

    imageHeightBeforeDrag = self.mainImageView.yy_height;//开始时的宽

    //计算图片Y需要考虑到图片此时的高,如果足够高时,交互发生时y一定是0

    CGFloat imageBeginY = (imageHeightBeforeDrag < kMainScreenHeight) ? (kMainScreenHeight - imageHeightBeforeDrag) * 0.5 : 0.0;

    imageYBeforeDrag = imageBeginY; //+ imageHeightBeforeDrag * 0.5;

    //centerX需要考虑到offset

    scrollOffsetX = self.mainScrollView.contentOffset.x;

    CGFloat imageX = -scrollOffsetX;

    imageCenterXBeforeDrag = imageX + imageWidthBeforeDrag * 0.5;

}


为什么是y值加centerX的值?


因为这样图片缩小的效果是往图片最中间缩。


其它小细节


  • 双手捏合来缩放图片时,也会调用代理scrollViewDidScroll

    解决:根据手势的手指数>1判断是否做交互,另外还可以设置一个变量来记录是不是正在缩放。

  • 向下拖拽时向左右滑动会出现上一张/下一张图片的边缘

    解决:拖拽时通知代理隐藏其它图片,结束时显示出来。

  • 手势拖动时,其实scrollview正在被拖动,里面的图片也就正在移动,图片弹回去时,位置就变化了,会有一个瞬移的感觉

    解决:拖动开始记录下offset,结束拖动时赋值回去。

  • 松开时怎么判断是否返回

    解决:在拖动时记录瞬间的方向,即如果当前手势点的y大于之前的,解释向下移动,松开就要退出页面。否则不退出页面。甚至还可以加一个,如果只像下拉了一丢丢,就不退出页面。

  • 当scrollview的offset.y小于0时才执行交互代码,那如果向下拉了,不松手又向上拉,此时offset.y就大于0了,那不就不执行代码了,图片不就卡住不动了吗?

    解决:设置变量来记录是否正在下拉,拉动开始是为yes,结束时为no,当offset.y<0或者变量为yes,就执行下拉。

  • 小细节太多了。。。。


实战效果


iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

项目效果.gif


其它



iOS开发——仿微信图片浏览交互的实现(向下拖拽图片退出图片浏览器)

始发于微信公众号:Cocoa开发者社区