之前在做直播的時(shí)候,參照了映客App,發(fā)現(xiàn)其首頁的效果還挺不錯(cuò),在網(wǎng)上找了一下相關(guān)仿映客App代碼和博客,大部分都是說如何播放直播流和推流,對于UI這塊甚少,所以我自己花了點(diǎn)時(shí)間研究了一下映客的首頁UI效果。
我們來看看最終效果
從效果圖上可以看出,映客首頁主要分兩部分,一部分是實(shí)現(xiàn)沒有文字而且中間按鈕突出的TabBar
,另一部分是顯示滑動(dòng)ScrollView
隱藏和顯示NavBar
和TabBar
。我們來慢慢看。
一、TabBar實(shí)現(xiàn)
首先,我們看下實(shí)現(xiàn)后的效果。
這里我們可以使用系統(tǒng)的TabBar
來實(shí)現(xiàn)該效果。
關(guān)于如何設(shè)置系統(tǒng)的TabBar
,這里就不贅述了,可以看到我項(xiàng)目的代碼。我們來看重點(diǎn)部分。
1. 提出問題:
- 如何實(shí)現(xiàn)中間的突出按鈕
- 中間突出按鈕超出
TabBar
部分是如何響應(yīng)點(diǎn)擊的 - 如何實(shí)現(xiàn)
TabBar
中Item
圖片居中且不帶文字
(1)中間突出按鈕
要實(shí)現(xiàn)中間突出的按鈕,直接使用系統(tǒng)TabBar
還是不行,需要用一個(gè)取巧的方法,通過KVC
的方式(使用KVC
可以修改readonly
屬性,不過不能濫用哦)使用自定義的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)中間按鈕了。我們在自定義TabBar
中添加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è)置的中間圖片來的
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
類型,自定義中間按鈕為UIButton
,所以可以根據(jù)Item
類型來區(qū)分是自定義按鈕還是系統(tǒng)Item
,再調(diào)整每個(gè)Item
的位置。這里系統(tǒng)UITabBarButton
寬度為TabBar
寬度減去中間按鈕寬度的一半。
//系統(tǒng)自帶的按鈕類型是UITabBarButton,找出這些類型的按鈕,然后重新排布位置,空出中間的位置
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)整子控件位置,空出中間位置
//按鈕寬度為TabBar寬度減去中間按鈕寬度的一半
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開始的),直接讓索引++,目的就是讓消息按鈕的位置向右移動(dòng),空出來中間按鈕的位置
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
部分,是不會響應(yīng)點(diǎn)擊事件的(不信的可以自己試試哦)。要響應(yīng)點(diǎn)擊事件,這里就需要重寫UIView
的 hitTest:
方法(該方法可以決定點(diǎn)擊事件的響應(yīng)者,關(guān)于hitTest
說明,可以參見iOS-使用hitTest控制點(diǎn)擊事件的響應(yīng)對象)了。
//重寫hitTest方法,去監(jiān)聽中間按鈕的點(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é)可能就會說了,要不帶文字,不設(shè)置tabBarItem
的title
不就好了。但是title
這個(gè)NavBar
的標(biāo)題也是要用的,所以還是必須要設(shè)置。
那要怎么辦呢?其實(shí)很簡單,要實(shí)現(xiàn)該效果,以下代碼就夠了
//設(shè)置圖片居中,這里的4.5,根據(jù)實(shí)際中間按鈕圖片大小來決定
Vc.tabBarItem.imageInsets = UIEdgeInsetsMake(4.5, 0, -4.5, 0);
//設(shè)置不顯示文字,將title的位置設(shè)置成無限遠(yuǎn),就看不到了
Vc.tabBarItem.titlePositionAdjustment = UIOffsetMake(0, MAXFLOAT);
到這里,TabBar
的實(shí)現(xiàn)就結(jié)束了,下面我們來看看如何實(shí)現(xiàn)隱藏和顯示NavBar
和TabBar
。
二、隱藏和顯示NavBar和TabBar實(shí)現(xiàn)
首先,我們來看看效果
1. 提出問題:
- 如何移動(dòng)
NavBar
和TabBar
- 如何控制
NavBar
和TabBar
移動(dòng)距離 - 如何控制使
ScrollView
移動(dòng)的同時(shí)其顯示的區(qū)域正確 - 如何在手指滑動(dòng)距離較小時(shí),收起或者展開
NavBar
和TabBar
- 如何在
Push
到其他頁面,再Pop
回來后,NavBar
和TabBar
顯示正確
首先,我們要解決最基本的問題,如何讓NavBar
和TabBar
移動(dòng)
(1)移動(dòng)NavBar和TabBar
移動(dòng)的話,其實(shí)很簡單,只需要改變他們的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
的相對移動(dòng)距離了,即相對之前contentOffset.y
滑動(dòng)了多少。
在計(jì)算相對移動(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)聽ScrollView
的移動(dòng)距離,從而計(jì)算相對移動(dòng)距離deltaY
CGFloat deltaY = scrollView.contentOffset.y - _previousOffsetY;
在得到相對移動(dòng)距離后,我們就需要分別控制NavBar
和TabBar
的移動(dòng)距離了。
這里,我們可以實(shí)現(xiàn)一個(gè)分類來專門控制他們的移動(dòng)。
要注意的是:當(dāng)相對距離超出應(yīng)移動(dòng)范圍時(shí),需要對其校正
那么,我們必須先知道NavBar
和TabBar
坐標(biāo)的上下限,即其展開和收起時(shí)的Y
坐標(biāo),以下代碼openOffsetY
為展開的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ù)相對移動(dòng)距離deltaY
計(jì)算移動(dòng)后的坐標(biāo)了。
對于NavBar
,計(jì)算后的坐標(biāo)(即當(dāng)前坐標(biāo)減去deltaY
),要大于收起的坐標(biāo)小于展開的坐標(biāo)。
對于TabBar
,計(jì)算后的坐標(biāo)(即即當(dāng)前坐標(biāo)加上deltaY
),要大于展開的坐標(biāo)小于收起的坐標(biāo)。
這里畫畫圖會好理解一些。
//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;
我們再來看看ScrollView
是怎么控制移動(dòng)的。
(3)控制使ScrollView移動(dòng)的同時(shí)其顯示的區(qū)域正確
細(xì)心的童鞋可能會發(fā)現(xiàn),當(dāng)NavBar
收起或者展開的過程中,ScrollView
是跟著一起移動(dòng)的,即ScrollView
本身并沒滑動(dòng),而是Y
坐標(biāo)在改變。那如何實(shí)現(xiàn)呢?
這里,我們可以改變ScrollView
的contentInset
來滿足我們的需求,相對于改變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
顯示就有問題了。
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í),收起或者展開NavBar和TabBar
細(xì)心的童鞋可能會發(fā)現(xiàn),映客在滑動(dòng)距離比較小的時(shí)候,有的時(shí)候NavBar
和TabBar
會彈回來,有的時(shí)候會收起。這個(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))的一半。如果沒有滑動(dòng),則收起,反之展開。這里可能比較繞,我們看看代碼。
//判斷當(dāng)前`NavBar`是展開還是收起
- (BOOL)hk_shouldOpen {
CGFloat viewY = CGRectGetMinY(self.frame);
//[self hk_openOffsetY]為展開的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)知道是展開還是收起后,就可以進(jìn)行滑動(dòng)了。這里我們做一個(gè)簡單動(dòng)畫,使滑動(dòng)看起來自然一些,這里除了需要改變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)前位置到展開滑動(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的偏移量來滑動(dòng)TableView
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += navBarOffsetY;
self.tableView.contentOffset = contentOffset;
}];
(5)在Push到其他頁面,再Pop回來后,NavBar和TabBar顯示正確
在Push
到其他頁面之前,必須把NavBar
和TabBar
都展開,不然在收起的狀態(tài)Push
到其他頁面,NavBar
和TabBar
都不見了。
這里就需要在- (void)viewWillDisappear:(BOOL)animated
中將NavBar
和TabBar
都展開。
還有一個(gè)地方需要注意:
當(dāng)Push
到其他頁面的時(shí)候,如果此時(shí)ScrollView
的contentInset
不為(0,0,0,0)
時(shí),系統(tǒng)會自動(dòng)將其置為(0,0,0,0)
,這樣在Push
后,還會走到之前頁面ScrollView
的scrollViewDidScroll:
方法中,會導(dǎo)致NavBar
消失。對于這種情況,就不應(yīng)該讓其繼續(xù)走我們處理展開和收取NavBar
和TabBar
的流程。
我們可以通過以下代碼控制,當(dāng)該UIViewController
不是當(dāng)前顯示的UIViewController
時(shí),就不往下走了。
//在push到其他頁面時(shí)候,還是會走該方法,這個(gè)時(shí)候不應(yīng)該繼續(xù)執(zhí)行
if (!(self.isViewLoaded && self.view.window != nil)) {
return;
}
到這里,映客首頁的效果就實(shí)現(xiàn)好了!
Demo項(xiàng)目
該Demo項(xiàng)目地址:https://github.com/HustHank/YingKeHomeDemo
封裝的滑動(dòng)隱藏NavBar和TabBar開源控件
項(xiàng)目地址:https://github.com/HustHank/HKScrollingNavAndTabBar
如果覺得該文章對你有用,請幫忙點(diǎn)贊或者Star
我的GitHub項(xiàng)目,謝謝!