美文网首页ios 八点钟学院
微信浮窗、微信浮球功能实现demo

微信浮窗、微信浮球功能实现demo

作者: 腾讯课堂八点钟学院 | 来源:发表于2018-06-09 16:00 被阅读337次

    微信6.6.7版本近日更新了,最大的亮点莫过于浮窗功能,主要用于将微信文章嵌入到浮窗内,方便大家看文章被其他信息打断后,还能便捷地回到之前的文章继续浏览。

    看到这个功能,就有点见猎心喜的感觉,于是动手来实现一下。

    微信浮窗.gif
    功能点列表:

    1、浮窗的展示,浮窗按钮 和 右下侧四分之一圆的实现和布局
    2、浮窗按钮拖动效果:上下拖动可以到屏幕边缘;左右拖动过程中,根据离左右两边的距离,回弹到最近的一边;浮窗点击能跳转页面,拖动过程中右下侧四分之一圆能动画展示出来;浮窗拖动进入右下侧四分之一圆范围后松开,浮窗消失;
    3、点击浮窗,进入浮窗页面的展开动画效果
    4、叉掉浮窗页面的收缩动画效果
    5、浮窗页面手势往右侧滑,超过1/2页面后松开,收缩动画效果

    创建EOCWeChatFloatingBtn和EOCSemiCircleView分别代表浮窗按钮和右下侧四分之一圆;在EOCWeChatFloatingBtn来封装实现浮窗功能的展现和上述功能点2

    ///  浮窗展示方法,如果你想添加浮窗,只需要简单调用这个方法就可以
    + (void)show {
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ///浮窗按钮和右下侧四分之一圆初始化
            floatingBtn = [[EOCWeChatFloatingBtn alloc] initWithFrame:CGRectMake(0.f, 200.f, 60.f, 60.f)];
            semiCircleView = [[EOCSemiCircleView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height, fixSpace, fixSpace)];
            
        });
        
        ///两者顺序不能颠倒,添加到window层级
        if (!semiCircleView.superview) {
            
            [[UIApplication sharedApplication].keyWindow addSubview:semiCircleView];
            [[UIApplication sharedApplication].keyWindow bringSubviewToFront:semiCircleView];
            
        }
        
        if (!floatingBtn.superview) {
            
            floatingBtn.frame = CGRectMake(0.f, 200.f, 60.f, 60.f);
            [[UIApplication sharedApplication].keyWindow addSubview:floatingBtn];
            [[UIApplication sharedApplication].keyWindow bringSubviewToFront:floatingBtn];
            
        }
        
    }
    

    拖动效果:浮窗按钮上下拖动可以到屏幕边缘;左右拖动过程中,根据离左右两边的距离,回弹到最近的一边;点击浮窗按钮,进行跳转。
    在EOCWeChatFloatingBtn的touch事件中进行处理。

    #pragma mark - touch 方法
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [super touchesBegan:touches withEvent:event];
        
        UITouch *touch = [touches anyObject];
        lastPoint = [touch locationInView:self.superview];  ///标记刚开始触摸时的位置
        pointInSelf = [touch locationInView:self];
    
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [super touchesMoved:touches withEvent:event];
        
        /// 动画展开semiCircleView
        CGRect rect = CGRectMake([UIScreen mainScreen].bounds.size.width - fixSpace, [UIScreen mainScreen].bounds.size.height - fixSpace, fixSpace, fixSpace);
        
        if (!CGRectEqualToRect(semiCircleView.frame, rect)) {
            
            [UIView animateWithDuration:0.3f animations:^{
                
                semiCircleView.frame = rect;
                
            }];
            
        }
        
        UITouch *touch = [touches anyObject];
        CGPoint point = [touch locationInView:self.superview];
        
        CGFloat theCenterX = point.x + (self.frame.size.width/2 - pointInSelf.x);
        CGFloat theCenterY = point.y + (self.frame.size.height/2 - pointInSelf.y);
        
        CGFloat x = MIN([UIScreen mainScreen].bounds.size.width - self.frame.size.width/2, MAX(theCenterX, self.frame.size.width/2));
        CGFloat y = MIN([UIScreen mainScreen].bounds.size.height - self.frame.size.height/2, MAX(theCenterY, self.frame.size.height/2));
        
        //移动的时候,该图标也跟随移动
        self.center = CGPointMake(x, y);
        
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [super touchesEnded:touches withEvent:event];
        
        ///收缩动画
        CGRect rect = CGRectMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height, fixSpace, fixSpace);
        
        if (!CGRectEqualToRect(semiCircleView.frame, rect)) {
            
            [UIView animateWithDuration:0.3f animations:^{
                
                semiCircleView.frame = rect;
                
               /// 两个圆心的距离 <= 四分之一圆的半径 - 圆形的半径  移除掉self
                CGFloat distance = sqrt(pow([UIScreen mainScreen].bounds.size.width - self.center.x, 2) + pow([UIScreen mainScreen].bounds.size.height - self.center.y, 2));
                
                if (distance <= fixSpace - 30.f) {
                    
                    [self removeFromSuperview];
                    
                }
                
            }];
            
        }
        
        UITouch *touch = [touches anyObject];
        CGPoint curPoint = [touch locationInView:self.superview];
        
        ///判断end和begin 两种状态之间是否有移动,如果没有移动,响应点击跳转事件
        if (CGPointEqualToPoint(curPoint, lastPoint)) {
            
            /// 跳转 到相应的控制器
            return;
            
        }
        
        /// 离左右两边的距离
        CGFloat left = curPoint.x;
        CGFloat right = [UIScreen mainScreen].bounds.size.width - curPoint.x;
        
        if (left <= right) {   ///往左边靠
            
            [UIView animateWithDuration:0.2f animations:^{
               
                self.center = CGPointMake(10+self.frame.size.width/2, self.center.y);
                
            }];
            
        } else {   ///往右边靠
            
            [UIView animateWithDuration:0.2f animations:^{
                
                self.center = CGPointMake([UIScreen mainScreen].bounds.size.width - (10+self.frame.size.width/2), self.center.y);
                
            }];
            
        }
        
    }
    

    接下来就是重点部分的内容,怎么来实现展开、收缩以及侧滑的动画呢??
    如果你对自定义转场动画有所了解的话,你的思路会是通过修改UINavigationController的转场动画,来达到目标,我们先来实现非交互式动画,也就是点击后展开和收缩效果

    在touchEnd里,实现跳转,核心是对navigationController添加代理

    ///判断end和begin 两种状态之间是否有移动,如果没有移动,响应跳转事件
        if (CGPointEqualToPoint(curPoint, lastPoint)) {
            
           UINavigationController *nav = (UINavigationController *)[UIApplication sharedApplication].keyWindow.rootViewController;
            nav.delegate = self;
            EOCNextViewController *nextViewCtrl = [EOCNextViewController new];
            
            [nav pushViewController:nextViewCtrl animated:YES];
            return;
            
        }
    

    在navigationController的代理方法里,返回自定义动画对象

    #pragma mark - UINavigationController delegate method
    - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                                animationControllerForOperation:(UINavigationControllerOperation)operation
                                                             fromViewController:(UIViewController *)fromVC
                                                               toViewController:(UIViewController *)toVC {
        
        if (operation == UINavigationControllerOperationPush) {
            
            self.alpha = 0.f;
            
        } 
        
        EOCAnimator *animator = [EOCAnimator new];
        animator.curPoint = self.frame.origin;
        animator.operation = operation;
        
        return animator;
        
    }
    

    EOCAnimator里的实现,也是微信浮窗效果的关键和重要部分,为了能达到流畅的动画效果,我这里通过截屏以及layer.mask来实现的,具体可以看代码

    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
        
        return 1.f;
        
    }
    
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
        
        UIView *containerView = [transitionContext containerView];
        
        if (_operation == UINavigationControllerOperationPush) {
        
            UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
            [containerView addSubview:toView];
            
            ///截屏
            EOCAnimView *theView = [[EOCAnimView alloc] initWithFrame:toView.bounds];
            
            UIGraphicsBeginImageContext(toView.bounds.size);
            [toView.layer renderInContext:UIGraphicsGetCurrentContext()];
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            theView.imgView.image = image;
            
            toView.hidden = YES;
            
            UIGraphicsEndImageContext();
            
            [containerView addSubview:theView];
            
            [theView startAnimationForView:toView fromRect:CGRectMake(_curPoint.x, _curPoint.y, 60.f, 60.f) toRect:toView.frame];
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                [transitionContext completeTransition:YES];
                
            });
            
            
        } else if (_operation == UINavigationControllerOperationPop) {
    
            UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
            [containerView addSubview:toView];
            
            UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
            [containerView bringSubviewToFront:fromView];
            
            UIView *floatingBtn = [UIApplication sharedApplication].keyWindow.subviews.lastObject;
                     
                ///截屏
                EOCAnimView *theView = [[EOCAnimView alloc] initWithFrame:fromView.bounds];
                UIGraphicsBeginImageContext(fromView.bounds.size);
                [fromView.layer renderInContext:UIGraphicsGetCurrentContext()];
                UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
                theView.imgView.image = image;
                UIGraphicsEndImageContext();
                
                CGRect fromRect = fromView.frame;
                fromView.frame = CGRectZero;
                
                [containerView addSubview:theView];
                
                
                [theView startAnimationForView:theView fromRect:fromRect toRect:CGRectMake(_curPoint.x, _curPoint.y, 60.f, 60.f)];
            
                [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
                floatingBtn.alpha = 1.f;
            }
        
    }
    

    通过上面的代码,我们可以看到有一个比较关键的方法
    -(void)startAnimationForView:(UIView *)view fromRect:(CGRect)fromRect toRect:(CGRect)toRect
    这里面就是自定义了一个EOCAnimView,在该文件里实现view从fromRect舒展到toRect的效果或者说从fromRect收缩到toRect的效果

        - (instancetype)initWithFrame:(CGRect)frame {
        
        self = [super initWithFrame:frame];
        
        _imgView = [[UIImageView alloc] initWithFrame:frame];
        [self addSubview:_imgView];
        
        self.backgroundColor = [UIColor clearColor];
        
        return self;
        
    }
    
    - (void)startAnimationForView:(UIView *)view fromRect:(CGRect)fromRect toRect:(CGRect)toRect {
        
        toView = view;
        
        _shapeLayer = [CAShapeLayer layer];
        _shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:fromRect cornerRadius:30.f].CGPath;
        _shapeLayer.fillColor = [UIColor grayColor].CGColor;
        self.imgView.layer.mask = _shapeLayer;
        
        CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"path"];
        anim.toValue = (__bridge id)[UIBezierPath bezierPathWithRoundedRect:toRect cornerRadius:30.f].CGPath;
        anim.duration = 0.5f;
        anim.delegate = self;
        anim.fillMode = kCAFillModeForwards;
        anim.removedOnCompletion = NO;
        
        [self.shapeLayer addAnimation:anim forKey:@"revealAnimation"];
        
    }
    
    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
        
        toView.hidden = NO;
        [self removeFromSuperview];
        
    }
    

    至此,非交互式动画已经实现完成,要实现侧滑的过程中的动画,就需要用到交互式动画了,新创建EOCInteractiveTransition的类,该类继承于UIPercentDrivenInteractiveTransition,同时在navigationController的代理里返回EOCInteractiveTransition的对象

    - (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                       interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
        
        return interactiveTransition.isInteractive?interactiveTransition:nil;
        
    }
    

    EOCInteractiveTransition里创建滑动手势,监听它的几种状态

    - (void)panAction:(UIPanGestureRecognizer *)gesture {
        
        UIView *floatingBtn = [UIApplication sharedApplication].keyWindow.subviews.lastObject;
        UINavigationController *nav = (UINavigationController *)[UIApplication sharedApplication].keyWindow.rootViewController;
        
        switch (gesture.state) {
                
            case UIGestureRecognizerStateBegan:
                
                _isInteractive = YES;
                
                [nav popViewControllerAnimated:YES];
                
                break;
                
            case UIGestureRecognizerStateChanged: {
                
                //监听当前滑动的距离
                CGPoint transitionPoint = [gesture translationInView:presentedViewController.view];
                
                CGFloat ratio = transitionPoint.x/[UIScreen mainScreen].bounds.size.width;
                
                transitionX = transitionPoint.x;
                
                ///获得floatingBtn,改变它的alpha值
               
                floatingBtn.alpha = ratio;
                
                if (ratio >= 0.5) {
                    
                    shouldComplete = YES;
                    
                } else {
                    
                    shouldComplete = NO;
                    
                }
                
                [self updateInteractiveTransition:ratio];
                
            }
                
                break;
            case UIGestureRecognizerStateEnded:
            case UIGestureRecognizerStateCancelled: {
                
                if (shouldComplete) {
    
                    /// 添加动画
                    ///截屏
                    UIView *fromView = presentedViewController.view;
                    
                    EOCAnimView *theView = [[EOCAnimView alloc] initWithFrame:fromView.bounds];
                    UIGraphicsBeginImageContext(fromView.bounds.size);
                    [fromView.layer renderInContext:UIGraphicsGetCurrentContext()];
                    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
                    theView.imgView.image = image;
                    UIGraphicsEndImageContext();
                    
                    CGRect fromRect = fromView.frame;
                    fromView.frame = CGRectZero;
                    
                    [fromView.superview addSubview:theView];
                    
                    [theView startAnimationForView:theView fromRect:CGRectMake(transitionX, 0.f, fromRect.size.width, fromRect.size.height) toRect:CGRectMake(_curPoint.x, _curPoint.y, 60.f, 60.f)];
                    
                    [self finishInteractiveTransition];
                    nav.delegate = nil;  //这个需要设置,而且只能在这里设置,不能在外面设置
    
                } else {
    
                    floatingBtn.alpha = 0.f;
                    [self cancelInteractiveTransition];
    
                }
                
                _isInteractive = NO;
                
            }
                break;
            default:
                break;
        }
        
    }
    

    这样,微信浮窗功能已经基本实现了。至于微信里还有当我们侧滑的时候,也能将该文章添加到浮窗按钮上,该功能和上面我所分析的流程方法是类似的,感兴趣你也可以实现一下。

    Git地址


    推荐一个iOS进阶视频课,摸我

    相关文章

      网友评论

      本文标题:微信浮窗、微信浮球功能实现demo

      本文链接:https://www.haomeiwen.com/subject/seypsftx.html