新版 Apple Music 应用中的同样动态的状态栏。

19

是否可以动态着色在新的Apple Music应用程序中的状态栏

输入图像描述

编辑:

iOS 8.4中的新版Apple Music应用程序具有此功能。

  • 打开应用程序。
  • 选择并播放歌曲(状态栏为白色)
  • 向下滑动播放器控制器以查看“我的音乐”控制器(它具有黑色状态栏,也许您需要返回导航层次结构)。
  • 现在只需上下滑动即可查看动态状态栏更改。

编辑2:

目前似乎无法使用Apple文档(iOS 8.4)。可能会在未来的iOS 9中提供。

编辑3: 目前在iOS 9中似乎还不可用。


考虑私有API - Jakub Truhlář
图片:顶部白色部分是第一个VC,带有白色状态栏和专辑封面的部分是另一个VC(在第一个VC之上)。 - Jakub Truhlář
感谢提供更新的信息。据我所知,这个问题没有简单的解决方案。您可以参考这个项目,它使用了类似于我在答案中建议的方法(但实现了可滚动状态栏行为)。作者对状态栏进行快照,并将其作为图像添加到虚假的状态栏窗口中。https://github.com/Antondomashnev/UIViewController-ScrollingStatusBar - Alexander Tkachenko
@AlexanderTkachenko 状态栏在两种样式下都是实时且正常工作的。这不可能是快照。 - Segev
快照可以在运行时进行,并且图像可能会被更新。我知道这不是一行代码的解决方案,但如果它是应用程序的重要功能,那么肯定有方法来实现它。 - Alexander Tkachenko
显示剩余2条评论
4个回答

4
我99.99%确定这不能使用公共API轻松完成,因为我已经尝试了几乎所有的方法(我个人也不认为这是他们状态栏的某种神奇方法,而是他们的应用程序能够检索状态栏视图,然后只需对其应用掩码)。
我确信你可以做自己的StatusBar,并且有MTStatusBarOverlay库可供使用,但很遗憾,这是一个非常古老的库,所以我无法确定它是否有效,但似乎仍有人在使用它。
但是,使用该库的方式,我认为可能会有一种解决方案,需要大量的工作,但是可行的,虽然不是“实时”的。简而言之,您需要这样做:
  • 截取屏幕顶部20个像素的截图(状态栏)
  • 从该截图中删除非黑色的所有内容(您可以改进它,使其搜索黑色边缘,这样您就可以保留绿色电池和透明度)。这将制作您的覆盖层掩模,并创建虚假的状态栏。
  • 用背景视图覆盖实际状态栏,使用刚刚创建的 alpha 图像
  • 对该图像应用遮罩,被遮罩的一切都会变成白色的阴影
  • 根据用户滚动更改掩模的高度
现在您应该能够正确滚动和更改颜色。唯一的问题是状态栏不活动,但它真的吗?一旦您滚动出去,立即删除覆盖层,让其刷新。当您滚动到最顶部时,您将执行相同的操作,但在这种情况下,您将状态栏的颜色更改为白色(无动画),以适应您的状态。它只会短暂地不活动。

希望它有所帮助!


谁给这个答案点了踩,请您能否提供一下您认为它不好的原因,这样我们才能进行迭代改进。谢谢! - Jiri Trecak
因为该答案提供了私有 API 的意见并且提供了一个示例(即使它让人感到痛苦),所以被奖励。真正的答案是我们可能都需要等待这个问题的解决。 - Jakub Truhlář
他们正在使用私有API来实现表格视图A-Z索引的动画效果,这显然不是公开可用的,并且在Apple Music中已经有了使用私有UI的先例 :( - Luke

3

在Jiri的回答基础上,这将让你接近成功。将MTStatusBarOverlay替换为CWStatusBarNotification。为了处理视图控制器之间的模态转换,我使用MusicPlayerTransition。我们假设在self.view中有一个imageView: "art",其frame为CGRect(0, 0, self.view.bounds.size.width, self.view.bounds.size.width)。需要稍作调整,但你明白要点。注意:虽然我们不是“实时”的,但最多只会有一秒钟的偏差,并且电池颜色不会被保留。此外,你需要将CWStatusBarNotification.m中的动画时间设置为零(notificationAnimationDuration属性)。

#import "CWStatusBarNotification.h"

#define kStatusTextOffset       5.4  // (rough guess of) space between window's origin.y and status bar label's origin.y

@interface M_Player () <UIGestureRecognizerDelegate> 

@property (retain) UIView *fakeStatusBarView;
@property (retain) CWStatusBarNotification *fakeStatusBar;
@property (retain) UIImageView *statusImgView;
@property (retain) UIImageView *statusImgViewCopy;
@property (retain) UIWindow *window;
@property (strong, nonatomic) NSTimer *statusTimer;

@end

@implementation M_Player
@synthesisze fakeStatusBarView, fakeStatusBar, statusImgView, statusImgViewCopy, window, statusTimer;

-(void)viewDidLoad{

    self.window = [[UIApplication sharedApplication] delegate].window;

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleStatusBarDrag:)];
    pan.delegate = self;
    [self.view addGestureRecognizer:pan];
}

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];

    if (!fakeStatusBar){
        [self buildFakeStatusBar];
    }

    if (!statusTimer) {
        [self setupStatusBarImageUpdateTimer];
    }

     // optional
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
    [self setNeedsStatusBarAppearanceUpdate]; 


-(void)viewDidDisappear:(BOOL)animated{
   [super viewDidDisappear:animated];

   [self destroyStatusBarImageUpdateTimer];

}

-(void)destroyFakeStatusBar{
    [statusImgView removeFromSuperview];
    statusImgView = nil;
    [fakeStatusBarView removeFromSuperview];
    fakeStatusBarView = nil;
    fakeStatusBar = nil;
}

-(void)buildFakeStatusBar{
    UIWindow *statusBarWindow =  [[UIApplication sharedApplication] valueForKey:@"_statusBarWindow"];  // This window is actually still fullscreen. So we need to capture just the top 20 points.

    UIGraphicsBeginImageContext(self.view.bounds.size);
    [statusBarWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, 20);
    CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
    UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

    statusImg = [statusImg imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];  // This allows us to set the status bar content's color via the imageView's .tintColor property

    statusImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    statusImgView.image = statusImg;
    statusImgView.tintColor = [UIColor colorWithWhite:0.859 alpha:1.000];  // any color you want

    statusImgViewCopy = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    statusImgViewCopy.image = statusImg;
    statusImgViewCopy.tintColor = statusImgView.tintColor;


    fakeStatusBarView = nil;
    fakeStatusBar = nil;
    fakeStatusBarView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    [fakeStatusBarView addSubview:statusImgView];

    fakeStatusBar = [CWStatusBarNotification new];
    fakeStatusBar.notificationStyle = CWNotificationStyleStatusBarNotification;
    [fakeStatusBar displayNotificationWithView:fakeStatusBarView forDuration:CGFLOAT_MAX];
}

-(void)handleStatusBarDrag:(UIPanGestureRecognizer*)gestureRecognizer{
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {

    }

    if (gestureRecognizer.state == UIGestureRecognizerStateChanged){
        CGPoint convertedPoint = [self.window convertPoint:art.frame.origin fromView:self.view];
        CGFloat originY = convertedPoint.y - kStatusTextOffset;

        if (originY > 0 && originY <= 10) {  // the range of change we're interested in
            //NSLog(@"originY:%f statusImgView.frame:%@", originY, NSStringFromCGRect(statusImgView.frame));

            // render in context from new originY using our untouched copy as reference view
            UIGraphicsBeginImageContext(self.view.bounds.size);
            [statusImgViewCopy.layer renderInContext:UIGraphicsGetCurrentContext()];
            UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            CGRect rect = CGRectMake(0, kStatusTextOffset + originY, self.view.bounds.size.width, 20);
            CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
            UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);

            statusImgView.image = statusImg;
            statusImgView.transform = CGAffineTransformMakeTranslation(0, kStatusTextOffset + originY);

        }

       // destroy
        if (originY > 90) {
            [self destroyFakeStatusBar];
        }
    }

    if (gestureRecognizer.state == UIGestureRecognizerStateEnded){

    }
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    return YES;
}

为了让您的状态栏截图与实际状态栏保持同步,请设置计时器。在viewWillAppear中启动它,在viewDidDisappear中停止它。

-(void)setupStatusBarImageUpdateTimer{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^(){
            // main thread
            if (!statusTimer) {
                statusTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleStatusTimer:) userInfo:nil repeats:YES];
                [[NSRunLoop currentRunLoop] addTimer:statusTimer forMode:NSRunLoopCommonModes];
            }
        });
    });
}

-(void)destroyStatusBarImageUpdateTimer{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^(){
            // main thread
            [statusTimer invalidate];
            statusTimer = nil;
        });
    });
}

-(void)handleStatusTimer:(NSTimer*)timer{
    UIWindow *statusBarWindow =  [[UIApplication sharedApplication] valueForKey:@"_statusBarWindow"];

    UIGraphicsBeginImageContext(CGSizeMake(self.view.bounds.size.width, 20));
    [statusBarWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, 20);
    CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
    UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

    statusImg = [statusImg imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    statusImgViewCopy.image = statusImg;
}

由于我们对计时器和设置有很强的引用,并且无效化发生在同一线程上,因此无需担心计时器未能失效。 最终结果应该看起来像这样:

enter image description here


0

乍一看,它看起来像是从状态栏的快照中进行操作,但状态栏在两端都是活动的,所以不是这种情况。

第二眼看起来像是在iOS 8.4中引入的一些新API,但在审查API后,我找不到任何相关的内容。

苹果在自己的应用程序中使用私有API对我来说似乎非常奇怪。这将为开发人员带来一些非常糟糕的例子,但另一方面,没有公共的东西可以让您在实时状态栏上拥有两种样式。

这使我们只能使用私有API或黑魔法。


如果苹果使用私有API,为什么会感到奇怪? - Legoless
我相信Apple在幕后使用了很多私有技术,但对于开发者明显无法使用的视觉元素...这让我感到有些不适。 - Segev

-1

思考如何在没有私有API的情况下实现这个功能。

我认为可以通过第二个UIWindow覆盖您的状态栏来解决问题。

在iPhone上添加视图到状态栏

也许可以不断地对状态栏进行截屏(从主窗口中获取),将其应用于某些过滤器并在第二个窗口上显示这个“虚假状态栏图像”(在“真实”状态栏之上)。

然后,您可以对第二个“虚假”状态栏进行任何操作。


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