iOS-實(shí)現(xiàn)映客首頁TabBar和滑動(dòng)隱藏NavBar和TabBar

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

我們來看看最終效果


滑動(dòng)隱藏NavBar和TabBar效果

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

一、TabBar實(shí)現(xiàn)

首先,我們看下實(shí)現(xiàn)后的效果。


映客底部TabBar效果

這里我們可以使用系統(tǒng)的TabBar來實(shí)現(xiàn)該效果。

關(guān)于如何設(shè)置系統(tǒng)的TabBar,這里就不贅述了,可以看到我項(xiàng)目的代碼。我們來看重點(diǎn)部分。

1. 提出問題:

  1. 如何實(shí)現(xiàn)中間的突出按鈕
  2. 中間突出按鈕超出TabBar部分是如何響應(yīng)點(diǎn)擊的
  3. 如何實(shí)現(xiàn)TabBarItem圖片居中且不帶文字
(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.在HKTabBarinitWithFrame:方法中,初始化中間按鈕

//設(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)ItemUITabBarButton類型,自定義中間按鈕為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)擊事件呢?

TabBar中間按鈕突出位置

(2)超出TabBar部分響應(yīng)點(diǎn)擊

按照系統(tǒng)默認(rèn)處理方式,超出TabBar部分,是不會響應(yīng)點(diǎn)擊事件的(不信的可以自己試試哦)。要響應(yīng)點(diǎn)擊事件,這里就需要重寫UIViewhitTest:方法(該方法可以決定點(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è)置tabBarItemtitle不就好了。但是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)隱藏和顯示NavBarTabBar

二、隱藏和顯示NavBar和TabBar實(shí)現(xiàn)

首先,我們來看看效果


滑動(dòng)隱藏NavBar和TabBar效果

1. 提出問題:

  1. 如何移動(dòng)NavBarTabBar
  2. 如何控制NavBarTabBar移動(dòng)距離
  3. 如何控制使ScrollView移動(dòng)的同時(shí)其顯示的區(qū)域正確
  4. 如何在手指滑動(dòng)距離較小時(shí),收起或者展開NavBarTabBar
  5. 如何在Push到其他頁面,再Pop回來后,NavBarTabBar顯示正確

首先,我們要解決最基本的問題,如何讓NavBarTabBar移動(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)ScrollViewcontentOffset.y,我們可以在- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView中獲取上次滑動(dòng)ScrollViewcontentOffset.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)距離后,我們就需要分別控制NavBarTabBar的移動(dòng)距離了。
這里,我們可以實(shí)現(xiàn)一個(gè)分類來專門控制他們的移動(dòng)。

要注意的是:當(dāng)相對距離超出應(yīng)移動(dòng)范圍時(shí),需要對其校正
那么,我們必須先知道NavBarTabBar坐標(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));   

之后,就只要將NavBarTabBar移動(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)呢?

這里,我們可以改變ScrollViewcontentInset來滿足我們的需求,相對于改變ScrollViewframe要方便很多哦。

我們要根據(jù)NavBarTabBar移動(dòng)后的坐標(biāo),改變ScrollViewcontentInsettopbottom
topNavBarMaxY,就是當(dāng)前Y坐標(biāo)加上本身的高度。
bottomTabBar突出的距離,即屏幕高度減去其Y坐標(biāo)大于0的部分。
這里要注意:ScrollViewscrollIndicatorInsets同時(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í)候NavBarTabBar會彈回來,有的時(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)看起來自然一些,這里除了需要改變ScrollViewcontentInset,還需要改變其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到其他頁面之前,必須把NavBarTabBar都展開,不然在收起的狀態(tài)Push到其他頁面,NavBarTabBar都不見了。
這里就需要在- (void)viewWillDisappear:(BOOL)animated中將NavBarTabBar都展開。

還有一個(gè)地方需要注意:
當(dāng)Push到其他頁面的時(shí)候,如果此時(shí)ScrollViewcontentInset不為(0,0,0,0)時(shí),系統(tǒng)會自動(dòng)將其置為(0,0,0,0),這樣在Push后,還會走到之前頁面ScrollViewscrollViewDidScroll:方法中,會導(dǎo)致NavBar消失。對于這種情況,就不應(yīng)該讓其繼續(xù)走我們處理展開和收取NavBarTabBar的流程。
我們可以通過以下代碼控制,當(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)目,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內(nèi)容