模仿 Facebook 隐藏/显示扩展/收缩导航栏

129
在新的iOS7 Facebook iPhone应用程序中,当用户向上滚动NavigationBar时,NavigationBar逐渐隐藏到完全消失的程度。然后,当用户向下滚动NavigationBar时,NavigationBar逐渐显示出来。
您如何自己实现此行为?我知道以下解决方案,但它会立即消失,并且与用户滚动手势的速度没有关系。
[navigationController setNavigationBarHidden: YES animated:YES];

希望这不是重复的内容,因为我不确定如何最好地描述“扩展/收缩”的行为。


2
同样的问题:https://dev59.com/C2Eh5IYBdhLWcg3w3muK。请注意,要**完全**匹配Safari的行为是非常困难的。里面有一些非常非常复杂的规则! - Fattie
1
在我的项目中,我使用了这个项目,它运行得非常好。请查看它的文档。 - Vinicius
https://github.com/bryankeller/BLKFlexibleHeightBar/ 可以让你实现更多想要的功能。它允许你在从最大化到最小化的每个阶段指定状态栏的外观,甚至可以指定自己的行为,使其像Safari、Facebook或其他应用程序一样运行。 - blkhp19
我没有使用uinavigationbar,而是添加了一个uiview。这个视图会根据滚动而扩展和收缩,以复制导航栏的效果。我使用了scrollViewDidScroll委托方法来实现这个任务。你可能想要检查并执行下面的源代码.. https://www.dropbox.com/s/b2c0zw6yvchaia5/FailedBanks.zip?dl=0 - Deepak Thakur
20个回答

162
@peerless 提供的解决方案是一个很好的起点,但它只在拖动开始时启动动画,而不考虑滚动速度。这会导致比 Facebook 应用程序中更加断断续续的体验。为了匹配 Facebook 的行为,我们需要:
  • 以与拖动速率成比例的速度隐藏/显示导航栏
  • 在滚动停止时,如果导航栏部分隐藏,则启动动画完全隐藏栏
  • 当栏缩小时淡化导航栏的内容。

首先,您需要以下属性:

@property (nonatomic) CGFloat previousScrollViewYOffset;

这里是 UIScrollViewDelegate 方法:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGRect frame = self.navigationController.navigationBar.frame;
    CGFloat size = frame.size.height - 21;
    CGFloat framePercentageHidden = ((20 - frame.origin.y) / (frame.size.height - 1));
    CGFloat scrollOffset = scrollView.contentOffset.y;
    CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
    CGFloat scrollHeight = scrollView.frame.size.height;
    CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;

    if (scrollOffset <= -scrollView.contentInset.top) {
        frame.origin.y = 20;
    } else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
        frame.origin.y = -size;
    } else {
        frame.origin.y = MIN(20, MAX(-size, frame.origin.y - scrollDiff));
    }

    [self.navigationController.navigationBar setFrame:frame];
    [self updateBarButtonItems:(1 - framePercentageHidden)];
    self.previousScrollViewYOffset = scrollOffset;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                  willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

你还需要以下这些辅助方法:

- (void)stoppedScrolling
{
    CGRect frame = self.navigationController.navigationBar.frame;
    if (frame.origin.y < 20) {
        [self animateNavBarTo:-(frame.size.height - 21)];
    }
}

- (void)updateBarButtonItems:(CGFloat)alpha
{
    [self.navigationItem.leftBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
        item.customView.alpha = alpha;
    }];
    [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
        item.customView.alpha = alpha;
    }];
    self.navigationItem.titleView.alpha = alpha;
    self.navigationController.navigationBar.tintColor = [self.navigationController.navigationBar.tintColor colorWithAlphaComponent:alpha];
}

- (void)animateNavBarTo:(CGFloat)y
{
    [UIView animateWithDuration:0.2 animations:^{
        CGRect frame = self.navigationController.navigationBar.frame;
        CGFloat alpha = (frame.origin.y >= y ? 0 : 1);
        frame.origin.y = y;
        [self.navigationController.navigationBar setFrame:frame];
        [self updateBarButtonItems:alpha];
    }];
}

如果需要稍微不同的行为,可以用以下代码替换在滚动时重新定位进度条的那一行(即 scrollViewDidScroll 函数中的 else 块):

frame.origin.y = MIN(20, 
                     MAX(-size, frame.origin.y - 
                               (frame.size.height * (scrollDiff / scrollHeight))));

这将根据最后的滚动百分比而不是绝对值来定位该栏,从而导致淡出速度变慢。原始行为更类似于Facebook,但我也喜欢这种方式。
注意:此解决方案仅适用于iOS 7+。如果您支持旧版本的iOS,请确保添加必要的检查。

1
你说得对。这个周末我可能会研究一下更通用的解决方案。将默认项作为目标而不是 customView 应该不太困难。 - Wayne
1
我修复了库存栏按钮的问题,但是这段代码还有另一个问题。如果ScrollViewcontentSize比frame小,则滑动动画将不起作用。同时,请确保在viewDidDisappear中将所有导航项的alpha重置为1.0。 - Legoless
没错。此外,你应该添加一些标志来防止这些动画在 scrollView 没有显示时随时发生。 - Wayne
1
你在使用AutoLayout的控制器上检查过这个解决方案吗?在我的情况下,没有使用AutoLayout时一切正常,但是当它打开时,仍然会出现一些奇怪的白色条纹。 - Aliaksandr B.
我们如何模仿导航栏下方的收缩工具栏? - thisiscrazy4
显示剩余26条评论

52
EDIT: 仅适用于 iOS 8 及以上版本。
您可以尝试使用
self.navigationController.hidesBarsOnSwipe = YES;

对我来说可行。

如果你在编写 Swift 代码,你必须使用这种方法(参考自https://dev59.com/QWIj5IYBdhLWcg3w24kF#27662702

navigationController?.hidesBarsOnSwipe = true

此功能不支持 iOS < 8.0。 - Petar
1
正如pet60t0所说,我很抱歉,此功能仅适用于iOS 8及以上版本。 - Pedro Romão
4
在此之后,你如何使其恢复? - C0D3
行为有点奇怪。我没有深入探索,但如果你滚动得更快,它会再次显示出来。 - Pedro Romão
@PedroRomão 很棒的回答。 - Gagan_iOS

43

这里还有一个实现:TLYShyNavBar v1.0.0 发布!

在尝试了已有的解决方案之后,我决定自己动手写一个。对我来说,它们要么表现不佳,要么有很高的入门门槛和样板代码,要么缺少导航栏下方的扩展视图。要使用此组件,您只需执行以下操作:

self.shyNavBarManager.scrollView = self.scrollView;

噢,而且它在我们自己的应用程序中经过了战斗考验。


4
在我写完这条评论后几秒钟,我发现了解决方案。我必须确保我的UICollectionViewControllerextendedLayoutIncludesOpaqueBars属性被设置为YES - Tim Arnold
哎呀,没错,这是个好主意,我道歉。我会在 Github 上发布一个问题。 - Tim Arnold
如果导航栏有子视图(例如左/右按钮、UISearchBar等项目),那么收缩会在导航栏原来的位置留下黑色背景。 - Katedral Pillon
@Mazyod 你知道我怎么把这个应用到底部工具栏吗? - Brian Nezhad
@NSGod 我假设你可以使用这个组件中的概念,但它默认不支持底部工具栏。尝试一下修改它,应该很简单(我认为)。 - Mazyod
显示剩余8条评论

33
你可以查看我的GTScrollNavigationBar,我已经通过继承UINavigationBar使其基于UIScrollView的滚动而滚动。
注意:如果您有一个不透明的导航栏,滚动视图必须在导航栏被隐藏时扩展。这正是GTScrollNavigationBar所做的事情。(就像iOS上的Safari一样。)

顺便提一句,对于任何阅读此内容的人,如何准确调用initWithNavigationBarClass...请参考https://dev59.com/xX3aa4cB1Zd3GeqPcmaC - Fattie
@Thuy,干得好!我已经在表视图控制器上完美地实现了这个,除了一个问题...我在顶部实现了下拉刷新。当尝试向下拉并刷新时,它变得奇怪。可能有解决方法吗? - aherrick
@Thuy 也就是说,假设我有一个视图控制器,在底部实现了一个表视图。我想连接到那个表视图,这很好用,但是我还有另一个视图位于表视图上方,我也希望它消失。这该怎么办? - aherrick

25

iOS8包含获取导航栏隐藏的属性。有一个WWDC视频演示了它,搜索"iOS 8中的视图控制器改进"。

class QuotesTableViewController: UITableViewController {

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    navigationController?.hidesBarsOnSwipe = true
}

}

其他属性:

class UINavigationController : UIViewController {

    //... truncated

    /// When the keyboard appears, the navigation controller's navigationBar toolbar will be hidden. The bars will remain hidden when the keyboard dismisses, but a tap in the content area will show them.
    @availability(iOS, introduced=8.0)
    var hidesBarsWhenKeyboardAppears: Bool
    /// When the user swipes, the navigation controller's navigationBar & toolbar will be hidden (on a swipe up) or shown (on a swipe down). The toolbar only participates if it has items.
    @availability(iOS, introduced=8.0)
    var hidesBarsOnSwipe: Bool
    /// The gesture recognizer that triggers if the bars will hide or show due to a swipe. Do not change the delegate or attempt to replace this gesture by overriding this method.
    @availability(iOS, introduced=8.0)
    var barHideOnSwipeGestureRecognizer: UIPanGestureRecognizer { get }
    /// When the UINavigationController's vertical size class is compact, hide the UINavigationBar and UIToolbar. Unhandled taps in the regions that would normally be occupied by these bars will reveal the bars.
    @availability(iOS, introduced=8.0)
    var hidesBarsWhenVerticallyCompact: Bool
    /// When the user taps, the navigation controller's navigationBar & toolbar will be hidden or shown, depending on the hidden state of the navigationBar. The toolbar will only be shown if it has items to display.
    @availability(iOS, introduced=8.0)
    var hidesBarsOnTap: Bool
    /// The gesture recognizer used to recognize if the bars will hide or show due to a tap in content. Do not change the delegate or attempt to replace this gesture by overriding this method.
    @availability(iOS, introduced=8.0)
    unowned(unsafe) var barHideOnTapGestureRecognizer: UITapGestureRecognizer { get }
}

通过http://natashatherobot.com/navigation-bar-interactions-ios8/发现。


12

这适用于iOS 8及以上版本,并确保状态栏仍保留其背景

self.navigationController.hidesBarsOnSwipe = YES;
CGRect statuBarFrame = [UIApplication sharedApplication].statusBarFrame;
UIView *statusbarBg = [[UIView alloc] initWithFrame:statuBarFrame];
statusbarBg.backgroundColor = [UIColor blackColor];
[self.navigationController.view addSubview:statusbarBg];

如果您希望在点击状态栏时显示导航栏,可以执行以下操作:

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
     self.navigationController.navigationBarHidden = NO;
}

12

我有一种简单的解决方法,没有进行深入测试,但是以下是我的想法:

该属性将保留导航栏中的所有项目,适用于我的UITableViewController类。

@property (strong, nonatomic) NSArray *navBarItems;

在同一个UITableViewController类中,我有:

-(void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
    if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
        return;
    }

    CGRect frame = self.navigationController.navigationBar.frame;
    frame.origin.y = 20;

    if(self.navBarItems.count > 0){
        [self.navigationController.navigationBar setItems:self.navBarItems];
    }

    [self.navigationController.navigationBar setFrame:frame];
}

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
        return;
    }

    CGRect frame = self.navigationController.navigationBar.frame;
    CGFloat size = frame.size.height - 21;

    if([scrollView.panGestureRecognizer translationInView:self.view].y < 0)
    {
        frame.origin.y = -size;

        if(self.navigationController.navigationBar.items.count > 0){
            self.navBarItems = [self.navigationController.navigationBar.items copy];
            [self.navigationController.navigationBar setItems:nil];
        }
    }
    else if([scrollView.panGestureRecognizer translationInView:self.view].y > 0)
    {
        frame.origin.y = 20;

        if(self.navBarItems.count > 0){
            [self.navigationController.navigationBar setItems:self.navBarItems];
        }
    }

    [UIView beginAnimations:@"toggleNavBar" context:nil];
    [UIView setAnimationDuration:0.2];
    [self.navigationController.navigationBar setFrame:frame];
    [UIView commitAnimations];
}

这只适用于ios >= 7,我知道它很丑,但这是一种快速实现的方法。欢迎提出任何评论/建议 :)


10
这是我的实现:SherginScrollableNavigationBar
在我的方法中,我使用 KVO 观察 UIScrollView 的状态,因此不需要使用代理(并且您可以将此代理用于其他任何所需的内容)。

FYI,这并不总是正确的。我也尝试过,只要你不在“弹跳”scrollview时它就可以工作。当处于弹跳部分时,似乎KVO不会被触发。但是contentOffset的委托调用确实会被触发。 - Joris Mans

7
请尝试使用我的解决方案,并告诉我为什么这不如之前的答案好。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    if (fabs(velocity.y) > 1)
        [self hideTopBar:(velocity.y > 0)];
}

- (void)hideTopBar:(BOOL)hide
{
    [self.navigationController setNavigationBarHidden:hide animated:YES];
    [[UIApplication sharedApplication] setStatusBarHidden:hide withAnimation:UIStatusBarAnimationSlide];
}

1
我做过类似的事情,这是最好的解决方案。我尝试了几个hack和库,这是唯一一个适用于iOS 9且tableView不覆盖整个屏幕的解决方案。赞! - skensell

6
我通过以下方式之一实现了这一点。
例如,将您的视图控制器注册为您的UITableViewUIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

UIScrollViewDelegate方法中,您可以获取新的contentOffset并相应地将UINavigationBar向上或向下平移。

根据一些阈值和因子,您可以设置和计算子视图的alpha值。

希望这能有所帮助!


在这里找到了类似的帖子 [链接] (https://dev59.com/TmIk5IYBdhLWcg3wWc7T) - Diana Sule
谢谢Diana!我怀疑我可能需要实现UIScrollViewDelegate方法,但似乎有点过度了。一旦我解决了这个问题,我会把它发布出来。干杯! - El Mocoso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接