微信浮窗、微信浮球功能实现demo
微信6.6.7版本近日更新了,最大的亮点莫过于浮窗功能,主要用于将微信文章嵌入到浮窗内,方便大家看文章被其他信息打断后,还能便捷地回到之前的文章继续浏览。
看到这个功能,就有点见猎心喜的感觉,于是动手来实现一下。
![](https://img.haomeiwen.com/i1928363/b947e18dd2f6d686.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;
}
}
这样,微信浮窗功能已经基本实现了。至于微信里还有当我们侧滑的时候,也能将该文章添加到浮窗按钮上,该功能和上面我所分析的流程方法是类似的,感兴趣你也可以实现一下。