最近在写电子书app的时候发现textView通过contentSize拿不到真实高度了,上网查过后才知道ios7更新了textView内部的初始化机制,需要通过NSString的
boundingRectWithSize:options:attributes:context:方法来获得textView内容的高度,也就是textView的真实高度。
可是问题是,我现在加入了一个功能:底部有一个slider。用户可以拖动slider来实现页数之间的跳动。
最初的页数实现思路非常简单:textView毕竟是scrollView的子类,拿到textView的contentOffset.y,除以用boundingRectWithSize:options:attributes:context:方法获得的rect的高度height,结果加1,记为currentPage。然后用currentPage去乘以textView的高度height,将该值设为新的contentOffset。说起来麻烦,直接上代码:
#pragma mark - Delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UITextView *textView = (UITextView *)scrollView;
//通过简单计算获得当前page
NSInteger page = (NSInteger)(textView.contentOffset.y / textView.height) + 1;
//调整page
if (page < 1 || page > self.allPages) {
return;
}
//保存page
self.currentPage = page;
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInteger:page] forKey:kCurrentPage];
[[NSUserDefaults standardUserDefaults] synchronize];
//设置底部标签的值
self.pageLabel.text = [NSString stringWithFormat:@"%d / %d", self.currentPage, self.allPages];
//设置slider的值
[self.slider setValue:self.currentPage animated:YES];
}
以上是scrollview代理方法,其中有page的计算方法
NSInteger page = (NSInteger)sender.value;
[self.textView setContentOffset:CGPointMake(0, page * self.textView.height) animated:NO];
self.pageLabel.text = [NSString stringWithFormat:@"%d / %d", page, self.allPages];
这是slider的方法,在其中拿到slider的value然后设置textView的contentOffset。
这种实现思路简单,本以为应该可以达到预计效果,结果却让我大失所望。
简单思路带来的问题:
由于目前bug已经改过来,所以懒得截图了╮(╯▽╰)╭。这里描述一下好了:
问题是,如果用拖动的方法浏览文章,那么一切正常,可以到最底下并且最后的页数也正确。然而如果刚打开应用就直接拖slider,会出现页数“缺失”现象:通过bounding方法计算出页数为300页左右,但是拖到50页左右就到文章底部了,往后拖就没有内容了。害得我以为是bounding方法出的问题。这个问题确实奇怪。我在网上没有见过类似描述。后来我再检查时发现当普通拖动时右边滚动条有些奇怪:大概50页左右就到下边3/4左右了,但是再往下拖时它不怎么动,有种下拉刷新时那种延迟加载的感觉,我怀疑textView有延迟加载机制,在展示内容过多时它会将部分内容省略直到需要展示的时候才展示出来。可惜网上搜textView的延迟加载毫无线索- -
又一种简单思路:
本来想去掉slider,这样就可以省好多工作,更何况code4app上也没有合适的slider控制textView的实例,所以改起来麻烦重重,但是看到现在主流的电子书应用都会有这一功能,于是决定攻下这一关。
既然一次性展示这么多数据不行,那就一次只展示一页的数据好了,然后禁止用户拖动textView,只允许左右滑动翻页或者拖动slider来改变页数。于是又一个简单地思路出现了:获得要展示string的length,除以总页数得到每页要展示的长度将其展示。这个思路不用说一定是失败的,用户体验绝对不会好,而且会导致最后一页展示不去的问题。总之是绝对不会被应用的一种想法。其代码非常简单:
NSInteger contentLengthInEveryPage = self.content.length / self.allPages;
NSString *contentInEveryPage = [self.content substringWithRange:NSMakeRange((self.currentPage - 1) * contentLengthInEveryPage, contentLengthInEveryPage)];
self.textView.text = contentInEveryPage;
果然偷懒是没有好结果的- -
解决方法:
后来从别人的博客中学习到了textView的一种很好地分页方法,在此特地记录学习
首先通过bounding方法计算出总共要展示的文本的高度
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:self.textView.font, NSFontAttributeName, paragraphStyle.copy, NSParagraphStyleAttributeName, nil];
//计算出要展示文章的高度
CGRect totalTextSize = [self.content boundingRectWithSize:CGSizeMake(self.textView.width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil];
计算总共的高度的目的是为了计算理想状态下的每页显示的字符数从而展开进一步计算,不过在此之前要做一个判定:有时用户的电子书是从网络上下载的,这些电子书并不一定是成千上万行的文本,也有可能是三行情诗呢O(∩_∩)O~,所以为了通用性及效率,当一页就能展示这些文本时就无需继续计算了.
//当字符过少时,不用分页
if (totalTextSize.size.height < self.textView.height) {
_charsPerPage = [self.content length];
_charsOfLastPage = [self.content length];
self.allPages = 1;
return NO;
}
其中私有变量_charPerPage和_charOfLastPage分别代表每页的字符数和最后一页的字符数,为什么最后一页要单独处理呢,这是为了防止越界问题,接着往下看即可知道。这里返回NO表示不用分页。
然后我们就可以开始我们的计算了,首先计算理想状态下每页的字符数,以后我们就根据这个进行调整。
//理想状态下的总页数和每页的字符数
NSInteger referTotalPage = (NSInteger)(totalTextSize.size.height / self.textView.height) + 1;
NSInteger referCharsPerPage = self.content.length / referTotalPage;
如果此时计算出来每页字符数过多,那么为了效率可以调整每页字符数
//如果每页字符数过多,则取上界以减少调整时间
if (referCharsPerPage > 1000) {
referCharsPerPage = 1000;
}
接着将referCharsPerPage数目的字符拿出来计算高度,有时第一页的内容会很少,为了防止计算不准确,我们可以取第二页,当然了,像哈利波特一样,第二页就一作者给他人的寄语,那就实在没办法了,也许这里可以改变算法
//为防止第一页字符数过少,从第二页开始获取每页的理想状态下字符数
NSRange range = NSMakeRange(referCharsPerPage, referCharsPerPage);
NSString *pageText = [self.content substringWithRange:range];
CGSize pageTextSize = [pageText boundingRectWithSize:CGSizeMake(self.textView.width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
接着有了高度以后就判断是否能容纳在textView高度的区域内,如果不能,则继续调整,直到能容纳
//一直调整每页字符数直到一页可以容纳下为止
while (pageTextSize.height > self.textView.height) {
referCharsPerPage -= 2;
range = NSMakeRange(0, referCharsPerPage);
pageText = [self.content substringWithRange:range];
pageTextSize = [pageText boundingRectWithSize:CGSizeMake(self.textView.width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil].size;
}
最后记录结果,算法完成
//记录计算结果
_charsPerPage = referCharsPerPage;
self.allPages = (NSInteger)(self.content.length / _charsPerPage) + 1;
_charsOfLastPage = self.content.length - (self.allPages - 1) * _charsPerPage;
return YES;
有了分页以后,在slider的响应方法中就可以调整要显示的字符串了
- (void)p_gotoPage:(UISlider *)sender
{
NSInteger page = sender.value;
self.currentPage = page;
[self p_saveCurrentPage];
NSString *contentInEveryPage = nil;
if (page == self.allPages) {
contentInEveryPage = [self.content substringWithRange:NSMakeRange((self.currentPage - 1) * _charsPerPage, _charsOfLastPage)];
} else {
contentInEveryPage = [self.content substringWithRange:NSMakeRange((self.currentPage - 1) * _charsPerPage, _charsPerPage)];
}
self.textView.text = contentInEveryPage;
}
这里由于要用到range,为防止越界,需要计算最后一页的字符数并单独保存。
通过这样分页,可以达到一种每页字符数均匀并且不会出现最后一页缺失的现象。