仿淘寶tabBar點選及滑動時logo和火箭?切換動畫

Climb發表於2019-05-09

最近專案改版裡,產品設計重新設計了tabbar動畫,旨在提升app的逼格。。。 設計圖是借鑑淘寶的tabBar:

仿淘寶tabBar點選及滑動時logo和火箭?切換動畫

找資料查詢下,還沒有相關開源的程式碼,好吧,那就自己開幹吧。

先拆解功能點:

  • 自定義tabBar,高度56
  • tab顯示:首頁tab在選中時是大logo 無文字,未選中時是圖片文字 ;其他tab未選中和選中都是圖片文字
  • tab切換:tab之間相互點選切換選中時的縮小放大的動畫
  • 當首頁滑動到一定距離時,首頁tab的大logo和小火箭執行切換動畫:手勢上滑 - logo向下切換到小火箭; 手勢下滑 - 小火箭向上切換到logo;

開始各個擊破吧

1. 自定義tabBar,高度56

刪除系統tabBar,建立自定義tabBar,

@implementation WMTabBar

// 刪除系統tabbar的UITabBarButton
- (void)layoutSubviews {
    [super layoutSubviews];
    
    for (UIView *view in self.subviews) {
        if ([view isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
            [view removeFromSuperview];
        }
    }
}

// 根據配置資訊建立自定義tabBar
+ (instancetype)tabBarWithTitleArray:(NSArray *)titleArray imageArray:(NSArray *)imageArray selectedImageArray:(NSArray *)selectedImageArray {
    WMTabBar *tabBar = [[WMTabBar alloc] init];
    tabBar.titleArray = titleArray;
    tabBar.imageArray = imageArray;
    tabBar.selectedImageArray = selectedImageArray;
    [tabBar setupUI];
    return tabBar;
}

- (void)setupUI {
    self.lastSelectIndex = 100;//預設為100
    self.backgroundColor = [UIColor whiteColor];
    for (int i = 0; i < self.titleArray.count; i++) {
        CGFloat itemWidth = (kScreen_width/self.titleArray.count);
        CGRect frame = CGRectMake(i*itemWidth, 0, itemWidth, 56);
        WMTabBarItem *tabBarItem = [[WMTabBarItem alloc] initWithFrame:frame index:i];
        tabBarItem.tag = i ;
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tabBarItemClickAction:)];
        [tabBarItem addGestureRecognizer:tap];
        [self addSubview:tabBarItem];
        [self.itemArray addObject:tabBarItem];
    }
    // 記錄選中index, 在其setter方法裡重寫邏輯
    self.selectedIndex = 0;
}

<!-- 選中index 重寫setter方法,在裡面做tabBar的item的內容動態配置 -->
- (void)setSelectedIndex:(NSInteger )selectedIndex {
    _selectedIndex = selectedIndex;
    [self.itemArray enumerateObjectsUsingBlock:^(WMTabBarItem *tabBarItem, NSUInteger idx, BOOL * _Nonnull stop) {
        // 當遍歷的idx=selectedIndex時,記錄選中狀態
        BOOL selected = (idx == selectedIndex);
        // 配置tabBarItem的內容資訊
        [tabBarItem configTitle:self.titleArray[idx] normalImage:self.imageArray[idx] selectedImage:self.selectedImageArray[idx] index:idx selected:selected lastSelectIndex:self.lastSelectIndex];
        // 當遍歷到最後一個時,賦值lastSelectIndex
        if (idx == (self.itemArray.count-1)) {
            self.lastSelectIndex = selectedIndex;
        }
    }];
}
複製程式碼

在WMTabBar裡仿照系統tabBar的點選方法,新增兩個wmTabBar代理方法,以滿足專案裡tabBar點選選中或點選tabBar時根據登入狀態進行是否選中等操作:

@protocol WMTabBarDelegate <NSObject>

/** 選中tabbar */
- (void)wmtabBar:(WMTabBar *)wmTabBar selectedWMTabBarItemAtIndex:(NSInteger)index;
/** 是否可選tabbar */
- (BOOL)wmtabBar:(WMTabBar *)wmTabBar shouldSelectedWMTabBarItemAtIndex:(NSInteger)index;

@end
複製程式碼

在WMTabBarController裡設定tabBar

// 重寫tabbar的frame,改變tabbar高度
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    
    CGRect frame = self.tabBar.frame;
    if (frame.size.height != kTABBARHEIGHT) {
        frame.size.height = kTABBARHEIGHT;
        frame.origin.y = self.view.frame.size.height - frame.size.height;
        self.tabBar.frame = frame;
    }
}

//新增子模組
- (void)creatTabBarWithChildVCArray:(NSArray *)childVCArray
                         titleArray:(NSArray *)titleArray
                         imageArray:(NSArray *)imageArray
                 selectedImageArray:(NSArray *)selectedImageArray {
    
    for (UIViewController *viewController in childVCArray) {
        MPNavigationController *navigationController = [[MPNavigationController alloc] initWithRootViewController:viewController];
        [self.controllersArray addObject:navigationController];
    }
    
    self.wmTabBar = [WMTabBar tabBarWithTitleArray:titleArray
                                      imageArray:imageArray
                              selectedImageArray:selectedImageArray];
    self.wmTabBar.tabBarDelegate = self;
    [self setValue:self.wmTabBar forKeyPath:@"tabBar"];
    self.viewControllers = self.controllersArray;
}
複製程式碼

2. tab顯示:首頁tab在選中時是大logo 無文字,未選中時是圖片文字 ;其他tab未選中和選中都是圖片文字

在tabBarItem裡建立顯示UI
@implementation WMTabBarItem

- (instancetype)initWithFrame:(CGRect)frame index:(NSInteger )index {
    self = [super initWithFrame:frame];
    if (self) {
        
        [self addSubview:self.titleLabel];
        [self addSubview:self.imageView];
        [self addSubview:self.homeTabSelectedBgView];
        
        self.imageView.frame  = CGRectMake(self.bounds.size.width/2-14, 7, 28, 28);
        self.titleLabel.frame = CGRectMake(0, CGRectGetMaxY(self.imageView.frame)+2, self.bounds.size.width, 14);
        self.homeTabSelectedBgView.frame = CGRectMake(self.bounds.size.width/2-21, 7, 42, 42);

        if (index == 0) {
            // 當為首頁tab時,加上以下內容
            [self addSubview:self.homeTabSelectedBgView];
            
//            /** 第一種方案 */
//            [self.homeTabSelectedBgView addSubview:self.homeTabAnimateImageView];
//            self.homeTabAnimateImageView.frame  = CGRectMake(0, 0, 32, 32);
//            self.homeTabAnimateImageView.center = CGPointMake(self.homeTabSelectedBgView.frame.size.width/2, self.homeTabSelectedBgView.frame.size.height/2);
            
            /** 第二種方案 */
            [self.homeTabSelectedBgView addSubview:self.collectionView];
            self.homeTabSelectedBgView.frame  = CGRectMake(self.bounds.size.width/2-21, 7, 42, 42);
            self.collectionView.frame  = CGRectMake(0, 0, 42, 42);
            self.collectionView.center = CGPointMake(self.homeTabSelectedBgView.frame.size.width/2, self.homeTabSelectedBgView.frame.size.height/2);

            [self.collectionView registerClass:[WMTabBarItemCell class]
                    forCellWithReuseIdentifier:NSStringFromClass([WMTabBarItemCell class])];
        }
    }
    return self;
}
複製程式碼
在tab點選事件裡處理:

// 當點選tab的item時,執行
- (void)configTitle:(NSString *)title normalImage:(NSString *)normalImage selectedImage:(NSString *)selectedImage index:(NSInteger)index selected:(BOOL)selected lastSelectIndex:(NSInteger )lastSelectIndex {
    self.titleLabel.text = title;
    // 當index == 0, 即首頁tab
    if (index == 0) {
        if (selected) {
            [self.homeTabSelectedBgView setImage:[UIImage imageNamed:@"tabbar_home_selecetedBg"]];
            self.homeTabSelectedBgView.hidden = NO;
             YES;self.imageView.hidden = self.titleLabel.hidden = YES;
            
//            /** 第一種方案 */
//            [self.homeTabAnimateImageView setImage:[UIImage imageNamed:@"tabbar_home_selecetedLogo"]];
//            /** 第二種方案 預設顯示第一個cell 所以不用在這裡設定圖片 */
            
            // 如果本次點選和上次是同一個tab 都是第0個,則執行push動畫,否則執行放大縮小動畫
            if (lastSelectIndex == index) {
                if (self.flag) {
                    // 如果已經是火箭狀態,則點選切換logo,且發通知 讓首頁滑到頂部
                    [self pushHomeTabAnimationDown];
                    [[NSNotificationCenter defaultCenter] postNotificationName:@"kPushDownAnimationScrollTopNotification" object:nil];
                }
            }else {
                [self animationWithHomeTab];
            }
        }else {
            [self.imageView setImage:[UIImage imageNamed:normalImage]];
            self.homeTabSelectedBgView.hidden = YES;
            self.imageView.hidden = self.titleLabel.hidden = NO;
        }
    }else {
        // 其他tab
        self.homeTabSelectedBgView.hidden = YES;
        self.imageView.hidden = self.titleLabel.hidden = NO;
        if (selected) {
            [self.imageView setImage:[UIImage imageNamed:selectedImage]];
            self.titleLabel.textColor = [self colorFromHexRGB:@"18A2FF"];
            // 如果本次點選和上次是同一個tab 則無反應,否則執行放大縮小動畫
            if (lastSelectIndex != index) {
                [self animationWithNormalTab];
            }
        }else {
            [self.imageView setImage:[UIImage imageNamed:normalImage]];
            self.titleLabel.textColor = [self colorFromHexRGB:@"575D66"];
        }
    }
}
複製程式碼

就是在tab點選事件裡做處理:

  • 當index==0 即首頁tab時,根據是否是seleted狀態 來判斷顯示是大logo控制元件還是正常文字圖片控制元件。
  • 其他tab時,隱藏大logo控制元件,只顯示圖片文字控制元件,再根據是否是seleted狀態 來判斷選中和未選中時的圖片及文字顏色配置。

3.tab切換:tab之間相互點選切換選中時的縮小放大的動畫

tab在相互點選切換選中時的縮小放大動畫,通過CABasicAnimation來實現:

/** 
 * tab之間切換動畫 
 */
// 首頁tab縮小放大動畫 
- (void)animationForHomeTab {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.duration  = 0.2f;
    animation.fromValue = [NSNumber numberWithFloat:0.5f];
    animation.toValue   = [NSNumber numberWithFloat:1.f];
    [self.homeTabSelectedBgView.layer addAnimation:animation forKey:nil];
}

// 其他tab縮小放大動畫
- (void)animationForNormalTab {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.duration  = 0.25f;
    animation.fromValue = [NSNumber numberWithFloat:0.5f];
    animation.toValue   = [NSNumber numberWithFloat:1.f];
    [self.imageView.layer  addAnimation:animation forKey:nil];
    [self.titleLabel.layer addAnimation:animation forKey:nil];
}

複製程式碼

效果如下:

仿淘寶tabBar點選及滑動時logo和火箭?切換動畫

4.當首頁滑動到一定距離時,首頁tab的大logo和小火箭執行切換動畫

觀察設計圖,可以得出根據首頁的滑動偏移量及滑動手勢,來確定滑動動畫方案:

在一次滑動行為中:當偏移量>閾值,且手勢上滑 - logo向下切換到小火箭;當偏移量<閾值,且手勢下滑 - 小火箭向上切換到logo;

首頁裡滑動代理實現:
// tabBar動畫 - 判斷滑動手勢,再根據手勢,偏移量 判斷動畫型別
- (void)tabBarAnimateWithScrollView:(UIScrollView *)scrollView {
    CGFloat currentPostionOffsetY = scrollView.contentOffset.y;
    if (currentPostionOffsetY > self.lastPositionOffestY) {
        NSLog(@"手勢上滑");
        // tabBar動畫
        if (self.tabAnimateOnceScrollFlag) {
            // 在一次滑動中,且currentPostionOffsetY>456,執行logo切換火箭?動畫
            if ((currentPostionOffsetY > 456.f)) {
                NSLog(@"執行-切換火箭");
                [[AppLoginHandle sharedInstance].tabBarController pushHomeTabBarAnimationType:anmationDirectionUp];
                self.tabAnimateOnceScrollFlag = NO;
            }
        }
    }else {
        NSLog(@"手勢下滑");
        // tabBar動畫
         if (self.tabAnimateOnceScrollFlag) {
             // 在一次滑動中,下滑手勢, 且currentPostionOffsetY<456,執行火箭?切換logo動畫
             if ((currentPostionOffsetY < 456.f)) {
                 NSLog(@"觸發-切換logo");
                 [[AppLoginHandle sharedInstance].tabBarController pushHomeTabBarAnimationType:anmationDirectionDown];
                 self.tabAnimateOnceScrollFlag = NO;
             }
         }
    }
}

// 當開始滾動檢視時,執行該方法。一次有效滑動(開始滑動,滑動一小段距離,只要手指不鬆開,只算一次滑動),只執行一次。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    if ([scrollView isEqual:self.collectionView]) {
        self.tabAnimateOnceScrollFlag = YES;
        self.lastPositionOffestY = scrollView.contentOffset.y;
    }
}

複製程式碼
WMTabBar裡暴露出外部呼叫切換動畫的方法如下:
// logo和火箭切換動畫的列舉anmationDirection
typedef NS_ENUM(NSUInteger, anmationDirection) {
    anmationDirectionUp,//push動畫,火箭頭出來,logo下去
    anmationDirectionDown,//push動畫,火箭頭下去,logo出來
};
@class WMTabBar;
@protocol WMTabBarDelegate <NSObject>

/** 選中tabbar */
- (void)wmtabBar:(WMTabBar *)wmTabBar didSelectWMTabBarItemAtIndex:(NSInteger)index;
/** 是否可選tabbar */
- (BOOL)wmtabBar:(WMTabBar *)wmTabBar shouldSelectWMTabBarItemAtIndex:(NSInteger)index;

@end

@interface WMTabBar : UITabBar

@property (nonatomic, weak) id <WMTabBarDelegate> tabBarDelegate;
@property (nonatomic, assign) anmationDirection anmationDirection;
/** 例項化 */
+ (instancetype)tabBarWithTitleArray:(NSArray *)titleArray imageArray:(NSArray *)imageArray selectedImageArray:(NSArray *)selectedImageArray;

// 外部指定跳轉到某個tab時呼叫
- (void)selectedTabbarAtIndex:(NSNumber *)index;
// 暴露外部的切換動畫logo和火箭的方法
- (void)pushHomeTabBarAnimationType:(anmationDirection )anmationDirection;

@end
複製程式碼
WMTabBarItem裡實現首頁tab的小火箭和logo之間切換動畫
這裡寫了兩種方法,第一種是最開始嘗試的,通過CATransition的push動畫,來達到一個imageView控制元件的兩個圖片切換,如下:
  • 第一種方案 push動畫方案:
// push動畫,火箭頭出來
-(void)pushHomeTabAnimationUp {
    self.flag = YES;
    //    /** 第一種方案 */
    //    [self.homeTabAnimateImageView setImage:[UIImage imageNamed:@"tabbar_home_selecetedPush"]];
    //    CATransition *animation = [CATransition animation];
    //    animation.type = kCATransitionPush;//設定動畫的型別
    //    animation.subtype = kCATransitionFromTop; //設定動畫的方向
    //    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    //    animation.duration = 0.25f;
    //    [self.homeTabAnimateImageView.layer addAnimation:animation forKey:@"pushAnimation"];
}

// push動畫,火箭頭落下
-(void)pushHomeTabAnimationDown {
    self.flag = NO;
    //    /** 第一種方案 */
    //    [self.homeTabAnimateImageView setImage:[UIImage imageNamed:@"tabbar_home_selecetedLogo"]];
    //    CATransition *animation = [CATransition animation];
    //    animation.type = kCATransitionPush;//設定動畫的型別
    //    animation.subtype = kCATransitionFromBottom; //設定動畫的方向
    //    animation.duration = 0.25f;
    //    [self.homeTabAnimateImageView.layer addAnimation:animation forKey:@"pushAnimation"];
}
複製程式碼

效果如下:

仿淘寶tabBar點選及滑動時logo和火箭?切換動畫

雖然能實現功能,但在切換過程中,會有殘留效果,這顯示是不太美好的,再想想有沒有更好的實現方法。

然後就想到了collectionView了。

  • 第二種方案 collection滑動item方案:
把collectionView設定成首頁tab大小,通過讓collection的cell的scrollToItem方法, 即滑動到第n個item來達到切換動畫效果,如下:
// push動畫,火箭頭出來
-(void)pushHomeTabAnimationUp {
    self.flag = YES;
    /** 第二種方案 */
    [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}

// push動畫,火箭頭落下
-(void)pushHomeTabAnimationDown {
    self.flag = NO;
    /** 第二種方案 */
    [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
複製程式碼

仿淘寶tabBar點選及滑動時logo和火箭?切換動畫

漂亮!

到此,新版tabBar實現就完成了!

demo地址戳這裡

相關文章