前言

前文介绍的是小说阅读器的设计和实现,本文作为补充对多种翻页模式做详细剖析。

正文

常见的阅读器翻页模式包括:平移、仿真、滑页和上下:

平移:左右滑动;

仿真:左右滑动;(纸质书翻页效果)

滑页:左右滑动;(覆盖效果)

上下:上下滑动;

1、平移

UIKit提供UIPageViewController可以很方便实现平移的页面切换效果,使用流程:

1、创建UIPageViewController;

self.pageVC = [[UIPageViewController alloc]

initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll

navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal

options:

@{

UIPageViewControllerOptionSpineLocationKey:@(UIPageViewControllerSpineLocationMin)

}];

self.pageVC.delegate = self;

self.pageVC.dataSource = self;

[self addChildViewController:self.pageVC];

[self.view addSubview:self.pageVC.view];

2、初始化首个界面;

- (void)customInitFirstPage {

UIViewController *vc = [self getRandomVCWithIndex:5];

[self.pageVC setViewControllers:@[vc]

direction:UIPageViewControllerNavigationDirectionReverse

animated:NO

completion:^(BOOL finished) {

}];

}

3、滑动时返回相邻的界面;

#pragma mark - UIPageViewControllerDelegate

- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

UIViewController *ret;

UIViewController *vc = viewController;

if (vc) {

NSInteger index = vc.view.tag;

if (index > 0) {

ret = [self getRandomVCWithIndex:index - 1];

}

}

return ret;

}

- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

UIViewController *ret;

UIViewController *vc = viewController;

if (vc) {

NSInteger index = vc.view.tag;

if (index < 10) {

ret = [self getRandomVCWithIndex:index + 1];

}

}

return ret;

}

2、仿真

相对安卓,iOS实现这个翻页效果非常方便——UIPageViewController同样支持这个翻页效果。

使用流程和平移类似,但多了一些注意事项:

initWithTransitionStyle:由UIPageViewControllerTransitionStyleScroll变为UIPageViewControllerTransitionStyleScroll;

支持翻页的时候,对背面做一个自定义展示,需要打开self.pageVC.doubleSided = YES;;

初始化界面的时候和平移一样,但是在使用过程中再调用-setViewControllers时,如果animated的参数为YES,则需要手动传入两个vc,如下:

- (void)manualChangePage {

UIViewController *vc = [self getRandomVCWithIndex:5];

NSArray *arr;

if (self.pageVC.doubleSided) {

BackViewController *backVC = [[BackViewController alloc] init];

[backVC updateWithViewController:vc];

backVC.view.tag = vc.view.tag;

arr = @[vc, backVC];

}

else {

arr = @[vc];

}

[self.pageVC setViewControllers:arr

direction:UIPageViewControllerNavigationDirectionReverse

animated:YES

completion:^(BOOL finished) {

}];

}

设置doubleSided为YES之后,每次翻页会调用两次viewControllerAfterViewController或viewControllerBeforeViewController,需要特殊返回一个BackViewController作为背面的VC:

- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

UIViewController *ret;

UIViewController *vc = viewController; // 注意这里不是pageViewController.viewControllers

if (vc) {

NSInteger index = vc.view.tag;

if (index > 0) {

if ([vc isKindOfClass:BackViewController.class]) {

ret = [self getRandomVCWithIndex:index - 1];

}

else {

BackViewController *backVC = [[BackViewController alloc] init];

[backVC updateWithViewController:vc];

backVC.view.tag = vc.view.tag;

ret = backVC;

}

}

}

return ret;

}

背面的VC可以添加自定义的view,但通常采用的做法是作为当前界面的镜像(用截图的方式):

- (UIImage *)captureView:(UIView *)view {

if ([self checkNullRect:view]) {

return nil;

}

CGRect rect = view.bounds;

UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0f);

CGContextRef context = UIGraphicsGetCurrentContext();

CGAffineTransform transform = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, rect.size.width, 0.0);

CGContextConcatCTM(context,transform);

[view.layer renderInContext:context];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

return image;

}

增加的-checkNullRect:方法是避免iOS9可能出现的frame为CGRectNull的crash。

- (BOOL)checkNullRect:(UIView *)view {

BOOL ret = CGRectIsNull(view.frame);

for (UIView *subView in view.subviews) {

ret = ret || [self checkNullRect:subView];

}

return ret;

}

3、滑页

滑页没有系统库支持,需要手动实现。

对前面两种翻页模式进行分析,我们可以发现一些共性,比如说以页(VC)为单位、实时获取界面VC和页面之间有先后顺序等。

分解UI层的实现,整个动画可以用以下流程来表示:

1、页面初始化,直接显示页面,监听用户pan手势;

2、用户pan手势开始,根据方向确定左滑还是右滑,获取新的VC;

3、处理用户左右滑动,视图跟随用户滑动;

4、用户pan手势结束,根据动画完成程度确定是补齐动画还是回退;

5、处理完动画相关,将状态重置为1,接受用户的pan手势;

如果还要支持tap手势,则自动完成一次动画效果,再将状态重置为status_show(只有在此状态才响应tap的手势)。

核心逻辑:

pan手势开始时,记录点的位置:

CGPoint point = [rec translationInView:self.view];

static CGPoint startPoint;

//手势开始

if (rec.state == UIGestureRecognizerStateBegan) {

startPoint = point;

}

pan手势触发过程中,先确定方向,再获取对应的VC;然后根据左右滑动,分别改变位置(showVC对应不不动的VC,moveVC跟着pan手势移动):

//手势进行

if (rec.state == UIGestureRecognizerStateChanged) {

if (self.currentStatus == SSReaderPageEffectViewStatusDefault) { // 用户开始移动,此时判断是左移还是右移

if (point.x >= startPoint.x) { // 右移

self.currentStatus = SSReaderPageEffectViewStatusMovingToLastPage;

}

else {

self.currentStatus = SSReaderPageEffectViewStatusMovingToNextPage;

}

if (self.delegate) {

if (self.currentStatus == SSReaderPageEffectViewStatusMovingToLastPage) {

UIViewController *lastVC = [self.delegate slideViewControllerGetLastVC:self];

if (!lastVC) {

[rec cancelCurrentGestureReccongizing];

self.currentStatus = SSReaderPageEffectViewStatusDefault;

SSLOG_INFO(@"info, reach last end");

}

else {

[self addChildViewController:lastVC];

[self.view insertSubview:lastVC.view aboveSubview:self.showVC.view];

self.moveVC = lastVC;

[self addMaskToVC:self.moveVC];

}

}

else if (self.currentStatus == SSReaderPageEffectViewStatusMovingToNextPage) {

UIViewController *nextVC = [self.delegate slideViewControllerGetNextVC:self];

if (!nextVC) {

[rec cancelCurrentGestureReccongizing];

self.currentStatus = SSReaderPageEffectViewStatusDefault;

SSLOG_INFO(@"info, reach next end");

}

else {

[self addChildViewController:nextVC];

[self.view insertSubview:nextVC.view belowSubview:self.showVC.view];

self.moveVC = self.showVC;

self.showVC = nextVC;

[self addMaskToVC:self.moveVC];

}

}

if (self.currentStatus == SSReaderPageEffectViewStatusMovingToLastPage) {

[self.delegate slideViewController:self willTransitionToViewControllers:self.moveVC];

}

else if (self.currentStatus == SSReaderPageEffectViewStatusMovingToNextPage) {

[self.delegate slideViewController:self willTransitionToViewControllers:self.showVC];

}

}

}

if (self.currentStatus == SSReaderPageEffectViewStatusMovingToNextPage) {

self.moveVC.view.right = self.view.width * (1 - rate);

}

else if (self.currentStatus == SSReaderPageEffectViewStatusMovingToLastPage) {

self.moveVC.view.right = self.view.width * rate;

}

}

pan手势结束时,根据动画完成程度决定是否完成该动作(用animateWithDuration:的动画block来完成);

注意事项:

滑页效果通常都需要添加一个阴影效果,可以对showVC进行处理:

- (void)addMaskToVC:(UIViewController *)vc {

vc.view.layer.shadowColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.8].CGColor;

vc.view.layer.shadowOffset = CGSizeMake(5, 5);

vc.view.layer.shadowOpacity = 0.8;

vc.view.layer.shadowRadius = 6;

}

在手势结束的时候,除了根据动画完成程度来判断是否完成该动作外,速度通常也会作为参考值:

CGPoint speed = [rec velocityInView:rec.view];

rate = (rate >= kCompleteRate || fabs(speed.x) > 200) ? 1 : 0; // 经验数值,多次尝试得出

另外一个问题是手势在进行到一半时如果APP切入后台,动画出现暂停的情况。这是因为pan手势在切后台时会自动cancel,所以需要在手势处理增加对cancel状态的处理。

4、上下滑动

上下滑动同样没有系统库支持,需要手动实现。

效果分解:

1、当用户滑动的过程,视图要跟随手指的移动;

2、当用户往上滑然后松开时,视图要带有加速度的往上滑动;(附加特性:在滑动过程中用户可以通过重复这个行为加速滑动)

3、在视图滑动的过程中,用户可以通过简单的tap操作停止交互;

用户的交互有3种touchBegin/touchMove/touchEnd,上述的三个效果实现如下:

1、监听touchMove,计算手指的移动距离,换算成view的移动;

2、touchEnd之后,根据pan手势的移动速度和原来的滑动速度,计算得到滑动的新初始速度;

3、touchBegin开始,讲当前速度重置为0;

上述的过程2的处理非常复杂,需要考虑原来的滑动速度,才能实现效果分解中的附加特性。

通常iOS实现滑动会有两大选择:UIScrollView和UITableView;(UICollectionView和UITableView类似)

UIScrollView存在一个较大的局限:上面的视图资源无法回收利用,当添加的view过多的时候会占用内存;

UITableView用cell重复利用规避上面的局限,但是存在新的问题:当数据源(排版数据)变化时,需要频繁调用reloadData,造成性能瓶颈;同时reload会造成contentSize和contentOffset的改变,导致界面可能会出现闪烁,需要各类逻辑的特殊处理。

综上的分析,这里提供一个基于UIScrollView的方案,避免去手动计算速度,也可以及时回收内存,并且contentSize一直保持不变。

以下图为例,我们使得UIScrollView的contentSize为(view.width, 3*view.height),偏移contentOffsetY为view.height(初始状态相当于将窗口放置在中间):

B是我们创建的第一个vc,大小和UIScrollView的size一样大;当我们向下滑动时,我们创建vcA放在B的上面;

当我们上滑到vcA完全展示的时候,vcB已经滑动到屏幕外面(红色为窗口大小);此时我们回收vcB,然后将UIScrollView的Y偏移重新改为view.height,回到了初始化状态。

同理,我们可以处理向上滑动的情况。至此,我们可以不依赖UITableView完成无限视图的滚动,同时避免各类touch事件处理和加速度计算。

简单的实现效果

上图的实现过程非常简短:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

if (self.scrollView.contentOffset.y >= (self.scrollView.contentSize.height - self.height)) {

UIView *firstView = [self.viewArr firstObject];

[self.viewArr removeObjectAtIndex:0];

firstView.top = self.scrollView.contentSize.height;

[self.viewArr addObject:firstView];

for (UIView *view in self.viewArr) {

view.top -= self.height;

}

[self.scrollView setContentOffset:CGPointMake(0, self.scrollView.contentOffset.y - self.height)];

}

}

基于出延伸出来我们的整体流程图:

遇到的问题(Q&A):

Q:如何实现UIScrollView改变offset,但是继承原来的速度?

A:

[self.scrollView setContentOffset:CGPointMake(0, self.view.height) animated:NO];

[self.scrollView setContentOffset:CGPointMake(0, self.view.height);

上面两个API均可以改变offset,但是-setContentOffset:animated:会使得当前的速度重置为0,使得跨页时滑动不流畅;使用-setContentOffset:可以解决这个问题,仅仅改变offset,并且继承原来的速度接着运动;

Q: -scrollViewDidScroll:方法怎么会出现递归循环调用?

A:

在通过-setContentOffset:改变offset之后,仍会触发-scrollViewDidScroll:的回调,如果在此回调又触发了offset的改变,则进入了递归调用的坑,从下图的堆栈可以看到:

解决办法是在设置偏移时,先把delegate取消,修改完成后再赋值回去:

- (void)safeSetContentOffsetY:(CGFloat)y {

self.scrollView.delegate = nil;

[self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, y) animated:NO];

self.scrollView.delegate = self;

}

Q: 滑动到最后一页的时候,没有再往下的VC(返回的nextVC为nil),如果用户没有中断手势继续滑动,如何避免触发再次获取nextVC?

A:

当滑动到最后一页的时候,此时没有nextVC,无法接着往下滑,但是因为手势还在,会频繁触发getNextVC的方法。对此可以新增手势取消的方法:

- (void)cancelCurrentGestureReccongizing {

// disabled gesture recognizers will not receive touches. when changed to NO the gesture recognizer will be cancelled if it's currently recognizing a gesture

self.enabled = NO;

self.enabled = YES;

}

Q:滑页效果,在进行到一半时切入后台,如何避免动画出现异常现象?

A:

这是因为pan手势在切后台时会自动cancel,所以需要在手势处理增加对cancel状态的处理;

Q:如果初始化的时候,传进的VC.view不满一屏,该如何处理?

A:

手动填充到满屏幕。

- (void)fullFillContent {

CGFloat downFillY;

if (self.viewControllers && self.viewControllers.count > 0) {

UIViewController *vc = [self.viewControllers lastObject];

downFillY = vc.view.bottom;

}

else {

downFillY = self.scrollView.contentOffset.y;

}

while (downFillY < windowMaxY) {

if (!self.delegate) {

NSLog(@"error, empty delegate");

break;

}

UIViewController *vc = [self.delegate scrollViewControllerGetNextVC:self];

if (!vc) {

NSLog(@"info, reach next end");

break;

}

[self.vcArr addObject:vc];

[self addChildViewController:vc];

[self.scrollView addSubview:vc.view];

vc.view.top = downFillY;

downFillY = vc.view.bottom;

NSLog(@"info, add next vc, frame:%@", NSStringFromCGRect(vc.view.frame));

}

}

总结

demo地址是在GitHub,包括四种翻页效果,其中的滑页和上下滑动都以参考UIPageViewController的接口做了调整,基本可以直接复制代码进行接入。

上下滑动的代码不多,但是经过多次尝试再有的定论,中间也换过多次方案,最终优化得到的结论就是demo中的做法。

阅读器的翻页模式多种多样,欢迎交流新的翻页模式或者其他实现方案。

控制翻页c语言,阅读器多种翻页的设计与实现相关推荐

  1. boost::graph模块实现Graphviz DOT 语言阅读器

    boost::graph模块实现Graphviz DOT 语言阅读器 实现功能 C++实现代码 实现功能 boost::graph模块实现Graphviz DOT 语言阅读器 C++实现代码 #def ...

  2. linux+手机+翻页,在Android手机上实现阅读器的翻页效果

    本篇文章来谈谈怎么使用java实现翻页效果,就像电子阅读器那样. 现在先来看看翻页的原理图: 先了解各个字母表示的含义: A-把书页翻起来后看到的背面区域 B-把书页翻起来后看到的下一页的一角 C-当 ...

  3. C语言阅读器2.0版

    有好几天没写博客了,原因是C语言学起来有难度,没搞懂的东西,写出来也是害人,今天呢,用这周学的知识,给阅读器进行了迭代,现在是2.0版. 话不多说,上才艺! 啊不,上代码!!! #include &q ...

  4. 使用Python库pyqt5制作TXT阅读器(一)-------UI设计

    项目地址:https://github.com/pikeduo/TXTReader PyQt5中文手册:https://maicss.gitbook.io/pyqt-chinese-tutoral/p ...

  5. 福昕阅读器 单个标签页单个窗口展示

    打开 偏好设置 选择 文档 勾选 ☑️ 允许多实例

  6. c语言数据页,c语言基础--数据类型(51页)-原创力文档

    我们已经看到程序中使用的各种变量都应预先加以说明,即先说明,后使用.对变量的说明可以包括三个方面: ·数据类型 ·存储类型 ·作用域 在本讲中,我们只介绍数据类型说明.其它说明在以后陆续介绍.所谓数据 ...

  7. android 阅读 翻页,极速PDF安卓版如何翻页、阅读模式修改等操作详解

    如今手机几乎代替电脑,曾经用电脑操作的办公软件也逐渐被APP替代.近几年安卓市场上线的极速PDF因其小巧.速度快,也被广大用户下载使用.但使用这款APP阅读PDF文档时如何将左右翻页改成上下翻页,屏幕 ...

  8. Mac苹果电脑上有哪些好用的txt小说阅读器?

    epub.txt是常见的电子书格式,我们在网上下载小说时经常会遇到.Mac电脑由于系统的"挑剔性",想必平时大家通常会遇到自己使用的小说阅读器不能在Mac系统上兼容的问题,今天小编 ...

  9. 用Mac电脑看txt小说,哪些阅读器软件更好用?

    epub.txt是常见的电子书格式,我们在网上下载免费小说时经常会遇到.Mac电脑由于系统的"挑剔性",想必平时大家通常会遇到自己使用的小说阅读器不能在Mac系统上兼容的问题,今天 ...

最新文章

  1. python培训出来的有公司要吗-参加Python培训到底需要学什么?好程序员
  2. Boost:双图bimap的范围标准方式的测试程序
  3. EF中创建、使用Oracle数据库的Sequence(序列)功能
  4. Google PR 到4了
  5. 给Ubuntu安装MacOS主题
  6. 文本密度 php,基于最大文本密度的网页正文抽取方法
  7. 饥荒steam联机版服务器无响应,《饥荒:联机版》服务器卡顿原因分析及解决教程...
  8. ASP.NET Core WebApi返回结果统一包装实践
  9. 计算机病毒有熊猫病毒,世界最厉害的电脑病毒排名 熊猫烧香病毒最使人讨厌...
  10. eclipse vail_在Windows Home Server“ Vail”上安装Microsoft Security Essentials 2.0 Beta
  11. 【大数据入门核心技术-Tez】(三)Tez与Hive整合
  12. 阿里云服务器购买完整流程
  13. [C语言]c语言之strcmp
  14. 2022年全球注释软件行业分析报告
  15. MATLAB 写入数据为科学计数法形式/ e03/ 形式
  16. Android NFC 标签读写Demo与历史漏洞概述
  17. Java 编译与反编译
  18. sql语句去重distinct、统计(count、sum)
  19. win10安装Andorid Studio常见问题
  20. module java.base does not opens java.lang to unnamed module @‘‘xxxxxxxx‘‘

热门文章

  1. API-String中的某些方法
  2. Visual Studio Code 安装Vim插件后,复制(Ctrl C)等快捷键变成Insert 模式的问题
  3. [转载]内存管理与TLB
  4. Visual Studio(VS)2013使用教程
  5. 【机器学习】使用scikitLearn对数据进行聚类:Kmeans聚类算法及聚类效果评估
  6. SNS2124SNS2224SNS2248 光纤交换机配置
  7. 社群就是微信群吗?社群的本质是什么?
  8. html 网页地图集制作ECHARTS,在页面使用echarts的地图(解决地图不完整)
  9. Linux 修改系统时间为东八区时间
  10. 快速了解区块链六大特点