仿新版微信浮窗效果

阅读公众号或其他文章,经常需要暂时退出文章.
在新版微信中,可以把浏览的文章缩小为浮窗.点击浮窗继续阅读.对于经常在微信里阅读的人来说,这简直就是人类之光.

微信效果如下


微信效果

对于这功能我进行了仿写.
效果如下

仿写效果

微信的大佬一定用了了不起的技术,我这里只是实现效果.

简单写了一个库,一句代码即可实现效果
https://github.com/SherlockQi/WeChatFloat

//在AppDelegate中将类名传入即可
[HKFloatManager addFloatVcs:@[@"HKSecondViewController"]];

使用到的技术点

监听侧滑返回
//设置边缘侧滑代理
self.navigationController.interactivePopGestureRecognizer.delegate = self;

//当开始侧滑pop时调用此方法
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    /* 判断是否开启边缘侧滑返回 **/
    if (self.navigationController.viewControllers.count > 1) {
         [self beginScreenEdgePanBack:gestureRecognizer];
        return YES;
    }
    return NO;
}
/* UIScreenEdgePanGestureRecognizer
@property(nullable, nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
/*! This subclass of UIPanGestureRecognizer only recognizes if the user slides their finger
    in from the bezel on the specified edge. */
//NS_CLASS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED @interface UIScreenEdgePanGestureRecognizer : UIPanGestureRecognizer
**/
//利用CADisplayLink 来实现监听返回手势
- (void)beginScreenEdgePanBack:(UIGestureRecognizer *)gestureRecognizer{
         /*
          * 引用 gestureRecognizer
          * 开启 CADisplayLink
          * 显示右下视图
          **/
    self.edgePan = (UIScreenEdgePanGestureRecognizer *)gestureRecognizer;
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(panBack:)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [[UIApplication sharedApplication].keyWindow addSubview:self.floatArea];
}
//此方法中进行操作
- (void)panBack:(CADisplayLink *)link {
    //判断手势状态
    if (self.edgePan.state == UIGestureRecognizerStateChanged) {//移动过程
       /*
        * 改变右下视图 frame
        * 判断手指是否进入右下视图中
        **/
    //手指在屏幕上的位置    
    CGPoint tPoint =  [self.edgePan translationInView:kWindow];
     ...根据tPoint设置右下视图 frame...
    //手指在右下视图上的位置(若 x>0 && y>0 说明此时手指在右下视图上)
    CGPoint touchPoint = [kWindow convertPoint:[self.edgePan locationInView:kWindow]  toView:self.floatArea];
    if (touchPoint.x > 0 && touchPoint.y > 0) {
              ...
                //由于右下视图是1/4圆 所以需要这步判断   
                if (pow((kFloatAreaR - touchPoint.x), 2) + pow((kFloatAreaR - touchPoint.y), 2)  <= pow((kFloatAreaR), 2)) {
                    self.showFloatBall = YES;
                }
              ...
    }else  if (self.edgePan.state == UIGestureRecognizerStatePossible) {
       /*
        * 停止CADisplayLink
        * 隐藏右下视图
        * 显示/隐藏浮窗
        **/
        [self.link invalidate];
        if (self.showFloatBall) {        
                self.floatBall.iconImageView.image=  [self.floatViewController valueForKey:@"hk_iconImage"];
                [kWindow addSubview:self.floatBall];
         }
    } 
}
监听浮窗移动/点击
#import "HKFloatBall.h" 类为浮窗视图类
//点击浮窗后让代理push之前保留起来的控制器
- (void)tap:(UIGestureRecognizer *)tap{
    if ([self.delegate respondsToSelector:@selector(floatBallDidClick:)]) {
        [self.delegate floatBallDidClick:self];
     }
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  ...结束监听...
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  ...结束监听...
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    /*
    * 改变浮窗 frame
    * 改变右下视图 frame
    * 判断浮窗center 是否在右下视图之上
    **/
    CGPoint center_ball = [kWindow convertPoint:self.floatBall.center toView:self.cancelFloatArea];
    if (pow((kFloatAreaR - center_ball.x), 2) + pow((kFloatAreaR - center_ball.y), 2)  <= pow((kFloatAreaR), 2)) {
        if (!self.cancelFloatArea.highlight) {
            self.cancelFloatArea.highlight = YES;
        }
    }
}
}
自定义push/pop动画
 //设置navigationController代理
 self.navigationController.delegate = self;

#pragma UINavigationControllerDelegate
//push/pop 时会调用此代理方法
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC{
    ... 判断是否执行动画 若 return nil 则执行原始 push/pop 动画...
   //HKTransitionPush HKTransitionPop 是自己写的两个动画类,需要实现<UIViewControllerAnimatedTransitioning>
    if(operation==UINavigationControllerOperationPush)  {
        return [[HKTransitionPush alloc]init];
    } else if(operation==UINavigationControllerOperationPop){
        return [[HKTransitionPop alloc]init];
    }
}
HKTransitionPush HKTransitionPop 代码类似已HKTransitionPush为例
#import "HKTransitionPush.h"
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
    return kAuration;//动画时间
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
   //获取上下文
    self.transitionContext = transitionContext;
    
    UIViewController * fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *contView = [transitionContext containerView];
    [contView addSubview:fromVC.view];
    [contView addSubview:toVC.view];
    
    //添加遮罩视图
    [fromVC.view addSubview:self.coverView];

    //浮窗的 frame push时这个是起始 frame ,pop时是结束时的 frame
    CGRect floatBallRect = [HKFloatManager shared].floatBall.frame;

    //开始/结束时的曲线 
    UIBezierPath *maskStartBP =  [UIBezierPath bezierPathWithRoundedRect:CGRectMake(floatBallRect.origin.x, floatBallRect.origin.y,floatBallRect.size.width , floatBallRect.size.height) cornerRadius:floatBallRect.size.height/2];
    UIBezierPath *maskFinalBP = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0,SCREEN_WIDTH, SCREEN_HEIGHT) cornerRadius:floatBallRect.size.width/2];
    
    //.layer.mask 是部分显示的原因
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = maskFinalBP.CGPath; 
    toVC.view.layer.mask = maskLayer;

    //动画类
    CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    maskLayerAnimation.fromValue = (__bridge id)(maskStartBP.CGPath);
    maskLayerAnimation.toValue = (__bridge id)((maskFinalBP.CGPath));
    maskLayerAnimation.duration = kAuration;
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction  functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    maskLayerAnimation.delegate = self;
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];

    //隐藏浮窗
    [UIView animateWithDuration:kAuration animations:^{
        [HKFloatManager shared].floatBall.alpha = 0;   
    }];
}
#pragma mark - CABasicAnimation的Delegate
//动画完成后代理
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    [self.transitionContext completeTransition:YES];
    [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
    [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view.layer.mask = nil;
    [self.coverView removeFromSuperview];
    
}
-(UIView *)coverView{
    if (!_coverView) {
        _coverView = [[UIView alloc]initWithFrame:[UIScreen mainScreen].bounds];
        _coverView.backgroundColor = [UIColor blackColor];
        _coverView.alpha = 0.5;
    };
    return _coverView;
}
解耦
将所有代码集中在 #import "HKFloatManager.h" 中
//在AppDelegate中将类名传入即可,在该类控制器侧滑返回时启动浮窗功能(需要在实例化导航控制器之后)
[HKFloatManager addFloatVcs:@[@"HKSecondViewController"]];
若需要设置浮窗头像,设置该控制器的"hk_iconImage"
@property (nonatomic, strong) UIImage *hk_iconImage;

Tips

  • 震动反馈
UIImpactFeedbackGenerator*impactLight = [[UIImpactFeedbackGenerator alloc]initWithStyle:UIImpactFeedbackStyleMedium]; 
[impactLight impactOccurred];
 //    UIImpactFeedbackStyleLight,
 //    UIImpactFeedbackStyleMedium,
 //    UIImpactFeedbackStyleHeavy
  • 分类获取当前控制器
#import "NSObject+hkvc.h"

@implementation NSObject (hkvc)
- (UIViewController *)hk_currentViewController
{
    UIWindow *keyWindow  = [UIApplication sharedApplication].keyWindow;
    UIViewController *vc = keyWindow.rootViewController;
        if ([vc isKindOfClass:[UINavigationController class]])
        {
            vc = [(UINavigationController *)vc visibleViewController];
        }
        else if ([vc isKindOfClass:[UITabBarController class]])
        {
            vc = [(UITabBarController *)vc selectedViewController];
        }
    return vc;
}

- (UINavigationController *)hk_currentNavigationController
{
    return [self hk_currentViewController].navigationController;
}
- (UITabBarController *)hk_currentTabBarController
{
    return [self hk_currentViewController].tabBarController;
}

@end
  • 判断控制器是否有"hk_iconImage"属性
- (BOOL)haveIconImage{
    BOOL have = NO;
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self.floatViewController class], &outCount);  
    for (unsigned int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        const char * nameChar = ivar_getName(ivar);
        NSString *nameStr =[NSString stringWithFormat:@"%s",nameChar];
        if([nameStr isEqualToString:@"_hk_iconImage"]) {
            have = YES;
        }
    }
    free(ivars);
    return have;
}

以上便是实现该效果的全部实现.上方含有部分伪代码.全部代码已上传至 ---Github--- 欢迎(跪求) Star.

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 承认别人比自己优秀是件特别难的事,比如很多人都会说,你考那么多证有什么用啊?学生会有什么用啊?拿奖学金有什么用?考...
    小一凡的日记本阅读 87评论 0 2
  • 01 张爱玲说,也许每一个男子全都有过这样两个女人,至少是两个。娶了红玫瑰,久而久之,红的变了墙上的一抹蚊子血,白...
    木呓阅读 2,868评论 10 17
  • 本周学习了《干法》 事先“看见完成时的状态”就能成功这一节,无论生活还是工作,只有事先精心准备、周密计划,并...
    付中强阅读 484评论 0 1