ios 八点钟学院

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

2018-06-09  本文已影响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进阶视频课,摸我

上一篇 下一篇

猜你喜欢

热点阅读