UITabBar 点击动画与切换 VC 动画实现

假如一个 APP 能带来良好的交互体验,那么它一定是”活的”。

效果图

show me the code.

tabBarController 切换 selectedViewController 的动画添加

- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
    NSUInteger shouldSelectIndex = [tabBarController.viewControllers indexOfObject:viewController];
    if (tabBarController.selectedIndex == shouldSelectIndex) {
        return YES;
    }

    CATransition *animation = [CATransition animation];
    animation.duration = 0.3;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.type = kCATransitionPush;
    if (tabBarController.selectedIndex > shouldSelectIndex) {
        animation.subtype = kCATransitionFromLeft;
    } else {
        animation.subtype = kCATransitionFromRight;
    }
    [[[tabBarController valueForKey:@"_viewControllerTransitionView"] layer] addAnimation:animation forKey:@"animation"];

    return YES;
}

点击 UITabBarItem添加动画效果

- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item {
    NSInteger index = [self.tabBar.items indexOfObject:item];
    [self animationWithIndex:index];
}

#pragma mark - private methods
- (void)animationWithIndex:(NSInteger)index {
    NSMutableArray *tabBarbuttonArray = [NSMutableArray array];
    // 找到所有的 UITabBarButton
    for (UIView *tabBarButton in self.tabBar.subviews) {
        if ([tabBarButton isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
            [tabBarbuttonArray addObject:tabBarButton];
        }
    }
    // tabBarButton 排序
    [tabBarbuttonArray sortUsingComparator:^NSComparisonResult(UIView *_Nonnull obj1, UIView *_Nonnull obj2) {
        return CGRectGetMinX(obj1.frame) > CGRectGetMinX(obj2.frame);
    }];

    UIView *tabbarBtn = tabBarbuttonArray[index];
    // 找到UITabBarButton中的imageView,加动画
    for (UIView *sub in tabbarBtn.subviews) {
        if ([sub isKindOfClass:NSClassFromString(@"UITabBarSwappableImageView")]) {
            [self preformAnimationForView:sub];
        }
    }
}

- (void)preformAnimationForView:(UIView *)view {
    [view.layer addAnimation:[self scaleAnimation] forKey:nil];
}

/**
 重力效果弹跳
 */
- (CAKeyframeAnimation *)gravityElasticAnimation {
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
    animation.values = @[@0.0, @-4.15, @-7.26, @-9.34, @-10.37, @-9.34, @-7.26, @-4.15, @0.0, @2.0, @-2.9, @-4.94, @-6.11, @-6.42, @-5.86, @-4.44, @-2.16, @0.0];
    animation.duration = 0.8;
    animation.beginTime = CACurrentMediaTime();
    animation.removedOnCompletion = YES;
    return animation;
}

/**
 缩放动画
 */
- (CABasicAnimation *)scaleAnimation {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.duration = 0.2;
    animation.repeatCount = 1;
    animation.removedOnCompletion = YES;
    animation.autoreverses = YES;
    animation.fromValue = @1;
    animation.toValue = @[@0.7, @1];

    return animation;
}

Swift 5 版本

extension BaseTabBarController: UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom _: UIViewController, to _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TabBarTransition(viewControllers: tabBarController.viewControllers)
    }

    private var scaleAnimation: CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "transform.scale")
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        animation.duration = 0.2
        animation.repeatCount = 1
        animation.isRemovedOnCompletion = true
        animation.autoreverses = true
        animation.fromValue = 1
        animation.toValue = [0.7, 1]

        return animation
    }

    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        let orderedTabBarItemViews: [UIView] = {
            let interactionViews = tabBar.subviews.filter { $0 is UIControl }
            return interactionViews.sorted(by: { $0.frame.minX < $1.frame.minX })
        }()

        guard
            let index = self.tabBar.items?.firstIndex(of: item),
            let imageView = orderedTabBarItemViews[index].subviews.first as? UIImageView
        else {
            return
        }

        // 找到UITabBarButton中的imageView,加动画
        imageView.layer.add(scaleAnimation, forKey: nil)
    }
}

TabBarTransition.swift:

//
//  TabBarTransition.swift
//  ViPay
//
//  Created by VanJay on 2019/6/30.
//  Copyright © 2019 VanJay. All rights reserved.
//

import UIKit

class TabBarTransition: NSObject, UIViewControllerAnimatedTransitioning {
    let viewControllers: [UIViewController]?
    let transitionDuration: TimeInterval = 0.3

    init(viewControllers: [UIViewController]?) {
        self.viewControllers = viewControllers
    }

    func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
        return transitionDuration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let fromView = fromVC.view,
            let fromIndex = getIndex(forViewController: fromVC),
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
            let toView = toVC.view,
            let toIndex = getIndex(forViewController: toVC)
        else {
            transitionContext.completeTransition(false)
            return
        }

        let frame = transitionContext.initialFrame(for: fromVC)
        var fromFrameEnd = frame
        var toFrameStart = frame
        fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width
        toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width
        toView.frame = toFrameStart

        DispatchQueue.main.async {
            transitionContext.containerView.addSubview(toView)
            UIView.animate(withDuration: self.transitionDuration, animations: {
                fromView.frame = fromFrameEnd
                toView.frame = frame
            }, completion: { success in
                fromView.removeFromSuperview()
                transitionContext.completeTransition(success)
            })
        }
    }

    private func getIndex(forViewController vc: UIViewController) -> Int? {
        guard let vcs = self.viewControllers else { return nil }
        for (index, thisVC) in vcs.enumerated() {
            if thisVC == vc { return index }
        }
        return nil
    }
}