之前在做直播的時(shí)候,參照了映客App,發(fā)現(xiàn)其首頁(yè)的效果還挺不錯(cuò),在網(wǎng)上找了一下相關(guān)仿映客App代碼和博客,大部分都是說(shuō)如何播放直播流和推流,對(duì)于UI這塊甚少,所以我自己花了點(diǎn)時(shí)間研究了一下映客的首頁(yè)UI效果。
我們來(lái)看看最終效果

從效果圖上可以看出,映客首頁(yè)主要分兩部分,一部分是實(shí)現(xiàn)沒(méi)有文字而且中間按鈕突出的TabBar,另一部分是顯示滑動(dòng)ScrollView隱藏和顯示NavBar和TabBar。我們來(lái)慢慢看。
一、TabBar實(shí)現(xiàn)
首先,我們看下實(shí)現(xiàn)后的效果。

這里我們可以使用系統(tǒng)的TabBar來(lái)實(shí)現(xiàn)該效果。
關(guān)于如何設(shè)置系統(tǒng)的TabBar,這里就不贅述了,可以看到我項(xiàng)目的代碼。我們來(lái)看重點(diǎn)部分。
1. 提出問(wèn)題:
- 如何實(shí)現(xiàn)中間的突出按鈕
- 中間突出按鈕超出
TabBar部分是如何響應(yīng)點(diǎn)擊的 - 如何實(shí)現(xiàn)
TabBar中Item圖片居中且不帶文字
(1)中間突出按鈕
要實(shí)現(xiàn)中間突出的按鈕,直接使用系統(tǒng)TabBar還是不行,需要用一個(gè)取巧的方法,通過(guò)KVC的方式(使用KVC可以修改readonly屬性,不過(guò)不能濫用哦)使用自定義的TabBar替換系統(tǒng)UITabBar。
//創(chuàng)建自己的tabbar,然后用KVC將自己的tabbar和系統(tǒng)的tabBar替換下
HKTabBar *tabbar = [[HKTabBar alloc] init];
//KVC實(shí)質(zhì)是修改了系統(tǒng)的_tabBar
[self setValue:tabbar forKeyPath:@"tabBar"];
替換了系統(tǒng)UITabBar后,就需要實(shí)現(xiàn)中間按鈕了。我們?cè)谧远xTabBar中添加UIButton,作為中間按鈕。
1.在HKTabBar的initWithFrame:方法中,初始化中間按鈕
//設(shè)置中間按鈕圖片和尺寸
UIButton *centerBtn = [[UIButton alloc] init];
[centerBtn setBackgroundImage:[UIImage imageNamed:@"tab_launch"] forState:UIControlStateNormal];
[centerBtn setBackgroundImage:[UIImage imageNamed:@"tab_launch"] forState:UIControlStateHighlighted];
//這里button的size是根據(jù)需要設(shè)置的中間圖片來(lái)的
centerBtn.size = centerBtn.currentBackgroundImage.size;
[centerBtn addTarget:self action:@selector(centerBtnDidClick) forControlEvents:UIControlEventTouchUpInside];
self.centerBtn = centerBtn;
[self addSubview:centerBtn];
2.在layoutSubviews中設(shè)置中間按鈕和其他Item位置
由于系統(tǒng)Item是UITabBarButton類(lèi)型,自定義中間按鈕為UIButton,所以可以根據(jù)Item類(lèi)型來(lái)區(qū)分是自定義按鈕還是系統(tǒng)Item,再調(diào)整每個(gè)Item的位置。這里系統(tǒng)UITabBarButton寬度為TabBar寬度減去中間按鈕寬度的一半。
//系統(tǒng)自帶的按鈕類(lèi)型是UITabBarButton,找出這些類(lèi)型的按鈕,然后重新排布位置,空出中間的位置
Class class = NSClassFromString(@"UITabBarButton");
self.centerBtn.centerX = self.centerX;
//調(diào)整中間按鈕的中線點(diǎn)Y值
self.centerBtn.centerY = (self.height - (self.centerBtn.height - self.height)) * 0.5;
NSInteger btnIndex = 0;
for (UIView *btn in self.subviews) {//遍歷tabbar的子控件
if ([btn isKindOfClass:class]) {//如果是系統(tǒng)的UITabBarButton,那么就調(diào)整子控件位置,空出中間位置
//按鈕寬度為T(mén)abBar寬度減去中間按鈕寬度的一半
btn.width = (self.width - self.centerBtn.width) * 0.5;
//中間按鈕前的寬度,這里就3個(gè)按鈕,中間按鈕Index為1
if (btnIndex < 1) {
btn.x = btn.width * btnIndex;
} else { //中間按鈕后的寬度
btn.x = btn.width * btnIndex + self.centerBtn.width;
}
btnIndex++;
//如果是索引是0(從0開(kāi)始的),直接讓索引++,目的就是讓消息按鈕的位置向右移動(dòng),空出來(lái)中間按鈕的位置
if (btnIndex == 0) {
btnIndex++;
}
}
}
[self bringSubviewToFront:self.centerBtn];
到這里,中間按鈕就實(shí)現(xiàn)好了,但是如何讓超出TabBar部分(即紅色框部分)響應(yīng)點(diǎn)擊事件呢?

(2)超出TabBar部分響應(yīng)點(diǎn)擊
按照系統(tǒng)默認(rèn)處理方式,超出TabBar部分,是不會(huì)響應(yīng)點(diǎn)擊事件的(不信的可以自己試試哦)。要響應(yīng)點(diǎn)擊事件,這里就需要重寫(xiě)UIView 的 hitTest:方法(該方法可以決定點(diǎn)擊事件的響應(yīng)者,關(guān)于hitTest說(shuō)明,可以參見(jiàn)iOS-使用hitTest控制點(diǎn)擊事件的響應(yīng)對(duì)象)了。
//重寫(xiě)hitTest方法,去監(jiān)聽(tīng)中間按鈕的點(diǎn)擊,目的是為了讓凸出的部分點(diǎn)擊也有反應(yīng)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//判斷當(dāng)前手指是否點(diǎn)擊到中間按鈕上,如果是,則響應(yīng)按鈕點(diǎn)擊,其他則系統(tǒng)處理
//首先判斷當(dāng)前View是否被隱藏了,隱藏了就不需要處理了
if (self.isHidden == NO) {
//將當(dāng)前tabbar的觸摸點(diǎn)轉(zhuǎn)換坐標(biāo)系,轉(zhuǎn)換到中間按鈕的身上,生成一個(gè)新的點(diǎn)
CGPoint newP = [self convertPoint:point toView:self.centerBtn];
//判斷如果這個(gè)新的點(diǎn)是在中間按鈕身上,那么處理點(diǎn)擊事件最合適的view就是中間按鈕
if ( [self.centerBtn pointInside:newP withEvent:event]) {
return self.centerBtn;
}
}
return [super hitTest:point withEvent:event];
}
處理完突出部分,就剩下不帶文字的Item了。
(3) TabBar中Item圖片居中且不帶文字
有的同學(xué)可能就會(huì)說(shuō)了,要不帶文字,不設(shè)置tabBarItem的title不就好了。但是title這個(gè)NavBar的標(biāo)題也是要用的,所以還是必須要設(shè)置。
那要怎么辦呢?其實(shí)很簡(jiǎn)單,要實(shí)現(xiàn)該效果,以下代碼就夠了
//設(shè)置圖片居中,這里的4.5,根據(jù)實(shí)際中間按鈕圖片大小來(lái)決定
Vc.tabBarItem.imageInsets = UIEdgeInsetsMake(4.5, 0, -4.5, 0);
//設(shè)置不顯示文字,將title的位置設(shè)置成無(wú)限遠(yuǎn),就看不到了
Vc.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, MAXFLOAT);
到這里,TabBar的實(shí)現(xiàn)就結(jié)束了,下面我們來(lái)看看如何實(shí)現(xiàn)隱藏和顯示NavBar和TabBar。
二、隱藏和顯示NavBar和TabBar實(shí)現(xiàn)
首先,我們來(lái)看看效果

1. 提出問(wèn)題:
- 如何移動(dòng)
NavBar和TabBar - 如何控制
NavBar和TabBar移動(dòng)距離 - 如何控制使
ScrollView移動(dòng)的同時(shí)其顯示的區(qū)域正確 - 如何在手指滑動(dòng)距離較小時(shí),收起或者展開(kāi)
NavBar和TabBar - 如何在
Push到其他頁(yè)面,再Pop回來(lái)后,NavBar和TabBar顯示正確
首先,我們要解決最基本的問(wèn)題,如何讓NavBar和TabBar移動(dòng)
(1)移動(dòng)NavBar和TabBar
移動(dòng)的話,其實(shí)很簡(jiǎn)單,只需要改變他們的Y坐標(biāo)即可。
//這里的self就是NavBar或者TabBar
CGRect viewFrame = self.frame;
viewFrame.origin.y = newOffsetY;
self.frame = viewFrame;
(2)控制NavBar和TabBar移動(dòng)距離
移動(dòng)距離,就要取決于ScrollView的相對(duì)移動(dòng)距離了,即相對(duì)之前contentOffset.y滑動(dòng)了多少。
在計(jì)算相對(duì)移動(dòng)距離之前,我們需要獲取上次滑動(dòng)ScrollView的contentOffset.y,我們可以在- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView中獲取上次滑動(dòng)ScrollView的contentOffset.y,即_previousOffsetY,
_previousOffsetY = scrollView.contentOffset.y;
之后實(shí)現(xiàn)ScrollView的委托方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView,在其中監(jiān)聽(tīng)ScrollView的移動(dòng)距離,從而計(jì)算相對(duì)移動(dòng)距離deltaY
CGFloat deltaY = scrollView.contentOffset.y - _previousOffsetY;
在得到相對(duì)移動(dòng)距離后,我們就需要分別控制NavBar和TabBar的移動(dòng)距離了。
這里,我們可以實(shí)現(xiàn)一個(gè)分類(lèi)來(lái)專(zhuān)門(mén)控制他們的移動(dòng)。
要注意的是:當(dāng)相對(duì)距離超出應(yīng)移動(dòng)范圍時(shí),需要對(duì)其校正
那么,我們必須先知道NavBar和TabBar坐標(biāo)的上下限,即其展開(kāi)和收起時(shí)的Y坐標(biāo),以下代碼openOffsetY為展開(kāi)的Y坐標(biāo),closeOffsetY為收起的Y坐標(biāo)。
//NavBar
//kStatusBarHeight為狀態(tài)欄高度
openOffsetY = kStatusBarHeight;
closeOffsetY = -CGRectGetHeight(self.frame;
//TabBar
//kScreenHeight為屏幕寬度,hk_extraDistance為中間按鈕突出的距離
openOffsetY = kScreenHeight - CGRectGetHeight(self.frame);
closeOffsetY = kScreenHeight + self.hk_extraDistance;
以下坐標(biāo)都代表Y坐標(biāo),我們這里只做豎直方向移動(dòng)
知道可以移動(dòng)的范圍后,就可以根據(jù)相對(duì)移動(dòng)距離deltaY計(jì)算移動(dòng)后的坐標(biāo)了。
對(duì)于NavBar,計(jì)算后的坐標(biāo)(即當(dāng)前坐標(biāo)減去deltaY),要大于收起的坐標(biāo)小于展開(kāi)的坐標(biāo)。
對(duì)于TabBar,計(jì)算后的坐標(biāo)(即即當(dāng)前坐標(biāo)加上deltaY),要大于展開(kāi)的坐標(biāo)小于收起的坐標(biāo)。
這里畫(huà)畫(huà)圖會(huì)好理解一些。
//NavBar最終要移動(dòng)的Y坐標(biāo)
newOffsetY = CGRectGetMinY(self.frame) - deltaY;
newOffsetY = MAX(closeOffsetY, MIN(openOffsetY, newOffsetY));
//TabBar最終要移動(dòng)的Y坐標(biāo)
newOffsetY = CGRectGetMinY(self.frame) + deltaY;
newOffsetY = MIN(closeOffsetY, MAX(openOffsetY, newOffsetY));
之后,就只要將NavBar和TabBar移動(dòng)到指定坐標(biāo)即可
CGRect viewFrame = self.frame;
viewFrame.origin.y = newOffsetY;
self.frame = viewFrame;
我們?cè)賮?lái)看看ScrollView是怎么控制移動(dòng)的。
(3)控制使ScrollView移動(dòng)的同時(shí)其顯示的區(qū)域正確
細(xì)心的童鞋可能會(huì)發(fā)現(xiàn),當(dāng)NavBar收起或者展開(kāi)的過(guò)程中,ScrollView是跟著一起移動(dòng)的,即ScrollView本身并沒(méi)滑動(dòng),而是Y坐標(biāo)在改變。那如何實(shí)現(xiàn)呢?
這里,我們可以改變ScrollView的contentInset來(lái)滿(mǎn)足我們的需求,相對(duì)于改變ScrollView的frame要方便很多哦。
我們要根據(jù)NavBar和TabBar移動(dòng)后的坐標(biāo),改變ScrollView的contentInset的top和bottom。
top取NavBar的MaxY,就是當(dāng)前Y坐標(biāo)加上本身的高度。
bottom取TabBar突出的距離,即屏幕高度減去其Y坐標(biāo)大于0的部分。
這里要注意:ScrollView的scrollIndicatorInsets同時(shí)也需要更新,不然Indicator顯示就有問(wèn)題了。
CGFloat navBarMaxY = CGRectGetMaxY(self.navigationController.navigationBar.frame);
CGFloat tabBarMinY = CGRectGetMinY(self.tabBarController.tabBar.frame);
UIEdgeInsets scrollViewInset = self.tableView.contentInset;
scrollViewInset.top = navBarMaxY;
scrollViewInset.bottom = MAX(0, kScreenHeight - tabBarMinY);
self.tableView.contentInset = scrollViewInset;
self.tableView.scrollIndicatorInsets = scrollViewInset;
(4)在手指滑動(dòng)距離較小時(shí),收起或者展開(kāi)NavBar和TabBar
細(xì)心的童鞋可能會(huì)發(fā)現(xiàn),映客在滑動(dòng)距離比較小的時(shí)候,有的時(shí)候NavBar和TabBar會(huì)彈回來(lái),有的時(shí)候會(huì)收起。這個(gè)是怎么做的呢?
這個(gè)就需要在停止滑動(dòng)的時(shí)候處理,我們可以在ScrollView的委托方法- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate中處理。
在滑動(dòng)時(shí),要判斷當(dāng)前NavBar或者TabBar滑動(dòng)距離,是否滑到到最大坐標(biāo)(最大坐標(biāo)減去最小坐標(biāo))的一半。如果沒(méi)有滑動(dòng),則收起,反之展開(kāi)。這里可能比較繞,我們看看代碼。
//判斷當(dāng)前`NavBar`是展開(kāi)還是收起
- (BOOL)hk_shouldOpen {
CGFloat viewY = CGRectGetMinY(self.frame);
//[self hk_openOffsetY]為展開(kāi)的Y坐標(biāo),[self hk_closeOffsetY] 為收起的Y坐標(biāo)
CGFloat viewMinY = [self hk_openOffsetY];
viewMinY = [self hk_closeOffsetY] + ([self hk_openOffsetY] - [self hk_closeOffsetY]) * 0.5;
if (viewY <= viewMinY) {
return NO;
}
return YES;
}
當(dāng)知道是展開(kāi)還是收起后,就可以進(jìn)行滑動(dòng)了。這里我們做一個(gè)簡(jiǎn)單動(dòng)畫(huà),使滑動(dòng)看起來(lái)自然一些,這里除了需要改變ScrollView的contentInset,還需要改變其contentOffset,因?yàn)?code>NavBar和TabBar移動(dòng)了,ScrollView也要跟著一起移動(dòng)。
[UIView animateWithDuration:0.2 animations:^{
CGFloat navBarOffsetY = 0;
if (opening) {
//navBarOffsetY為NavBar從當(dāng)前位置到展開(kāi)滑動(dòng)的距離
navBarOffsetY = [self.navigationController.navigationBar hk_open];
[self.tabBarController.tabBar hk_open];
} else {
//navBarOffsetY為NavBar從當(dāng)前位置到收起滑動(dòng)的距離
navBarOffsetY = [self.navigationController.navigationBar hk_close];
[self.tabBarController.tabBar hk_close];
}
//更新TableView的contentInset
[self updateScrollViewInset];
//根據(jù)NavBar的偏移量來(lái)滑動(dòng)TableView
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += navBarOffsetY;
self.tableView.contentOffset = contentOffset;
}];
(5)在Push到其他頁(yè)面,再Pop回來(lái)后,NavBar和TabBar顯示正確
在Push到其他頁(yè)面之前,必須把NavBar和TabBar都展開(kāi),不然在收起的狀態(tài)Push到其他頁(yè)面,NavBar和TabBar都不見(jiàn)了。
這里就需要在- (void)viewWillDisappear:(BOOL)animated中將NavBar和TabBar都展開(kāi)。
還有一個(gè)地方需要注意:
當(dāng)Push到其他頁(yè)面的時(shí)候,如果此時(shí)ScrollView的contentInset不為(0,0,0,0)時(shí),系統(tǒng)會(huì)自動(dòng)將其置為(0,0,0,0),這樣在Push后,還會(huì)走到之前頁(yè)面ScrollView的scrollViewDidScroll:方法中,會(huì)導(dǎo)致NavBar消失。對(duì)于這種情況,就不應(yīng)該讓其繼續(xù)走我們處理展開(kāi)和收取NavBar和TabBar的流程。
我們可以通過(guò)以下代碼控制,當(dāng)該UIViewController不是當(dāng)前顯示的UIViewController時(shí),就不往下走了。
//在push到其他頁(yè)面時(shí)候,還是會(huì)走該方法,這個(gè)時(shí)候不應(yīng)該繼續(xù)執(zhí)行
if (!(self.isViewLoaded && self.view.window != nil)) {
return;
}
到這里,映客首頁(yè)的效果就實(shí)現(xiàn)好了!
Demo項(xiàng)目
該Demo項(xiàng)目地址:https://github.com/HustHank/YingKeHomeDemo
封裝的滑動(dòng)隱藏NavBar和TabBar開(kāi)源控件
項(xiàng)目地址:https://github.com/HustHank/HKScrollingNavAndTabBar
如果覺(jué)得該文章對(duì)你有用,請(qǐng)幫忙點(diǎn)贊或者Star我的GitHub項(xiàng)目,謝謝!