首先來看下微信上的效果:
git
再來看下咱們的實現效果:
github
微信的懸浮窗功能已經出來有好幾個月了,最近因某些特殊緣由正好想嘗試實現它。接下來就有了一頓操做(學習)猛如虎,一看效果好像還行滴!在此過程參考過許多大神的資料,也學習過現有的一些demo,可是做爲一個完美主義者,網上現有的demo始終達不到我心目中的「高仿」。
接下來,又開始了一播摳圖、做圖的操做,立求把「高仿」兩字體現得淋漓盡致。沒錯我就是那個大家說的「不會摳圖的產品經理不是一個好的程序猿」。
好了,廢話了這麼多,接下來開始介紹正題:微信
Github地址走過路過給個🌟吧app
1.若是你的項目沒有相似以下代碼:_navigationController.delegate
和_navigationController.interactivePopGestureRecognizer.delegate
也就是沒有對UINavigationController
和UINavigationController
的右滑返回手勢設置代理。
那麼你只須要添加一行代碼就能集成...
一行代碼,真的只有一行:學習
//添加要監控的類名 [[FloatBallManager shared] addFloatMonitorVCClasses:@[@"SecondViewController"]];
最好在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
裏添加。 測試
2.若是不巧的是,你的項目設置了上述兩個代理(固然,大部分狀況下都會設置)。不方,只要添加以下配置就行了:字體
#pragma mark - UINavigationControllerDelegate #pragma mark 自定義轉場動畫 - (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { return [[FloatBallManager shared] floatBallAnimationWithOperation:operation fromViewController:fromVC toViewController:toVC]; } #pragma mark 交互式轉場 - (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController { return [[FloatBallManager shared] floatInteractionControllerForAnimationController:animationController]; } - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; }
接下來讓我帶你一步步講解實現過程:
1.首先懸浮球的添加位置得是全局置頂的,因此首選添加到UIWindow
上,咱們選擇[UIApplication sharedApplication].keyWindow
。 動畫
2.懸浮球須要添加一個單擊手勢和一個拖動手勢:spa
//添加拖動手勢 [_floatView addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragFloatView:)]]; //添加點擊手勢 [_floatView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapFloatView:)]];
3.接下來就是重點:自定義轉場動畫的實現。
要實現自定義轉場動畫,得實現UINavigationControllerDelegate
的方法:代理
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
該方法就是告訴navigationController
,從fromViewController
到toViewController
以哪一種 operation
(pop或push)方式,經過UIViewControllerAnimatedTransitioning
協議來自定義該轉場動畫。
你覺得用這個就能實現了嗎?不,微信怎麼可能用這麼low的解決方案。
咱們觀察微信的實現效果,當手勢拖動超過屏幕一半後離開,從離開的位置開始作一個縮小到懸浮球位置,而且跟懸浮球一樣大小的動畫。
網上的demo大多隻實現了這一步。 也就是說從手指離開屏幕的那一刻,動畫會直接以一個translate
的方式,將toViewController.view
從屏幕的左邊移向右邊。
那麼,這個動畫的轉變該怎麼實現呢?
最開始我找到了UIViewPropertyAnimator
,而且實現了對動畫的轉變效果。可是這個類只支持iOS10以上。我用iOS8的設備對微信進行了測試,發現iOS8上也是支持動畫轉變效果的。
接下來我就開始思考該如何支持到iOS7呢?在閉關研究了一天無果以後,肚子餓得不行,我就以爲得先去填飽肚子先。就在我跨出門的那一刻,腦路忽然通了(這個故事告訴咱們,在長期深陷於某個問題找不到解決方案的時候,能夠先嚐試放鬆下,也許有意外收穫呢?)。既然手勢是自身添加的(接下來會說),那我徹底能夠控制整個交互過程呀...
而後,咱們要想對整個轉場動畫進行控制,那咱們得實現:
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
該方法就是告訴navigationController
,你要自定義一個實現了UIViewControllerInteractiveTransitioning
協議的類來全程控制轉場。
咱們這裏用系統給咱們封裝過一個類UIPercentDrivenInteractiveTransition
,這個類提供了對轉場動畫的常規控制。
咱們主要用到下面三個方法:
//更新轉場進度 - (void)updateInteractiveTransition:(CGFloat)percentComplete; //取消轉場 ,用於拖動手勢未達到pop條件時,讓動畫還原 - (void)cancelInteractiveTransition; //轉場結束,這個很重要,不執行的話屏幕會卡在動畫結束的那一刻 - (void)finishInteractiveTransition;
具體用法,請繼續看下面的介紹。
4.右滑返回手勢的監控。 CADisplayLink
:一個能讓咱們以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器。
正常的話咱們能夠添加一個CADisplayLink
就能監控到當前拖動手勢的位置,可是這裏它已經知足了咱們的需求了(是的,普通物種已經知足不了人類了)。
這裏介紹一個類UIScreenEdgePanGestureRecognizer
,此類繼承於UIPanGestureRecognizer
,跟UIPanGestureRecognizer
用法大體相同,可是它多了一個UIRectEdge
屬性:
typedef NS_OPTIONS(NSUInteger, UIRectEdge) { UIRectEdgeNone = 0, UIRectEdgeTop = 1 << 0, UIRectEdgeLeft = 1 << 1, UIRectEdgeBottom = 1 << 2, UIRectEdgeRight = 1 << 3, UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight } NS_ENUM_AVAILABLE_IOS(7_0);
也就是說,經過該屬性改變手勢的邊緣觸發位置,這裏咱們設置成gesture.edges = UIRectEdgeLeft;
。
那咱們在何時添加UIScreenEdgePanGestureRecognizer
呢?
就是使用方法裏介紹的第2種狀況:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; } - (void)didShowViewController:(UIViewController *)viewController navigationController:(UINavigationController *)navigationController { //若是當前顯示的類爲咱們添加要監控的類,則將系統手勢禁用,本身添加一個邊緣拖動手勢,模擬系統右滑返回交互 if ([self.monitorVCClasses containsObject:NSStringFromClass([viewController class])]) { navigationController.interactivePopGestureRecognizer.enabled = NO; // 邊緣手勢 UIScreenEdgePanGestureRecognizer *gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleNavigationTransition:)]; gesture.edges = UIRectEdgeLeft; gesture.delegate = self; [viewController.view addGestureRecognizer:gesture]; } else { //將系統右滑返回手勢還原 navigationController.interactivePopGestureRecognizer.enabled = YES; } }
在UINavigationController
執行完- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
代理方法後,咱們對當前屏幕顯示的控制器進行一些手勢添加與系統手勢的禁用控制。
接下來講下UIScreenEdgePanGestureRecognizer
手勢回調的簡單用法,請忽略中間省略的幾萬行代碼,哈哈哈哈....
- (void)handleNavigationTransition:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { ...//此處省略幾萬行代碼 //手勢開始時調用pop方法告訴系統轉場要開始了 //kPopWithPanGes是用來判斷pop是由手勢觸發的仍是點擊左上角返回按鈕觸發 objc_setAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_ASSIGN); [[NSObject currentNavigationController] popViewControllerAnimated:YES]; } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) { ...//此處省略幾萬行代碼 //更新轉場動畫進度 [animator updateInteractiveTransition:progress]; [interactive updateInteractiveTransition:progress]; } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded || gestureRecognizer.state == UIGestureRecognizerStateCancelled) { ...//此處省略幾萬行代碼 //快速滑動時,經過手勢加速度算出動畫執行時間可移動距離,模擬系統快速拖動時可pop操做 CGPoint velocityPoint = [gestureRecognizer velocityInView:[UIApplication sharedApplication].keyWindow]; CGFloat velocityX = velocityPoint.x * AnimationDuration; //滑動超過屏幕一半,完成轉場 if (fmax(velocityX, point.x) > FloatScreenWidth / 2.0) { if (notShowFloatContent) { //右滑手勢,滑動至右下角1/4圓內則顯示懸浮球 if ([self p_checkTouchPointInRound:point]) { [animator replaceAnimation]; } else { [animator continueAnimationWithFastSliding:velocityX > FloatScreenWidth / 2.0]; } } else { //正在顯示懸浮球內容 //右滑手勢拖動超過一半,手指離開屏幕,也會從當前觸摸位置縮小到懸浮球 [animator replaceAnimation]; } [interactive finishInteractiveTransition]; } else { //未觸發pop,取消轉場操做,動畫迴歸 [animator cancelInteractiveTransition]; [interactive cancelInteractiveTransition]; } }
5.轉場動畫FloatTransitionAnimator
的介紹
這個類遵循了UIViewControllerAnimatedTransitioning
協議,該協議只有2個方法
//動畫執行時間 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; //動畫執行過程 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
動畫的具體實現,看源碼吧~~~這裏咱們用的是UIBezierPath
,不建議在這裏用animateWithDuration
來改變frame
與layer.cornerRadius
,動畫執行過程很不天然,固然也有多是我使用姿式不對...具體實現各位自行決定吧。
6.其它說明
a.項目中我用runtime
對UIViewController
與FloatTransitionAnimator
、UIPercentDrivenInteractiveTransition
進行了綁定,由於相互之間進行了相互強引用,因此在交互完以後都進行了手動置nil
,防止循環引用引發內存泄漏。
#pragma mark 手勢清除controller綁定的轉場動畫與轉場交互 - (void)p_clearControllerAnimatorAndInteractive:(UIViewController *)vc { objc_setAssociatedObject(vc, &kPopInteractiveKey, nil, OBJC_ASSOCIATION_ASSIGN); objc_setAssociatedObject(vc, &kAnimatorKey, nil, OBJC_ASSOCIATION_ASSIGN); }
在此提醒各們猿們
,在日常的開發過程當中也要多注意這種相似的循環引用。
b.懸浮球拖動到右下角的觸發條件,這裏判斷方法是觸摸點到圓心(屏幕右下角的座標)的距離是否小於圓半徑。
//判斷手勢觸摸點是否在圓內 - (BOOL)p_checkTouchPointInRound:(CGPoint)point { CGPoint center = CGPointMake(FloatScreenWidth, FloatScreenHeight); double dx = fabs(point.x - center.x); double dy = fabs(point.y - center.y); double distance = hypot(dx, dy); //觸摸點到圓心的距離小於半徑,則表明觸摸點在圓內 return distance < RoundViewRadius; }
c.懸浮球進入圓內的手機震動反饋提醒。
這裏是用的UIImpactFeedbackGenerator
,該類只支持iOS10以上,它的震動效果更輕柔,至於iOS10如下的震動,各位自身去搜索吧(動起來,不要作伸手黨)!
#pragma mark - 手機震動 - (void)p_shockPhone { static BOOL canShock = YES; if (@available(iOS 10.0, *)) { if (!canShock) { return; } canShock = NO; UIImpactFeedbackGenerator *impactFeedBack = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; [impactFeedBack prepare]; [impactFeedBack impactOccurred]; //防止同時觸發幾個震動 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ canShock = YES; }); } }
好了,大體的原理都已經介紹完了。
其實在開始作以前,我對自定義轉場動畫一點都不瞭解,剛看到文檔介紹還有那麼一丟丟抗拒,甚至也有想過放棄!可是對於技術的堅持讓我一點點地去啃下了這個陌生的硬骨頭。直到如今把它分享出來以後,我以爲這一切都是有意義了,甚至還有那麼一丟丟成就感,可能這就是(程序猿)命吧!
經過這個demo,但願給你們提供一些技術上的幫助吧!
Github地址記得給個小星星哦🌟
順便給你們附上相關資料傳送門吧:
轉場動畫的詳細介紹,很詳細