转载

iPad分屏框架-SPStackedNav源码解读

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

iPad分屏框架-SPStackedNav源码解读

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.效果图

iPad分屏框架-SPStackedNav源码解读

效果图1

iPad分屏框架-SPStackedNav源码解读

效果图2

设计

iPad分屏框架-SPStackedNav源码解读

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 的信息呢?

iPad分屏框架-SPStackedNav源码解读

SPSideTabBar的层级结构

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

iPad分屏框架-SPStackedNav源码解读

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;

iPad分屏框架-SPStackedNav源码解读

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 层次结构是这样的。

iPad分屏框架-SPStackedNav源码解读

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 的简单示意图如下

iPad分屏框架-SPStackedNav源码解读

SPStackedNavigationController 的 push 操作

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

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

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

iPad分屏框架-SPStackedNav源码解读

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。

iPad分屏框架-SPStackedNav源码解读

contentOffset = 0

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

iPad分屏框架-SPStackedNav源码解读

contentOffset = rootView.width / 2

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

iPad分屏框架-SPStackedNav源码解读

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

正文到此结束
Loading...