美文网首页iOS技术专题iOS、swift技术交流!iOS Review
好奇心日报iPad版的分屏(split view & s

好奇心日报iPad版的分屏(split view & s

作者: 野火wildfire | 来源:发表于2015-12-06 22:43 被阅读1848次

    好奇心日报iPad版的分屏适配实现

    好奇心日报是一个媒体阅读类app,因此不可能放弃iPad这个平台,恰逢iPad pro上线,我们决定开发iPad版本并直接适配iOS9的新特性----分屏。

    文章中提到的适配方案,思路基础源于微信iOS客户端的ui处理,特此感谢微信,感谢曾经在广研工作的日子。

    写在前面

    Apple在iOS9中针对iPad做了一些多任务处理模式,其中分屏模式为“Slide Over”和“Split View”。

    • Slide Over支持全部iPad版本,支持其功能的app会在其它app在前台的情况下,通过侧滑右边,获得一个宽度为320(iPad Pro为375)的空间用于展示。这种为伪多任务,最终其实也只能处理一个app。样式如下:
    split1.jpg
    • Split View才是我们所理解的真正应用程序分屏功能,可以将屏幕分成大约3:1,2:2,2:1等多种情况(我们遍历宽度,发现一共12种...)。支持的机型现有3中,就是iPad pro,iPad air2,iPad mini4。样式如下


      split2.jpg

    通过调研,我们发现通过iPad分屏,以及转屏切换等操作,我们开发者将面对的app屏幕宽度足足有11种之多:

    iPad pro : 1366,1024,981,678,639,375
    iPad air : 1024,768,694,507,438,320
    

    其中宽度相差很大,可以和混乱的Android市场一拼,写死frame的方式肯定不行,单纯的采用autolayout布局的方式也不可能满足如此跨度宽度的需求。市面上和我们比较类似的应用,同时支持了split view的只发现了豌豆荚一览,参考他们的适配风格,发现他们是1行,在不同情况下只需要更改间距即可。下图是豌豆荚一览的图片,可以看出他们针对split view的适配是比较舒服而且比较简单的


    split6.png

    好奇心日报的iPhone版本是一种2列卡片形式的瀑布流,相应的,iPad版本预计也设计成卡片形式,这样我们面对的适配情况要比两列复杂的多,那么确定适配方案将会是最重要的部分。下图是好奇心日报iPhone版本和iPad版本视觉初稿,我们预计要搞成这个样子:


    split5.png

    适配方案

    即使采用autolayout,split view切分过程中依然要修改其约束值,几套不同参数的适配值不可避免。
    11种屏幕方案,UI同学不可能出11套视觉稿,所以我们根据宽度和机型不同,将这11种情况划分3个区间:

        4列情况:1366,1024,981,768(非mini)
        3列情况:768(mini),694,478,639
        2列情况:407,438,375,320
    

    UI同学只需要根据出上述3中情况最大尺寸的视觉稿,我们开发根据最大视觉稿做向下兼容适配。在同一个配置文件下,根据宽度的不同,可以让其中的一些间距、字体不变或者等比压缩。

    在这种混乱屏幕的适配方面,web页面以及Android的app在这方面远远走在了iOS的前面。在吸取两者优点的情况下,我们最终采用方案是基于css的配置模式,每个css中可以定义当前模式下的间距,颜色,以及字体等与UI相关的参数。具体:

    定义了3中css,style~iPad4Column.css、style~iPad3Column、style~iPad2Column.css,并约定style~iPad4Column.css为其基础配置。即通过一个theme管理层,使其在2列的情况下先读取2Column.css的数值,如果该值不存在,读取基础类4Column.css的值,3列情况于此相同。
    而且,在css参数内部,定义了一个dynamic属性,使其能够根据宽度自由放缩。例如在4Column.css中定义了一个”1366 dynamic“的值,由于1366、1024、981都会读取该配置,但是却在这三种情况下分别获得的值为1366、1024和981,实现了等比放缩。
    

    总结,方案基本就是代码层采用autolayout(iOS方案),theme层用3套配置文件(Android方案---xml)+等比放缩(web方案,css百分比),通过这种方案,基本能够满足我们对多屏幕适配的要求。

    下面贴一点css部分的代码:


    split3.png
    通过这种配置文件,以后老板如果说,2列下面这个字体有点大吧,我们就不需要到无限的m文件里面去痛苦的查找,直接找到这里改掉就好了,而且编包不需要重新编译,超快~
    

    针对css文件的解析,我是在facebook开源出来的react Native中找到了其中的css解析文件,做了一些适应性修改,拿来用的,文件名如下图,自己去找:


    split4.png

    再贴一部分theme层的代码,这一部分通过css的配置文件将相同的宏变量在不同的宽度下映射出不同的值:

    ThemeMgr.m
        //loadcss部分,将3套资源load成3个dictionary,最终的值是一个NSArray对象
        - (void)loadCSSStyle{
            NSString *bundleRoot = [[NSBundle mainBundle] bundlePath];
            NSString *path = [bundleRoot stringByAppendingPathComponent:THEME_STYLE_DEFINE_FILE];
            
            NSDictionary* parentDic = nil;
            
            NSString* column2Path = [[[path stringByDeletingPathExtension] stringByAppendingString:THEME_RES_PATH_2COLUMN_SUBFIX] stringByAppendingPathExtension:@"css"];;
            parentDic = [[[NICSSParser alloc] init] dictionaryForPath:column2Path];
            [self rebuildThemeDictionaryWithThemeDictionary:parentDic toDictionary:_2ColumnThemeDic];
            ...重复代码,省略...
        }
    
        //根据css的key获取其值,我们定义了一个方法[DeviceInfo iPadColumn]来确认当前需要读取的配置文件。
        - (NSArray*)getValueOfProperty:(NSString*)property forSeletor:(NSString *)selector
        {
            NSArray* result = nil;
            int column = [DeviceInfo iPadColumn];
            if (column == 2) {
                NSDictionary* child2ColumnThemeDic = [_2ColumnThemeDic objectForKey:selector];
                if (child2ColumnThemeDic) {
                    result = [child2ColumnThemeDic objectForKey:property];
                }
                result = [self constantsValue:result];
            }
            if (result && result.count > 0) {
                return result;
            }
            if (column == 3) {
                NSDictionary* child3ColumnThemeDic = [_3ColumnThemeDic objectForKey:selector];
                if (child3ColumnThemeDic) {
                    result = [child3ColumnThemeDic objectForKey:property];
                }
                result = [self constantsValue:result];
            }
            if (result && result.count > 0) {
                return result;
            }
            //2和3没找到,默认走4
            NSDictionary* child4ColumnThemeDic = [_4ColumnThemeDic objectForKey:selector];
            if (child4ColumnThemeDic) {
                result = [child4ColumnThemeDic objectForKey:property];
            }
            result = [self constantsValue:result];
            return result;
        }
        
        
    ThemeUtil.m
        //将传入数组转成对应UIFont
        +(UIFont*) parseFontFromValues:(NSArray*)value
        {
            if (value==nil||[value count]==0) {
                return [UIFont systemFontOfSize:[UIFont systemFontSize]];
            }
            
            NSInteger fontSize = [[value firstObject] integerValue];
            if (fontSize<=5) {
                fontSize = 5;
            }
            if ([value count]==1) {
                return [UIFont systemFontOfSize:fontSize];
            }
            
            if ([[value lastObject] isEqualToString:@"dynamic"]) {
                fontSize = (int) (fontSize * [DeviceInfo iPadThemeScale]);
            }
            
            if ([value count]==2) {
                if ([[value objectAtIndex:1] isEqualToString:@"bold"]) {
                    return [UIFont boldSystemFontOfSize:fontSize];
                }else if([[value objectAtIndex:1] isEqualToString:@"italic"]){
                    return [UIFont italicSystemFontOfSize:fontSize];
                }else{
                    return [UIFont systemFontOfSize:fontSize];
                }
            }
            return [UIFont systemFontOfSize:[UIFont systemFontSize]];
        }
        //传入数组转成对应的CGFloat
        +(CGFloat) parseFloatFromValues:(NSArray*)value
        {
            CGFloat floatValue = [[value firstObject] floatValue];
            if ([[value lastObject] isEqualToString:@"dynamic"]) {
                floatValue *= [DeviceInfo iPadThemeScale];
            }
           return
    

    适配过程中定义的一些工具

    如何识别iPad pro,iPad mini,以及普通iPad

    有时候要根据设备不同进行区分对待,尤其是mini和air具有相同的分辨率但尺寸不同,就有其需要有一些特殊化操作。贴代码:

    + (BOOL) isiPadPro {
            static BOOL s_isiPadPro = NO;
            
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                if ([self isiPad]) {
                    s_isiPadPro = MAX(QDScreenHeight, QDScreenWidth) > 1024;
                }
            });
            
            return s_isiPadPro;
        }
    
        + (BOOL) isiPadMini {
            static BOOL s_isiPadMini = NO;
            
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                if ([self isiPad]) {
                    NSString *nsPlatform = [UIDevice currentDevice].platform;
                    s_isiPadMini = ([nsPlatform hasPrefix:@"iPad2,5"] || [nsPlatform hasPrefix:@"iPad2,6"] || [nsPlatform hasPrefix:@"iPad2,7"]    //mini
                                    || [nsPlatform hasPrefix:@"iPad4,4"] || [nsPlatform hasPrefix:@"iPad4,5"] || [nsPlatform hasPrefix:@"iPad4,6"] //mini2
                                    || [nsPlatform hasPrefix:@"iPad4,7"] || [nsPlatform hasPrefix:@"iPad4,8"] || [nsPlatform hasPrefix:@"iPad4,9"] //mini3
                                    || [nsPlatform hasPrefix:@"iPad5,1"] || [nsPlatform hasPrefix:@"iPad5,2"] //mini4
                                    );
                }
            });
            
            return s_isiPadMini;
        }
    
        + (BOOL) isiPadNormal { //默认认为不是mini和pro的都是normal
            static BOOL s_isiPadNormal = NO;
            
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                if ([self isiPad]) {
                    s_isiPadNormal = ![self isiPadMini] && ![self isiPadPro];
                }
            });
            
            return s_isiPadNormal;
        }
    

    获得分屏的通知

    系统没有针对分屏发送通知,我们需要自己实现。通过实验,虽然在分屏过程中系统会回掉appdelegate的applicationDidBecomeActive:方法,但我们发现最先获得分屏事件的是可见window的layoutSubviews方法,因此在此做一些操作。贴代码:

        - (void) layoutSubviews {
            [super layoutSubviews];
            if (self == [AppDelegate sharedAppDelegate].window && _lastWidth != self.width) {
                 _lastWidth = self.width;
                [DeviceInfo changeSplitValueIfNeed];
            }
        }
    
        DeviceInfo.m
        + (void) changeSplitValueIfNeed {
            if ([DeviceInfo isiPadUniversal] && [DeviceInfo isiOS9plus]) {
                UIScreen *screen = [UIScreen mainScreen];
                CGFloat appWidth = screen.applicationFrame.size.width;
                BOOL curSplitState = (appWidth != screen.bounds.size.width && appWidth != screen.bounds.size.height);
                if (curSplitState != s_biPadSplitState) { //本次后台前台切换了状态
                    s_biPadSplitState = curSplitState;
                }
            } else {
                s_biPadSplitState = NO;
            }
            int column = [DeviceInfo getCurrentiPadColumnIfChange];
            if (column != s_biPadColumnNum) {
                s_biPadColumnNum = column;
                [[NSNotificationCenter defaultCenter] postNotificationName:KNotificationColumnChange object:nil];
            }
        }
    

    获取当前宽度

    由于分屏了,使用[UIScreen mainScreen].bounds可能就不能满足了。贴代码:

        + (CGFloat) appWidth {
            UIScreen *screen = [UIScreen mainScreen];
            
            if ([DeviceInfo isiPadUniversal] && [DeviceInfo isiOS9plus]) {
                return screen.applicationFrame.size.width;;
            }
            return screen.bounds.size.width;
        }
    

    什么时候处理UI布局更新事件

    viewcontroller的viewWillLayoutSubviews,view的layoutSubviews时处理UI的最好时刻,如果使用了collectionview,那么如果在分屏过程中重新定义了layout,那么在cell的applyLayoutAttributes:方法中处理ui也是个不错的时机。

    一些在适配过程中的注意

    1、window的设定
    在iOS9下window不要设置它的宽高,它默认init就好了。
    但是,iOS8及以下版本一定要初始化宽高,要不然发生任何恐怖事情不负责。
    2、多使用autolayout,如果UI很简单,不需要AutoLayout,起码要记得添加autoResizingMask属性。
    3、面对的宽度会相差近5倍,所以所有的宽度值都不会是安全的了,每个写死的数值都要考虑如果分屏了怎么办。
    4、cache、cache、cache!分屏可能会出现多种ui情况,很多宽度高度要不停的计算,如果算过了,请把值缓存下来。
    5、使用splitview必须要支持4个方向的旋转,so,你可能要改好多交互方案了

    最后

    gif欣赏

    以后加上,或者过两天你去appstore下载个看看

    最后的最后

    如果你嫌麻烦,可以通过在plist中增加参数来关闭对split view的支持。。。
    <key>UIRequiresFullScreen</key>
    <true/>

    如果你喜欢我的文章,可以直接查看我的博客,上面会有更多内容

    相关文章

      网友评论

        本文标题:好奇心日报iPad版的分屏(split view & s

        本文链接:https://www.haomeiwen.com/subject/hicehttx.html