至于标题到底说了个什么我也描述不清楚,直接看图(语文不要是硬伤)
如图功能
文章涉及技术点
- 浮窗添加方式
- Pan手势
- Protocol协议
- 自定义转场动画
文章涉及三方库
思路整理
//类似这种功能的实现,理清功能流程是重点。
//这种功能实现方式应该有很多种,这里我只讲我的实现方式。
1.
使用微信浮窗功能的时候不难发现,浮窗不会随着你界面的进退而消失,所以这里的浮窗我直接添加到@property (strong, nonatomic) UIWindow *window;
上。
一个是右下角用户不可交互的控制浮窗
一个是用户可以交互移动的真实浮窗
2.
自定义转场动画(支持屏幕左边缘pop),来做到手势pop时用户可选是否保留操作。本文不做自定义转场的教学,但我会贴上代码。留一个专场动画详解
3.
为用户可以交互移动的真实浮窗添加Pan手势用于交互
4.
添加多个协议,实现多空间与页面间的交互控制
浮窗的定义、手势和协议的互相控制
代码分段贴上,便于阅读
两个浮窗。
//定义两个类
@interface FloatingControlView : UIView
@interface FloatingWindow : UIView
//单例创建 便于使用
#define WindowSize 200
//右下角窗口
+ (FloatingControlView *)shareFloatingControlView
{
static FloatingControlView *handleFloatingControlView = nil;
static dispatch_once_t FloatingControlViewToken;
dispatch_once(&FloatingControlViewToken, ^{
if (handleFloatingControlView == nil) {
handleFloatingControlView = [[FloatingControlView alloc] init];
handleFloatingControlView.layer.backgroundColor = RGB(242, 242, 242).CGColor;
handleFloatingControlView.layer.cornerRadius = WindowSize/2.f;
handleFloatingControlView.userInteractionEnabled = NO;
handleFloatingControlView.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Knob_OFF"]];
[handleFloatingControlView addSubview:handleFloatingControlView.imageView];
[[UIApplication sharedApplication].windows[0] addSubview:handleFloatingControlView];
handleFloatingControlView.didContain = NO;
handleFloatingControlView.sd_layout
.rightSpaceToView([UIApplication sharedApplication].windows[0], -WindowSize)
.bottomSpaceToView([UIApplication sharedApplication].windows[0], -WindowSize)
.widthIs(WindowSize)
.heightIs(WindowSize);
handleFloatingControlView.imageView.sd_layout
.centerXIs(WindowSize/3.f)
.centerYIs(WindowSize/3.f)
.widthIs(WindowSize/3.f)
.heightIs(WindowSize/3.f);
}
});
return handleFloatingControlView;
}
//单例创建 便于使用
#define WindowSize 60
//圆形小窗口
+ (FloatingWindow *)shareFloatingWindow
{
static FloatingWindow *handleFloatingWindow = nil;
static dispatch_once_t FloatingWindowToken;
dispatch_once(&FloatingWindowToken, ^{
if (handleFloatingWindow == nil) {
handleFloatingWindow = [[FloatingWindow alloc] init];
handleFloatingWindow.layer.backgroundColor = RGB(255, 83, 89).CGColor;
handleFloatingWindow.layer.cornerRadius = 30;
handleFloatingWindow.userInteractionEnabled = YES;
[handleFloatingWindow addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:handleFloatingWindow action:@selector(panGesture:)]];
handleFloatingWindow.hidden = YES;
handleFloatingWindow.imgButton = [[UIButton alloc] init];
handleFloatingWindow.imgButton.layer.backgroundColor = RGB(179, 179, 179).CGColor;
handleFloatingWindow.imgButton.layer.cornerRadius = 25;
[handleFloatingWindow.imgButton setImage:[UIImage imageNamed:@""] forState:UIControlStateNormal];
[handleFloatingWindow.imgButton addTarget:handleFloatingWindow action:@selector(detailAction:) forControlEvents:UIControlEventTouchUpInside];
[handleFloatingWindow addSubview:handleFloatingWindow.imgButton];
[[UIApplication sharedApplication].windows[0] addSubview:handleFloatingWindow];
handleFloatingWindow.sd_layout
.rightSpaceToView([UIApplication sharedApplication].windows[0], 20)
.topSpaceToView([UIApplication sharedApplication].windows[0], K_NavigationBarHeight+50)
.widthIs(WindowSize)
.heightIs(WindowSize);
handleFloatingWindow.imgButton.sd_layout
.spaceToSuperView(UIEdgeInsetsMake(5, 5, 5, 5));
}
});
return handleFloatingWindow;
}
流程线
这里我们称圆形小View为A,右下角半圆View为B
在浏览界面手势pop触发选择是否将页面作为浮窗(这是能走接下来所有操作的第一步),从上面的代码里可以看到,当我创建A单例的时候,初始状态是handleFloatingWindow.hidden = YES;
隐藏的。当用户开始手势pop时,“激活”B动画弹出,用户可以自行选择手势是否滑入B内。
这里小提一下微信做的很人性化,只有滑入B并且松手才会触发浮窗,滑入时有振动提示并且B露出部分变大,滑出恢复。贴上振动方法
//注意版本兼容
- (void)impactFeedback {
UIImpactFeedbackGenerator*impactLight = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
[impactLight impactOccurred];
}
若用户pop手势滑入B并且再内部松开,激活A变为非隐藏状态
注:所谓的“缩小浮窗”,一定要做到点击重新push后不是重新加载页面(否则体验依然很卵),所以我们在手势pop后不能让页面释放,而做出pop手势时,所以我们需要在Push进入页面时让引用计数+1,这样在pop时不至于直接dealloc页面。
说了这么多其实就是定义一个属性来保存push的页面。
至于这个属性定义在哪里都可以,只要保证定义所在的类不会释放,我是定义在AppDelegate里的。
//存储,避免释放、二次重新加载。push时调用self.detailViewController
@property (nonatomic, strong) FloatingDetailViewController *detailViewController;
@property (nonatomic, strong) FloatingDetailViewController *tempDetailViewController;
至于我为什么要定义两个,是为了做到在已经保存了一个浮窗页面后,进入了另一个页面。微信的做法是,如果用户激活了A保存了一个页面叫“吃饭睡觉”,然后用户在没有删除浮窗A的情况下,进入了另一个页面“打豆豆”,此时微信会释放“吃饭睡觉”页面,保存“打豆豆”页面。如果看不懂我这段话,自己去玩一玩微信这个功能可能会明朗些
说完怎么保留页面避免释放以后,就要说如何注销(隐藏)A、释放保存的页面了。这里涉及到两步。
1. 用户拖动A,激活B动画出现(读到这里如果没有什么疑问,应该理解能够激活B弹出的方式有两种了,一种是pop手势,一种是拖动A)
2. 用户将A拖动到B里,这时隐藏A,释放页面
self.detailViewController = nil;
self.tempDetailViewController = nil;
到这里功能就说的差不多了
整个过程都是A、B、ViewController之间的相互控制,很容易搞混。
最后用自定义转场、Pan手势、协议的代码给大家梳理一下,最后会附上Demo。
功能的开始--自定义转场pop手势
//转场代理 自带手势禁用 添加自己的手势
self.navigationController.delegate = self;
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
UIScreenEdgePanGestureRecognizer *edgePan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgePanGesture:)];
edgePan.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:edgePan];
- (void)edgePanGesture:(UIScreenEdgePanGestureRecognizer *)edgePan {
float progress = [edgePan translationInView:self.view].x / KSCreenWidth;
CGPoint point = [edgePan locationInView:[UIApplication sharedApplication].windows[0]];
if (GetAppDelegate.detailViewController) {
[FloatingWindow shareFloatingWindow].alpha = progress;
}
if (edgePan.state == UIGestureRecognizerStateBegan) {
self.percentDrivenTransition = [UIPercentDrivenInteractiveTransition new];
[self.navigationController popViewControllerAnimated:YES];
if (!GetAppDelegate.detailViewController && self.delegate && [self.delegate respondsToSelector:@selector(didStartPanGesture)]) {
[self.delegate didStartPanGesture];
}
}
else if (edgePan.state == UIGestureRecognizerStateChanged) {
[self.percentDrivenTransition updateInteractiveTransition:progress];
if (!GetAppDelegate.detailViewController && self.delegate && [self.delegate respondsToSelector:@selector(didChangePanGesture:)]) {
[self.delegate didChangePanGesture:point];
}
}
else if (edgePan.state == UIGestureRecognizerStateEnded) {
if (!GetAppDelegate.detailViewController && self.delegate && [self.delegate respondsToSelector:@selector(didEndPanGesture)]) {
[self.delegate didEndPanGesture];
}
if (progress > 0.5) {
[self.percentDrivenTransition finishInteractiveTransition];
if (GetAppDelegate.detailViewController) {
[FloatingWindow shareFloatingWindow].alpha = 1;
}
}
else {
[self.percentDrivenTransition cancelInteractiveTransition];
}
self.percentDrivenTransition = nil;
}
}
A控制B、B控制页面状态、ViewController控制A
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[FloatingControlView shareFloatingControlView].delegate = self;
[FloatingWindow shareFloatingWindow].delegate = [FloatingControlView shareFloatingControlView];
// Override point for customization after application launch.
return YES;
}
至于协议的具体内容、手势的具体控制方法就直接看Demo吧。
进入Demo后选择TableView的第十四条 仿微信朋友圈 Floating Window
表述能力不行敬请谅解。
最后注意身体,小心秃顶😬
网友评论