iPad分屏框架-SPStackedNav源码解读

2017-04-21 11:36:41来源:http://www.jianshu.com/p/03fc728deb4d作者:要上班的斌哥人点击

SPStackedNav 是全球最大的流音乐服务商 Spotify 开源的一个 iPad 分屏框架,用于 Spotify 的 iPad 版 App 中,网易云音乐 iPad 版 App 也是采用相似的分屏交互方案,该框架的交互表现如下图所示:

656644-9056159e3925ec0e.png

SPStackedNav实现的交互方式

使用

根据 GitHub 上面的说明完成项目导入之后,那么就可以开始搭建UI框架了。

  1. 创建 SPSideTabController, SPSideTabController 的用法和UITabController的用法没有什么大的区别。

  2. 分别创建 SPSideTabController 的 RootViewController,设置 UITabBarItem 属性。

  3. 给 SPSideTabController 的 viewControllers 属性赋值对应的 RootViewController 数组。

Demo 的 AppDelegate 代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];    // Override point for customization after application launch.    self.window.backgroundColor = [UIColor whiteColor];    // 步骤 1 创建 SPSideTabController    self.tabs = [[SPSideTabController alloc] init];    // 步骤 2 分别创建 SPSideTabController 的 RootViewController,设置 UITabBarItem 属性    RootTestViewController *root1 = [RootTestViewController new];    root1.title = @"Root 1";    root1.tabBarItem.image = [UIImage imageNamed:@"114-balloon"];    RootTestViewController *root2 = [RootTestViewController new];    root2.title = @"Root 2";    root2.tabBarItem.image = [UIImage imageNamed:@"185-printer"];    root2.tabBarItem.badgeValue = @"5";    root2.tabBarItem.badgeColor = [UIColor redColor];    RootTestViewController *root3 = [RootTestViewController new];    root3.title = @"Root 3";    root3.tabBarItem.image = [UIImage imageNamed:@"114-balloon"];    // 步骤 3 给 SPSideTabController 的 viewControllers 属性赋值对应的 RootViewController 数组    self.tabs.viewControllers = @[        [[SPStackedNavigationController alloc] initWithRootViewController:root1],        [[SPStackedNavigationController alloc] initWithRootViewController:root2],        [[SPStackedNavigationController alloc] initWithRootViewController:root3]    ];    self.window.rootViewController = self.tabs;    [self.window makeKeyAndVisible];            return YES;}

5.效果图

656644-868252e5afa69fbd.png

效果图1

656644-c413a01681ef5ed1.png

效果图2

设计

656644-3ca6c07d584f5a61.png

View的层次结构

从图中的 View 层次结构图可以看到,左边的侧边栏 View 是一个 SPSideTabBar,该 SPSideTabBar 包含若干个 SPSideTabItemButton 。右边的容器 View 是一个 SPStackedNavigationScrollView ,该 SPStackedNavigationScrollView 里面包含了若干个 SPStackedPageContainer , 一个 SPStackedPageContainer 可以简单的看做一个ViewController。

当我们在 Demo 项目中的 RootTestViewController 里面 push 一个 ViewController 的时候。其实就相当于往 SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 view。SPStackedPageContainer的显示内容来自于 ViewController 的 view 属性。

ChildTestViewController *vc = [ChildTestViewController new];[self.stackedNavigationController pushViewController:vc animated:YES];

SPSideTabBar 和 SPSideTabItemButton 解析

RootTestViewController *root2 = [RootTestViewController new];    root2.title = @"Root 2";    root2.tabBarItem.image = [UIImage imageNamed:@"185-printer"];    root2.tabBarItem.badgeValue = @"5";    root2.tabBarItem.badgeColor = [UIColor redColor];

Demo 代码里面的 AppDelegate 设置的明明是 UITabBarItem 的各类属性, 但是为什么在 SPSideTabBar 里面没有看到关于 UITabBarItem 的信息呢?

656644-964de97691878bea.png

SPSideTabBar的层级结构

再来看看 SPSideTabBar 这个 View 的层级结构图,可以猜出 SPSideTabBar 将 UITabBarItem 的属性设置映射成 SPSideTabItemButton 的属性设置了。

656644-bd9202c1f8ef4ef1.png

SPSideTabController 的 viewDidLoad 方法

查看 SPSideTabController.m 文件的 viewDidLoad 方法,我们可以看到 _tabBar.items = validItems 这个属性设置方法将 SPSideTabController 的 tabBarItem 的对象数组传给SPSideTabBar 的 items属性。

来到 SPSideTabBar.m 实现文件查看 - (void)setItems:(NSArray*)items 方法

//将 UITabBarItem 数组转成 SPSideTabItemButton 数组- (void)setItems:(NSArray*)items{    if ([items isEqual:_items]) return;    self.selectedItem = nil;    _items = [items copy];    for(UIView *b in _itemButtons) [b removeFromSuperview];    self.itemButtons = nil;    if (_items) {        NSMutableArray *itemButtons = [NSMutableArray array];        CGRect pen = CGRectMake(0, 10, 80, 70);        for(UITabBarItem *item in _items) {            //关键步骤 将 UITabBarItem 转成 SPSideTabItemButton            UIView *b = [self buttonForItem:item withFrame:pen];            [itemButtons addObject:b];            [self addSubview:b];            pen.origin.y += pen.size.height + 10;        }        self.itemButtons = itemButtons;    }}

继续跟踪查看方法

UIView *b = [self buttonForItem:item withFrame:pen];
// 设置 SPTabBarItem 的 frame,并返回 SPTabBarItem 的 View- (UIView*)buttonForItem:(UITabBarItem*)item withFrame:(CGRect)pen{    if ([item isKindOfClass:[SPTabBarItem class]] && [(SPTabBarItem*)item view]) {        UIView *view = [(SPTabBarItem*)item view];        [view setFrame:pen];        return view;    }    SPSideTabItemButton *b = [[SPSideTabItemButton alloc] initWithFrame:pen];     // 省略 UITabBarItem 的属性转成 SPSideTabItemButton 的属性过程,     // 具体细节可以详看源码    return b;}

使用 SPSideTabBar 自定义 View 来替代系统的 UITabBar, 使用 SPTabBarItem 自定义 View 来替代系统的 UITabBarItem,SPSideTabBar 将 UITabBarItem 的属性设置映射到 SPTabBarItem。这个就是常见的自定义 TabBar 的思路。

SPStackedNavigationController 解析

SPStackedNavigationController 继承与 UIViewController,并定义和实现了一系列和 NavigationController 相关的方法,简而言之就是自己实现一个 NavigationController,这里做重讲解2个主要的方法.

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate- (UIViewController *)popViewControllerAnimated:(BOOL)animated;

656644-ebfd430348c1d8d2.png

SPStackedNavigationController 的示意

当 SPStackedNavigationController 做 push 操作的时候,就是往 SPStackedNavigationScrollView 这个仿 ScrollView 的 View 添加一个 SPStackedPageContainer 子View。从上图中的左边的 View 层次结构中可以看到SPStackedNavigationScrollView 里面有2个 SPStackedPageContainer 子 View。而上图中右边的 View 表现正好印证了这个结构。

查看 SPStackedNavigationController.m 文件的 - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate 实现方法

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated activate:(BOOL)activate{    // 省略代码    // 添加 viewController 到 viewControllers 的数组    [self willChangeValueForKey:@"viewControllers"];    [self addChildViewController:viewController];    //将 viewController 添加到 self,    if ([self isViewLoaded])    // 关键步骤 SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 View        [self pushPageContainerWithViewController:viewController];    if (activate)        [self setActiveViewController:viewController position:activePosition animated:animated];    // 调用 viewController 生命周期方法    [viewController didMoveToParentViewController:self];    [self didChangeValueForKey:@"viewControllers"];}

接下来看看 SPStackedNavigationController.m 文件 - (void)pushPageContainerWithViewController:(UIViewController*)viewController 的方法

- (void)pushPageContainerWithViewController:(UIViewController*)viewController{    CGSize size = self.view.frame.size;    CGRect frame = CGRectMake(self.view.bounds.size.width, 0, 0, size.height);    frame.size.width = (viewController.stackedNavigationPageSize == kStackedPageHalfSize ?                        kSPStackedNavigationHalfPageWidth :                        size.width);    SPStackedPageContainer *pageC = [[SPStackedPageContainer alloc] initWithFrame:frame VC:viewController];    //SPStackedNavigationScrollView 添加一个 SPStackedPageContainer 子 View    [_scroll addSubview:pageC];}

从代码中可以验证我们上文所述,当 SPStackedNavigationController 做 push 操作的时候,就是往 SPStackedNavigationScrollView 这个 View 添加一个 SPStackedPageContainer 子 View。

我们现在是否可以这样猜测,当 SPStackedNavigationController 做 pop 操作的时候,就是在 SPStackedNavigationScrollView 这个View 移除一个 SPStackedPageContainer View。

接下来查看 SPStackedNavigationController.m 文件的 - (UIViewController *)popViewControllerAnimated:(BOOL)animated 方法来验证一下我们的猜测。

- (UIViewController *)popViewControllerAnimated:(BOOL)animated{    UIViewController *viewController = [[self childViewControllers] lastObject];    if (!viewController)        return nil;    [self willChangeValueForKey:@"viewControllers"];    [viewController willMoveToParentViewController:nil];    if ([self isViewLoaded])    {        // 关键步骤 ,将 SPStackedPageContainer 标记为移除状态,后续 SPStackedNavigationScrollView 会将它移除        SPStackedPageContainer *pageC = [_scroll containerForViewController:viewController];        pageC.markedForSuperviewRemoval = YES;    }    //关键步骤,移除 viewController    [viewController removeFromParentViewController];    [self didChangeValueForKey:@"viewControllers"];    [self setActiveViewController:[self.childViewControllers lastObject]                         position:SPStackedNavigationPagePositionRight                         animated:animated];    return viewController;}

如我们猜测 SPStackedNavigationController 做 pop 操作的时候,就是在 SPStackedNavigationScrollView 这个View 移除一个 SPStackedPageContainer View。并让 SPStackedPageContainer 对应的 ViewController 发一个 removeFromParentViewController 的消息。

SPStackedPageContainer 解析

SPStackedPageContainer 的作用是承载 ViewController 的 View,并对一些手势动作进行处理,在这里 SPStackedPageContainer 这个概念在这里等同于一个分屏 View。

打开 SPStackedPageContainer.m 查看 - (void)setVCVisible:(BOOL)VCVisible 方法。

//将VC的View加到Container里面- (void)setVCVisible:(BOOL)VCVisible{    if (VCVisible == self.VCVisible) return;    if (VCVisible) {        [self.screenshot removeFromSuperview];        self.screenshot = nil;        if (!self.markedForSuperviewRemoval || [_vc isViewLoaded])        {            _vcContainer.backgroundColor = _vc.view.backgroundColor;            _vc.view.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);            if (!_vc.view.superview)                // 关键步骤 添加 View                [_vcContainer insertSubview:_vc.view atIndex:0];        }    } else {        if ([_vc isViewLoaded])            // 关键步骤 移除 View            [_vc.view removeFromSuperview];    }}

SPStackedNavigationScrollView 解析

SPStackedNavigationScrollView 是一个模仿 UIScrollView 实现的 View。关于 UIScrollView 的深入理解,推荐 ObjC 中国的文章 理解 Scroll Views, 这里就不再详述,默认大家都是能理解 UIScrollView 的相关概念。

当使用 SPStackedNavigationController 做3次 Push 操作的时候, SPStackedNavigationScrollView 的 View 层次结构是这样的。

QQ截图20170421093206.png

SPStackedNavigationScrollView 的层次结构

SPStackedNavigationController 的 rootView 就是 Container0 这个 View。而 Push 的 View 分别是 Container1,Container2,Container3。左边的半屏 View 的位置从底往上分别是 Container1 --> Container2。右边的半屏 View 则是 Container3。若是 SPStackedNavigationController 再 Push 一个 View 的话,那么 左边的半屏 View 的位置从底往上分别是 Container1 --> Container2 --> Container3 。右边的半屏 View 则是 Container4,Container 这个概念在这里等同于一个分屏 View。 在这个时候 SPStackedNavigationScrollView 的View 的简单示意图如下

QQ截图20170421093306.png

SPStackedNavigationController 的 push 操作

从上面的 View 结构示意图中可以看出,SPStackedNavigationScrollView 对 UIScrollView 的模仿主要体现在 UIScrollView 的滑动机制上。

当 SPStackedNavigationController 做 push 操作的时候,SPStackedNavigationScrollView 右边半屏的 View 会从右向左滑动到左边半屏的位置,而右边半屏则从右向左显示一个新的 push 进来的 View。

当 SPStackedNavigationController 做 pop 操作的时候,SPStackedNavigationScrollView 右边半屏的 View 会从左向右滑动出屏幕显示范围,而左边半屏的 View 则会从左向右滑动到右边半屏。

63.png

SPStackedNavigationController 的 pop 操作

讲完了 SPStackedNavigationScrollView 的大概表现之后,若是大家还是不怎么了解的话,可以运行 Demo 详细体会SPStackedNavigationScrollView 的UI变化。

我们接下来查看 SPStackedNavigationScrollView.h 文件,寻找和 UIScrollView 相关的代码。

@interface SPStackedNavigationScrollView : UIView // ...... 省略代码@property(nonatomic) CGPoint contentOffset;- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;- (NSRange)scrollRange;// ...... 省略代码@end

从 SPStackedNavigationScrollView 的头文件中,我们可以看到 SPStackedNavigationScrollView 继承于 UIView。和 UIScrollView 相关的概念有 contentOffset 和 scrollRange。关于 UIScrollView 的深入理解,推荐 查看 ObjC 中国的文章 理解 Scroll Views ,这里就不再详述,默认大家都是能理解 UIScrollView 的相关概念。

接下来开始讲解 SPStackedNavigationScrollView 的具体实现。

看下面的图,当屏幕上只有 rootView 没有分屏的 View 的时候 SPStackedNavigationScrollView 的 frame 的坐标原点是在 rootView 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = 0。

656644-62616380e6de3fc5.png

contentOffset = 0

接着看图,当屏幕上出现一个分屏的 View 的时候,我们叫这个 View 为 Container1。 SPStackedNavigationScrollView 的 frame 的坐标原点是在 Container1 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = rootView.width / 2。

656644-d067b6f3aff9affc.png

contentOffset = rootView.width / 2

接着看图,当屏幕上出现二个分屏的 View 的时候,我们分别叫这二个 View 为 Container1 和 Container2。 SPStackedNavigationScrollView 的 frame 的坐标原点是在 Container1 的左上角,这个时候SPStackedNavigationScrollView 的 contentOffset = rootView.width。

656644-a785e46411ff894d.png

contentOffset = rootView.width

从上面的示意图中不难看出理解 SPStackedNavigationScrollView 的重点在于理解 SPStackedNavigationScrollView 不断变化的 frame 原点 和 contentOffset。只要 contentOffset 发生了变化,那么 SPStackedNavigationScrollView 就会发生滚动。


查看 SPStackedNavigationScrollView.m 文件,看到了2个和contentOffset相关的变量 _actualOffset 和 _targetOffset,接下来跟踪这2个变量的变化。

@implementation SPStackedNavigationScrollView {    CGPoint _actualOffset; //模拟 ScrollView 当前的 contentOffset    CGPoint _targetOffset;// 模拟 ScrollView 将要滚动到的 contentOffset}

查看 SPStackedNavigationScrollView 的 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated 方法,作用是赋值 _targetOffset 和 _actualOffset 。

// 模仿 UIScrollView 滚动到指定位置- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated{    //  给 _targetOffset 赋值    _targetOffset = contentOffset;    if (animated)        [self animateToTargetScrollOffset];    else {    //  给 _actualOffset 赋值        _actualOffset = _targetOffset;        if (_onScrollDone)        {            self.onScrollDone();            self.onScrollDone = nil;        }       // 关键步骤        [self setNeedsLayout];    }}

UIView 在调用 setNeedsLayout 方法之后,会调用 layoutSubviews 方法。接下看查看该方法。

- (void)layoutSubviews{    // pen 的作用是stretch scroll at start and end    // 用于在第一屏从左向右拉扯和最后一屏从右向左拉扯,    // 让手势拖动的距离2倍于View移动的距离。    // _actualOffset 改变之后,通过特定的规则计算 pen 的 frame,然后将 frame 赋值给 View ,    // 总之作用就是调整 View 的 frame 位置    // 可以说 pen 就是对应的每个分屏的 frame    CGRect pen = CGRectZero;    // 为什么需要 -  _actualOffset.x ?    // 为了得到每个分屏 View 的坐标的 X 值 (坐标原点是 SPStackedNavigationScrollView 的坐标原点,即在屏幕范围内的最左边的分屏 View 的左上角位置)    // 详见 ContentOffset 的计算方法    pen.origin.x = -_actualOffset.x;    // stretch scroll at start and end    if (_actualOffset.x < 0){        // 第一页从左向右拉扯 _actualOffset.x < 0 才成立,        // _actualOffset 就是当前模仿的 UIScrollView 的 contentOffset        // 手势拖动的距离2倍于 View 移动的距离        pen.origin.x = -_actualOffset.x/2;    }    CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject];    if (_actualOffset.x > maxScroll){            pen.origin.x = -(maxScroll + (_actualOffset.x-maxScroll)/2);    }    int i = 0;    // markedForSuperviewRemovalOffset 标记 pageC 自己的 offset 坐标    // 用来给 superview 把 pageC 从当前位置移动到 markedForSuperviewRemovalOffset 指定的坐标    // 可以让自己的 View 对边缘层叠效果做出对应的位置    // 也可以让 pageC 自己全屏或者半屏,    CGFloat markedForSuperviewRemovalOffset = pen.origin.x;// View 的坐标位置x    NSMutableArray *stackedViews = [NSMutableArray array];    for(SPStackedPageContainer *pageC in self.subviews) {        pen.size = pageC.bounds.size;        pen.size.height = self.frame.size.height;        if (pageC.vc.stackedNavigationPageSize == kStackedPageFullSize)            pen.size.width = self.frame.size.width;        CGRect actualPen = pen;        if (pageC.markedForSuperviewRemoval)            actualPen.origin.x = markedForSuperviewRemovalOffset;        // Stack on the left        // 小于 (0,1,2,3)*3        // 左边是一个 stackedViews,最多有3层边缘层叠效果        if (actualPen.origin.x < (MIN(i, 3))*3){           // 如果actualPen.origin.x 小于 (MIN(i, 3))*3 那么说明该 pageC 的位置不是在 stackedViews 最顶部的三个以内           [stackedViews addObject:pageC];        }else{           pageC.hidden = NO;        }        if (self.scrollAnimationTimer == nil)            // floorf取整操作            actualPen.origin.x = floorf(actualPen.origin.x);        // 改变pageC.frame,那么pageC就会动了        pageC.frame = actualPen;        markedForSuperviewRemovalOffset += pen.size.width;        // NavVC 做 POP 操作的时候会将 markedForSuperviewRemoval 置为 YES        // 前面 pen.origin.x = -_actualOffset.x;        // 这里计算下一个屏幕的位置 frame 的 x 值        // 所以需要加上 pen.size.width        if (!pageC.markedForSuperviewRemoval)            pen.origin.x += pen.size.width;        // 覆盖不透明度        if (actualPen.origin.x <= 0 && pageC != [self.subviews lastObject]) {            // abs()绝对值函数            pageC.overlayOpacity = 0.3/actualPen.size.width*abs(actualPen.origin.x);        } else {            pageC.overlayOpacity = 0.0;        }        i++;    }    i = 0;    for (NSInteger index = 0; index < [stackedViews count]; index++)    {        SPStackedPageContainer *pageC = stackedViews[index];        // stackedViews 包括 RootVC 的 View;        // stackedViews 里面的最后3个 View 显示        if ([stackedViews count] > 3 && index < ([stackedViews count]-3))            pageC.hidden = YES;        else        {            // 左边是一个 stackedViews,最多有3层边缘层叠效果            pageC.hidden = NO;            CGRect frame = pageC.frame;            // 调整坐标,显示层叠效果            frame.origin.x = 0 + MIN(i, 3)*3;            pageC.frame = frame;            i++;        }    }    // Only make sure we show what we need to, don't unload stuff until we're done animating    [self updateContainerVisibilityByShowing:YES byHiding:NO];}

在 layoutSubviews 方法里面 根据 _actualOffset 计算好每个分屏的 frame ,以及哪些分屏是可以显示在屏幕上的,哪些分屏是需要移除的,哪些分屏的位置是在屏幕显示的分屏的左边,哪些分屏的位置是在屏幕显示的分屏的右边。

在layoutSubviews 方法里面调用了一个方法用于控制分屏 View 的显示与隐藏,在这里分屏 View的概念可以等同于SPStackedPageContainer。这个方法是 - (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide 。

- (void)updateContainerVisibilityByShowing:(BOOL)doShow byHiding:(BOOL)doHide{    // fabsf 浮点数的绝对值    // 分屏 View 是否需要弹跳效果    BOOL bouncing = self.scrollAnimationTimer && fabsf(_targetOffset.x - _actualOffset.x) < 30;    // layoutSubViews的 pen 是一个 frame、    // 这里的 pen 是一个 frame 的 x 坐标    // 但是用法和 layoutSubViews 的 pen 没什么区别    CGFloat pen = -_actualOffset.x;    // stretch scroll at start and end    if (_actualOffset.x < 0)        pen = -_actualOffset.x/2;    CGFloat maxScroll = [self scrollOffsetForAligningPageWithRightEdge:self.subviews.lastObject];    if (_actualOffset.x > maxScroll)        pen = -(maxScroll + (_actualOffset.x-maxScroll)/2);    // 用来让 SuperView 移动 pageC 的 x 坐标,原点是屏幕显示的最左边的分屏的 X 坐标    CGFloat markedForSuperviewRemovalOffset = pen;    NSMutableArray *viewsToDelete = [NSMutableArray array];    for(SPStackedPageContainer *pageC in self.subviews) {        CGFloat currentPen = pen;        // 该 pageC 被做了 POP 操作,需要被 SuperView移除        if (pageC.markedForSuperviewRemoval)            currentPen = markedForSuperviewRemovalOffset;        // 该分屏是否是在屏幕可见的分屏的右边同时无法看见该分屏        BOOL isOffScreenToTheRight = currentPen >= self.bounds.size.width;        NSRange scrollRange = [self scrollRangeForPageContainer:pageC];        // View 是否被其他 View 覆盖了        BOOL isCovered = currentPen + scrollRange.length <= 0;        // View 现在是否可见        BOOL isVisible = !isOffScreenToTheRight && !isCovered;        // pageC 的可见性发生变化 && ( (isVisible == NO  && doHide == Yes)  ||  isVisible == Yes && doShow ==Yes)        // 只要 pageC 的可见性发生变化,不管是隐藏还是显示都执行下面的if条件分支        if (pageC.VCVisible != isVisible && ((!isVisible && doHide) || (isVisible && doShow)))        {            // pageC分屏将出现            // pageC分屏将离开屏幕            //(isVisible == No || bouncing == No || (isVisible ==Yes && needsInitialPresentation == Yes))            if (!isVisible || !bouncing || (isVisible && pageC.needsInitialPresentation)) {                pageC.needsInitialPresentation = NO;                pageC.VCVisible = isVisible;            }        }        // 要隐藏 pageC 并且该 pageC 被标记为销毁的        //(doHide ==Yes && pageC.markedForSuperviewRemoval ==Yes)        // 将 pageC 加入销毁数组 viewsToDelete        if (doHide && pageC.markedForSuperviewRemoval)            [viewsToDelete addObject:pageC];        //经过 Demo 验证 pen 和 markedForSuperviewRemovalOffset 的值一样        markedForSuperviewRemovalOffset += pageC.frame.size.width;        // markedForSuperviewRemoval = No        // 计算 pen 的值,该值为下一个分屏的 X 坐标        if (!pageC.markedForSuperviewRemoval)            pen += pageC.frame.size.width;    }    // 对viewsToDelete数组里面的View执行销毁操作    [viewsToDelete makeObjectsPerformSelector:@selector(removeFromSuperview)];}

限于篇幅关系无法一一介绍SPStackedNavigationScrollView 的各种实现。

未介绍的细节知识点包括但不限于 NSRunLoop,用于 SPStackedNavigationScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。SPStackedNavigationScrollView 的 scrollRange 的计算细节,SPStackedNavigationScrollView 的手势处理等等,大家若是有兴趣可以在我的 GitHub 上下载对应注释版本源码,地址https://github.com/junbinchencn/SPStackedNav-Note 。

总结

SPStackedNav 项目是一个用于 iPad 分屏的 UI 解决方案。该方案的核心在于 SPStackedNavigationScrollView 这个类。SPStackedNavigationScrollView 模仿了 UIScrollView 的实现。SPStackedNav 的分屏方案的设计非常精巧,实现思路清晰明确,实现过程中的很多细节还是非常具有参考和学习价值的,一些 contentOffset 的计算方法还是非常巧妙的。本人能力有限,文章难免有不足之处,若是您有发现,请在评论中指出,确认之后马上修改,谢谢!

参考

理解 Scroll Views https://www.objccn.io/issue-3-2/

SPStackedNav https://github.com/spotify/SPStackedNav

SPStackedNav-Note https://github.com/junbinchencn/SPStackedNav-Note

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台