iOS技术点牛叉的demoiOS开发者

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

2017-12-08  本文已影响1141人  YY程序猿

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

效果如下图

效果图.gif

架构设计

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


A-B.jpeg

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

界面B的结构

最终设计如下图:


结构示意.jpeg
转场动画

这里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、调用事件执行者绑定的手势事件。
所以,手势所绑定的事件在执行前,会先通过逐级查找的方式,找到最适合响应手势的控件,再执行其方法,流程如下图(白色原点处发生点击)


事件响应链.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手势UIPanGestureRecognizer,所以UIScrollview既然能在手指移动时做事儿,那它也离不开UIPanGestureRecognizer。
翻看UIScrollview的.h文件不难发现,它其实已经暴露了它的pan手势(不暴露就runtime遍历属性,总能找到想要的)。

h文件.jpeg
上文说到,调用手势绑定事件时,会把触摸相关的信息放进手势对象里,所以手指在scrollview里移动时,就能从它的panGestureRecognizer拿到我们想要的信息:手指移动路径,就能根据这些数据,做想要的效果。
所以方案如下:当scrollview在顶部并向下拖拽时,隐藏scrollview中本来的UIImageview,造一个一模一样的用来移动的imageview。获取到scrollview的panGestureRecognizer的手指位置信息,移动并缩放图片,通知代理设置控制器背景透明度。手指松开时,根据手指瞬间方向判断是否需要退出页面,并执行相应操作。
难点解决
/** 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不能左右拖动,那代理方法不会执行,会造成图片卡住不动的感觉。

- (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的值?
因为这样图片缩小的效果是往图片最中间缩。

其它小细节

实战效果

项目效果.gif

其它

上一篇下一篇

猜你喜欢

热点阅读