1. 程式人生 > >仿新版微信浮窗效果

仿新版微信浮窗效果

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.