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

微信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进阶视频课,摸我

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335