Leaves是由Tow Brow开发的一个简单的图书翻页控件,它巧妙地结合了镜像层、阴影层(用于半透明页)和渐变层(用于阴影)来实现图书的翻页效果。其翻页效果如下图所示:
特性
Leaves支持:
文本、图像、PDF等任何可被渲染到Graphics Context上的对象
通过拖动或点击来翻页
支持ipad和iphone大小的显示区域
Levels目前不支持以下特性
页面上的交互元素
轻扫动作
类和接口
Leaves中主要有三个类:LevelsView、LevelsViewController、LevelsCache:
LevelsCache:是一个辅助类,用于缓存显示页。它将显示的内容缓存为图片并保存。
LevelsView:是翻页视图,翻页的主要效果便在些实现。它定义了一系列的层对象,并通过操作这些层对象来实现翻页中各种效果。
LevelsViewController: LevelsView的控制器
类似于UITableView, LevelsView也有一个相关的数据源类(LeaveViewDataSource)与委托类(LeavesViewDelegate),它们分别有两个方法,如下所示
复制代码
@protocol LeavesViewDataSource <NSObject>
- (NSUInteger) numberOfPagesInLeavesView:(LeavesView*)leavesView;
- (void) renderPageAtIndex:(NSUInteger)index inContext:(CGContextRef)ctx;
@end
@protocol LeavesViewDelegate <NSObject>
@optional
- (void) leavesView:(LeavesView *)leavesView willTurnToPageAtIndex:(NSUInteger)pageIndex;
- (void) leavesView:(LeavesView *)leavesView didTurnToPageAtIndex:(NSUInteger)pageIndex;
@end
层树结构
LevelsView中的层树结构如下图所示:
每一个层(Layer)都有其特殊的用途,或作为内容的显示层,或作为阴影层,具体说明如下:
topPage层:显示当前页的内容。
topPageOverlay层:在翻页过程中,该层覆盖于topPage层上,且颜色偏暗,从而使topPage未翻转的部分变暗,有阴影的感觉。
topPageShadow层:在翻页过程中,该层用于表达topPage被翻转部分所形成的阴影。
topPageReverse层:翻页过程中,topPage被翻转部分的反面的容器层。
topPageReverseImage层:反面的内容页。在竖屏下,用于显示topPage被翻转部分的内容,这些内容被映射到该层,给人感觉书是透明的。在横屏下,显示的是下一页的内容。
topPageReverseOverlay层:该层用于覆盖topPageReverse层,效果与topPageOverlay类似。
topPageReverseShading层:该层在topPageReverse层右侧形成一个阴影。
bottomPage层:topPage页的下一页所在的层。
bottomPageShadow层:该层为在翻页过程中在 bottomPage左侧形成的一个阴影层。
leftPage层:该层为横屏模式下左侧页所在的层。
leftPageOverlay层:该层覆盖于为 leftPage层,效果与topPageOverlay类似。
由上可以看出,层树中的层主要分为三类:
内容显示层:topPage、topPageReverseImage、bottomPage、leftPage
阴影层:topPageShadow、topPageReverseShading、bottomPageShadow
覆盖层:topPageOverlay、topPageReverseOverlay、leftPageOverlay
图片缓存
Tow Brow在处理不同的内容(文本、图像、PDF)时显示时,所采取的方法是一样的。他将内容缓存为图像,并显示在屏幕上。基本方法是将内容写进CGContextRef中,然后根据CGContextRef中的信息创建图像,具体方法如下:
复制代码
-(CGImageRef) imageForPageIndex:(NSUInteger)pageIndex {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL,
pageSize.width,
pageSize.height,
8, /* bits per component*/
pageSize.width * 4, /* bytes per row */
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextClipToRect(context, CGRectMake(0, 0, pageSize.width, pageSize.height));
[dataSource renderPageAtIndex:pageIndex inContext:context];
CGImageRef image = CGBitmapContextCreateImage(context);
CGContextRelease(context);
[UIImage imageWithCGImage:image];
CGImageRelease(image);
return image;
}
当然程序没有缓存所有页的内容,而是根据横竖屏的不同缓存适当数量的内容。每次翻页时会重新整理缓存中的内容。
翻页动画实现
在Leaves中,翻页的基本原理其实很简单:翻页过程中,根据手指的划动来不断的调整层树结构中每个层的frame,翻页结束后,重新调整内容显示层所显示的内容。
为此,LevelsView中设置了一个leafEdge变量,该变量是手指在屏幕上划动时Touch Point在屏幕x轴上的百分比位置,这个操作在touchesMoved:withEvent中完成:
复制代码
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
......
UITouch *touch = [event.allTouches anyObject];
CGPoint touchPoint = [touch locationInView:self];
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:0.07]
forKey:kCATransactionAnimationDuration];
self.leafEdge = touchPoint.x / self.bounds.size.width;
[CATransaction commit];
}
而在leafEdge的set方法中,我们根据leafEdge的值来重新设定各个Layer的frame属性
复制代码
- (void) setLayerFrames {
CGRect rightPageBoundsRect = self.layer.bounds;
CGRect leftHalf, rightHalf;
CGRectDivide(rightPageBoundsRect, &leftHalf, &rightHalf, CGRectGetWidth(rightPageBoundsRect) / 2.0f, CGRectMinXEdge);
if (self.mode == LeavesViewModeFacingPages) {
rightPageBoundsRect = rightHalf;
}
topPage.frame = CGRectMake(rightPageBoundsRect.origin.x,
rightPageBoundsRect.origin.y,
leafEdge * rightPageBoundsRect.size.width,
rightPageBoundsRect.size.height);
topPageReverse.frame = CGRectMake(rightPageBoundsRect.origin.x + (2*leafEdge-1) * rightPageBoundsRect.size.width,
rightPageBoundsRect.origin.y,
(1-leafEdge) * rightPageBoundsRect.size.width,
rightPageBoundsRect.size.height);
......
}
最后便是当手指离开屏幕时,如何处理翻页结果(将当前页翻过去还是没有翻过去)。这个操作在 这个操作在touchesEnded:withEvent中完成
复制代码
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
......
UITouch *touch = [event.allTouches anyObject];
CGPoint touchPoint = [touch locationInView:self];
BOOL dragged = distance(touchPoint, touchBeganPoint) > [self dragThreshold];
[CATransaction begin];
float duration;
if ((dragged && self.leafEdge < 0.5) || (!dragged && [self touchedNextPage])) {
[self willTurnToPageAtIndex:currentPageIndex + numberOfVisiblePages];
self.leafEdge = 0;
duration = leafEdge;
......
}
else {
[self willTurnToPageAtIndex:currentPageIndex];
self.leafEdge = 1.0;
duration = 1 - leafEdge;
.......
}
[CATransaction setValue:[NSNumber numberWithFloat:duration]
forKey:kCATransactionAnimationDuration];
[CATransaction commit];
}
如果需要在翻页后执行某些操作(如在屏幕上显示当前页数等),则可以在继承自 LevelsViewController的控制器中实现leavesView:didTurnToPageAtIndex方法。
在此需要注意的就是 topPageReverseImage在竖屏时做了如下的变换
复制代码
topPageReverseImage.contentsGravity = kCAGravityRight;
topPageReverseImage.transform = CATransform3DMakeScale(-1, 1, 1);
从而使topPageReverseImage显示的内容让人感觉是透过纸张,看到topPage的内容。
横屏与竖屏
Leaves还有一个特点就是其支持横屏时,能同时看到两页的内容(该效果是由Ole Begemann改进的)。该改进最关键的地方就是增加了leftPage层,同时在横屏显示时将屏幕一分为二,在左侧显示leftPage。同进在翻页 的过程中,topPageReverseImage显示的是topPage页下一页的内容。在翻转屏幕时,会根据方向重新调整内容的显示。
参考资料:App Store-safe Page Curl animations
源码下载地址:https://github.com/brow/leaves