iOS之让你的App动起来

前言

本文只要描述了iOS中的Core Animation(核心动画:隐式动画、显示动画)、贝塞尔曲线、UIView动画的封装。

Core Animation,中文翻译为核心动画,它是一组非常强大的动画处理API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍。也就是说,使用少量的代码就可以实现非常强大的功能。Core Animation可以用在Mac OS X和iOS平台。Core Animation的动画执行过程都是在后台操作的,不会阻塞主线程。要注意是,Core Animation是直接作用在CALayer上的,并非UIView。

CALayer与UIView的关系:

在iOS中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是UIView。其实UIView之所以能显示在屏幕上,完全是因为它内部的一个图层:在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性可以访问这个层。

CALayer是个与UIView很类似的概念,同样有layer,sublayer...,同样有backgroundColor、frame等相似的属性,我们可以将UIView看做一种特殊的CALayer,只不过UIView可以响应事件而已。一般来说,layer可以有两种用途,二者不互相冲突:一是对view相关属性的设置,包括圆角、阴影、边框等参数,二是实现对view的动画操控。因此对一个view进行core animation动画,本质上是对该view的.layer进行动画操纵。

动画分为隐式动画和显示动画,我们先来看看

隐式动画

Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。

当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。

这看起来这太棒了,似乎不太真实,我们来用一个demo解释一下:首先和第一章“图层树”一样创建一个蓝色的方块,然后添加一个按钮,随机改变它的颜色。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变。

 随机改变图层颜色:

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView*layerView;

@property (nonatomic, weak) IBOutlet CALayer*colorLayer;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create sublayer

self.colorLayer =[CALayer layer];

self.colorLayer.frame= CGRectMake(50.0f,50.0f,100.0f,100.0f);

self.colorLayer.backgroundColor=[UIColor blueColor].CGColor;

//add it to our view

[self.layerView.layer addSublayer:self.colorLayer];

}

-(IBAction)changeColor

{//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.colorLayer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;                                                                                     

}

@end

运行效果图

这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持显式动画,下面详细说明。

但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。

事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。

事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。

任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。

Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

明白这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事的+setAnimationDuration:方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。

运行程序,你会发现色块颜色比之前变得更慢了。

使用CATransaction控制动画时间:

-(IBAction)changeColor

{//begin a new transaction

[CATransaction begin];

//set the animation duration to 1 second

[CATransaction setAnimationDuration:1.0];

//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.colorLayer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

//commit the transaction

[CATransaction commit];

}

如果你用过UIView的动画方法做过一些动画效果,那么应该对这个模式不陌生。UIView有两个方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法类似。实际上在+beginAnimations:context:和+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。

在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。

CATransaction的+begin和+commit方法在+animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin和+commit匹配的失误造成的风险。

完成块

基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。我们来调整上个例子,在颜色变化结束之后执行一些操作。我们来添加一个完成之后的block,用来在每次颜色变化结束之后切换到另一个旋转90的动画。

在颜色动画完成之后添加一个回调:

-(IBAction)changeColor

{//begin a new transaction

[CATransaction begin];

//set the animation duration to 1 second

[CATransaction setAnimationDuration:1.0];

//add the spin animation on completion

[CATransaction setCompletionBlock:^{

//rotate the layer 90 degrees

CGAffineTransform transform =self.colorLayer.affineTransform;

transform=CGAffineTransformRotate(transform, M_PI_4);

self.colorLayer.affineTransform=transform;

}];

//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.colorLayer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

//commit the transaction

[CATransaction commit];

运行效果图

注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。

图层行为

现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,并且直接设置layerView关联图层的背景色。

清单7.4 直接设置图层的属性

@interfaceViewController ()

@property (nonatomic, weak) IBOutlet UIView*layerView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//set the color of our layerView backing layer directly

self.layerView.layer.backgroundColor =[UIColor blueColor].CGColor;

}

-(IBAction)changeColor

{//begin a new transaction

[CATransaction begin];

//set the animation duration to 1 second

[CATransaction setAnimationDuration:1.0];

//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.layerView.layer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

//commit the transaction

[CATransaction commit];

}

运行程序,你会发现当按下按钮,图层颜色瞬间切换到新的值,而不是之前平滑过渡的动画。发生了什么呢?隐式动画好像被UIView关联图层给禁用了。

试想一下,如果UIView的属性都有动画特性的话,那么无论在什么时候修改它,我们都应该能注意到的。所以,如果说UIKit建立在Core Animation(默认对所有东西都做动画)之上,那么隐式动画是如何被UIKit禁用掉呢?

我们知道Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。

我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:

图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。

如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验:

测试UIView的actionForLayer:forKey:实现:

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView*layerView;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//test layer action when outside of animation block

NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);

//begin animation block

[UIView beginAnimations:nil context:nil];

//test layer action when inside of animation block

NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);

//end animation block

[UIView commitAnimations];

}

@end

运行程序,控制台显示结果如下:

$ LayerTest[21215:c07] Outside: $ LayerTest[21215:c07] Inside:

于是我们可以预言,当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation。

当然返回nil并不是禁用隐式动画唯一的办法,CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:

[CATransaction setDisableActions:YES];

总结一下,我们知道了如下几点

UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画。

对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。

我们来对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。我们也可以使用委托来实现,但是actions字典可以写更少的代码。那么到底改如何创建一个合适的行为对象呢?

行为通常是一个被Core Animation隐式调用的显式动画对象。这里我们使用的是一个实现了CATransaction的实例,叫做推进过渡。不过对于现在,知道CATransition响应CAAction协议,并且可以当做一个图层行为就足够了。结果很赞,不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。

实现自定义行为

@interfaceViewController ()

@property (nonatomic, weak) IBOutlet UIView*layerView;

@property (nonatomic, weak) IBOutlet CALayer*colorLayer;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create sublayer

self.colorLayer =[CALayer layer];

self.colorLayer.frame= CGRectMake(50.0f,50.0f,100.0f,100.0f);

self.colorLayer.backgroundColor=[UIColor blueColor].CGColor;//add a custom actionCATransition *transition =[CATransition animation];

transition.type=kCATransitionPush;

transition.subtype=kCATransitionFromLeft;

self.colorLayer.actions= @{@"backgroundColor": transition};

//add it to our view

[self.layerView.layer addSublayer:self.colorLayer];

}

-(IBAction)changeColor

{//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.colorLayer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

}

@end

运行效果图

呈现与模型

CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢?

当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。

当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。

我们讨论的就是一个典型的微型MVC模式。CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。

在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必

须要知道当前显示在屏幕上的属性值的记录。

每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值,如下图。

我们在第一章中提到除了图层树,另外还有呈现树。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。

你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。

一个移动的图层是如何通过数据模型呈现的


大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。

如果你在实现一个基于定时器的动画,而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。

如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

我们可以用一个简单的案例来证明后者。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过对呈现图层调用-hitTest:来判断是否被点击。

如果修改代码让-hitTest:直接作用于colorLayer而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)。

使用presentationLayer图层来判断当前图层位置

@interfaceViewController ()

@property (nonatomic, strong) CALayer*colorLayer;

@end

@implementation ViewController

- (void)viewDidLoad

{

[super viewDidLoad];

//create a red layer

self.colorLayer =[CALayer layer];

self.colorLayer.frame= CGRectMake(0,0,100,100);

self.colorLayer.position= CGPointMake(self.view.bounds.size.width /2, self.view.bounds.size.height /2);

self.colorLayer.backgroundColor=[UIColor redColor].CGColor;

[self.view.layer addSublayer:self.colorLayer];

}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

//get the touch point

CGPoint point =[[touches anyObject] locationInView:self.view];

//check if we‘ve tapped the moving layer

if([self.colorLayer.presentationLayer hitTest:point]) {

//randomize the layer background color

CGFloat red = arc4random() /(CGFloat)INT_MAX;

CGFloat green= arc4random() /(CGFloat)INT_MAX;

CGFloat blue= arc4random() /(CGFloat)INT_MAX;

self.colorLayer.backgroundColor= [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

}else{

//otherwise (slowly) move the layer to new position

[CATransaction begin];

[CATransaction setAnimationDuration:4.0];

self.colorLayer.position=point;

[CATransaction commit];

}

}

总结

这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何通过它们来判断出图层当前位置以及将要到达的位置。

显示动画

上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机

制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着

任意一条曲线移动。

属性动画

首先我们来探讨一下属性动画。属性动画作用于图层的某个单一属性,并指定了它的一个目标值,或者一连串将要做动画的值。属性动画分为两种:基础和关键帧。

基础动画

动画其实就是一段时间内发生的改变,最简单的形式就是从一个值改变到另一个值,这也是CABasicAnimation最主要的功能。

CABasicAnimation是CAPropertyAnimation的一个子类,CAPropertyAnimation同时也是Core Animation所有动画类型的抽象基类。作为一个抽象类,CAAnimation本身并没有做多少工作,它提供了一个计时函数,

一个委托(用于反馈动画状态)以及一个removedOnCompletion,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。

CAAnimation同时实现了一些协议,包括CAAction(允许CAAnimation的子类可以提供图层行为),以及

CAMediaTiming。CAPropertyAnimation通过指定动画的keyPath作用于一个单一属性,CAAnimation通常应用于一个指定的CALayer,于是这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法可以在层级关系中指向任意嵌套的对象),而不仅仅是一个属性的名称,因为这意味着动画不仅可以作用于图层本身的属性,而且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。

CAPropertyAnimation通过指定动画的keyPath作用于一个单一属性,CAAnimation通常应用于一个指定的CALayer,于是这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法可以在层级关系中指向任意嵌套的对象),而不仅仅是一个属性的名称,因为这意味着动画不仅可以作用于图层本身的属性,而且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。

CABasicAnimation继承于CAPropertyAnimation,并添加了如下属性:

id fromValue

id toValue

id byValue

从命名就可以得到很好的解释:fromValue代表了动画开始之前属性的值,toValue代表了动画结束之后的值,byValue代表了动画执行过程中改变的值。

通过组合这三个属性就可以有很多种方式来指定一个动画的过程。它们被定义成id类型而不是一些具体的类型是因为属性动画可以用作很多不同种的属性类型,包括数字类型,矢量,变换矩阵,甚至是颜色或者图片。

形象的说,在iOS中,展示动画可以类比于显示生活中的“拍电影”。拍电影有三大要素:演员+剧本+开拍,概念类比如下:

演员--->CALayer,规定电影的主角是谁

剧本--->CAAnimation,规定电影该怎么演,怎么走,怎么变换

开拍--->AddAnimation,开始执行

CAAnimation是什么呢?

CAAnimation可分为四种:

1.CABasicAnimation

通过设定起始点,终点,时间,动画会沿着你这设定点进行移动。可以看做特殊的CAKeyFrameAnimation

2.CAKeyframeAnimation

Keyframe顾名思义就是关键点的frame,你可以通过设定CALayer的始点、中间关键点、终点的frame,时间,动画会沿你设定的轨迹进行移动

3.CAAnimationGroup

Group也就是组合的意思,就是把对这个Layer的所有动画都组合起来。PS:一个layer设定了很多动画,他们都会同时执行,如何按顺序执行我到时候再讲。

4.CATransition

这个就是苹果帮开发者封装好的一些动画

二、动手干活

实践出真知,看个例子就知道:

比如我们想实现一个类似心跳的缩放动画可以这么做,分为演员初始化、设定剧本、电影开拍三个步骤:


效果请参考附图中的蓝色方块。其他效果可以依葫芦画瓢轻松实现。想要实现不同的效果,最关键的地方在于CABasicAnimation对象的初始化方式中keyPath的设定。在iOS中有以下几种不同的keyPath,代表着不同的效果:

此外,我们还可以利用GroupAnimation实现多种动画的组合,在GroupAnimation中的各个动画类型是同时进行的。

最后运行效果:

贝塞尔曲线:

使用UIBezierPath类可以创建基于矢量的路径,这个类在UIKit中。此类是Core Graphics框架关于path的一个封装。使用此类可以定义简单的形状,如椭圆或者矩形,或者有多个直线和曲线段组成的形状。

1.Bezier Path 基础

UIBezierPath对象是CGPathRef数据类型的封装。path如果是基于矢量形状的,都用直线和曲线段去创建。我们使用直线段去创建矩形和多边形,使用曲线段去创建弧(arc),圆或者其他复杂的曲线形状。每一段都包括一个或者多个点,绘图命令定义如何去诠释这些点。每一个直线段或者曲线段的结束的地方是下一个的开始的地方。每一个连接的直线或者曲线段的集合成为subpath。一个UIBezierPath对象定义一个完整的路径包括一个或者多个subpaths。

创建和使用一个path对象的过程是分开的。创建path是第一步,包含一下步骤:

(1)创建一个Bezier path对象。

(2)使用方法moveToPoint:去设置初始线段的起点。

(3)添加line或者curve去定义一个或者多个subpaths。

(4)改变UIBezierPath对象跟绘图相关的属性。

例如,我们可以设置stroked path的属性lineWidth和lineJoinStyle。也可以设置filled path的属性usesEvenOddFillRule。

当创建path,我们应该管理path上面的点相对于原点(0,0),这样我们在随后就可以很容易的移动path了。为了绘

制path对象,我们要用到stroke和fill方法。这些方法在current graphic

context下渲染path的line和curve段。

2、使用UIBezierPath创建多边形---在path下面添加直线条形成多边形

多边形是一些简单的形状,这些形状是由一些直线线条组成,我们可以用moveToPoint: 和 addLineToPoint:方法去构建。

方法moveToPoint:设置我们想要创建形状的起点。从这点开始,我们可以用方法addLineToPoint:去创建一个形状的线段。

我们可以连续的创建line,每一个line的起点都是先前的终点,终点就是指定的点。

下面的代码描述了如何用线段去创建一个五边形。第五条线通过调用closePath方法得到的,它连接了最后一个点(0,40)和第一个点(100,0)

说明:closePath方法不仅结束一个shape的subpath表述,它也在最后一个点和第一个点之间画一条线段,如果我们画多边形的话,这个一个便利的方法我们不需要去画最后一条线。

// Only override drawRect: if you perform custom drawing.

// An empty implementation adversely affects performance during animation.

- (void)drawRect:(CGRect)rect

{

UIColor *color = [UIColor redColor];

[color set];//设置线条颜色

UIBezierPath* aPath = [UIBezierPath bezierPath];

aPath.lineWidth = 5.0;

aPath.lineCapStyle = kCGLineCapRound;//线条拐角

aPath.lineJoinStyle = kCGLineCapRound;//终点处理

// Set the starting point of the shape.

[aPath moveToPoint:CGPointMake(100.0, 0.0)];

// Draw the lines

[aPath addLineToPoint:CGPointMake(200.0, 40.0)];

[aPath addLineToPoint:CGPointMake(160, 140)];

[aPath addLineToPoint:CGPointMake(40.0, 140)];

[aPath addLineToPoint:CGPointMake(0.0, 40.0)];

[aPath closePath];//第五条线通过调用closePath方法得到的

[aPath stroke];//Draws line 根据坐标点连线

}

注:这个类要继承自UIView。

运行的结果如下图:

如果修改最后一句代码:[aPathfill];

运行结果就如下:

这样就知道stroke  和  fill  方法的区别了吧!

3、使用UIBezierPath创建矩形

使用这个方法即可:

Creates and returns anewUIBezierPath object initialized with a rectangular path.

+ (UIBezierPath *)bezierPathWithRect:(CGRect)rect

demo代码:

- (void)drawRect:(CGRect)rect

{

UIColor *color = [UIColor redColor];

[color set];//设置线条颜色

UIBezierPath* aPath = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 50)];

aPath.lineWidth = 5.0;

aPath.lineCapStyle = kCGLineCapRound;//线条拐角

aPath.lineJoinStyle = kCGLineCapRound;//终点处理

[aPath stroke];

}

4、使用UIBezierPath创建圆形或者椭圆形

使用这个方法即可:

Creates and returns anewUIBezierPath object initialized with an oval path inscribed in the specified rectangle

+ (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect

这个方法根据传入的rect矩形参数绘制一个内切曲线。

当传入的rect是一个正方形时,绘制的图像是一个内切圆;当传入的rect是一个长方形时,绘制的图像是一个内切椭圆。

5、使用UIBezierPath创建一段弧线

使用这个方法:

Creates and returns anewUIBezierPath object initialized with an arc of a circle.

+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise

Parameters

center

Specifies the center point of the circle (in the current coordinate system) used to define the arc.

radius

Specifies the radius of the circle used to define the arc.

startAngle

Specifies the starting angle of the arc (measured in radians).

endAngle

Specifies the end angle of the arc (measured in radians).

clockwise

The direction in which to draw the arc.

Return Value

Anewpath object with the specified arc.

其中的参数分别指定:这段圆弧的中心,半径,开始角度,结束角度,是否顺时针方向。

下图为弧线的参考系。

demo代码:

#define pi 3.14159265359

#define   DEGREES_TO_RADIANS(degrees)  ((pi * degrees)/ 180)

- (void)drawRect:(CGRect)rect

{

UIColor *color = [UIColor redColor];

[color set];//设置线条颜色

UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150)

radius:75

startAngle:0

endAngle:DEGREES_TO_RADIANS(135)

clockwise:YES];

aPath.lineWidth = 5.0;

aPath.lineCapStyle = kCGLineCapRound;//线条拐角

aPath.lineJoinStyle = kCGLineCapRound;//终点处理

[aPath stroke];

}

结果如下图:

6、UIBezierPath类提供了添加二次贝塞尔曲线和三次贝塞尔曲线的支持。

曲线段在当前点开始,在指定的点结束。曲线的形状有开始点,结束点,一个或者多个控制点的切线定义。下图显示了两种曲线类型的相似,以及控制点和curve形状的关系。

(1)绘制二次贝塞尔曲线

使用到这个方法:

Appends a quadratic Bézier curve to the receiver’s path.

- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint

Parameters

endPoint

The end point of the curve.

controlPoint

The control point of the curve.

demo代码:

- (void)drawRect:(CGRect)rect

{

UIColor *color = [UIColor redColor];

[color set];//设置线条颜色

UIBezierPath* aPath = [UIBezierPath bezierPath];

aPath.lineWidth = 5.0;

aPath.lineCapStyle = kCGLineCapRound;//线条拐角

aPath.lineJoinStyle = kCGLineCapRound;//终点处理

[aPath moveToPoint:CGPointMake(20, 100)];

[aPath addQuadCurveToPoint:CGPointMake(120, 100) controlPoint:CGPointMake(70, 0)];

[aPath stroke];

}

(2)绘制三次贝塞尔曲线

使用到这个方法:

Appends a cubic Bézier curve to the receiver’s path.

- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2

Parameters

endPoint

The end point of the curve.

controlPoint1

The first control point to use when computing the curve.

controlPoint2

The second control point to use when computing the curve.

demo代码:

- (void)drawRect:(CGRect)rect

{

UIColor *color = [UIColor redColor];

[color set];//设置线条颜色

UIBezierPath* aPath = [UIBezierPath bezierPath];

aPath.lineWidth = 5.0;

aPath.lineCapStyle = kCGLineCapRound;//线条拐角

aPath.lineJoinStyle = kCGLineCapRound;//终点处理

[aPath moveToPoint:CGPointMake(20, 50)];

[aPath addCurveToPoint:CGPointMake(200, 50) controlPoint1:CGPointMake(110, 0) controlPoint2:CGPointMake(110, 100)];

[aPath stroke];

}

7.使用Core Graphics函数去修改path。

UIBezierPath类只是CGPathRef数据类型和path绘图属性的一个封装。虽然通常我们可以用

UIBezierPath类的方法去添加直线段和曲线段,UIBezierPath类还提供了一个属性CGPath,我们可以用来直接修改底层的path

data type。如果我们希望用Core Graphics 框架函数去创建path,则我们要用到此属性。

有两种方法可以用来修改和UIBezierPath对象相关的path。可以完全的使用Core

Graphics函数去修改path,也可以使用Core

Graphics函数和UIBezierPath函数混合去修改。第一种方法在某些方面相对来说比较容易。我们可以创建一个CGPathRef数据类型,

并调用我们需要修改path信息的函数。

下面的代码就是赋值一个新的CGPathRef给UIBezierPath对象。

// Create the path data

CGMutablePathRef cgPath = CGPathCreateMutable();

CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(0, 0, 300, 300));

CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(50, 50, 200, 200));

// Now create the UIBezierPath object

UIBezierPath* aPath = [UIBezierPath bezierPath];

aPath.CGPath = cgPath;

aPath.usesEvenOddFillRule = YES;

// After assigning it to the UIBezierPath object, you can release

// your CGPathRef data type safely.

CGPathRelease(cgPath);

如果我们使用Core Graphics函数和UIBezierPath函数混合方法,我们必须小心的移动path 信息在两者之间。因为UIBezierPath类拥有自己底层的CGPathRef data type,我们不能简单的检索该类型并直接的修改它。相反,我们应该生成一个副本,然后修改此副本,然后赋值此副本给CGPath属性,如下代码:

Mixing Core Graphics andUIBezierPathcalls

UIBezierPath*    aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 300, 300)];

// Get the CGPathRef and create a mutable version.

CGPathRef cgPath = aPath.CGPath;

CGMutablePathRef  mutablePath = CGPathCreateMutableCopy(cgPath);

// Modify the path and assign it back to the UIBezierPath object

CGPathAddEllipseInRect(mutablePath, NULL, CGRectMake(50, 50, 200, 200));

aPath.CGPath = mutablePath;

// Release both the mutable copy of the path.

CGPathRelease(mutablePath);

8.rendering(渲染)Bezier Path对象的内容。

当创建一个UIBezierPath对象之后,我们可以使用它的stroke和fill方法在current graphics context中去渲染它。在调用这些方法之前,我们要进行一些其他的任务去确保正确的绘制path。

使用UIColor类的方法去stroke和fill想要的颜色。

设置形状在目标视图中的位置。如果我们创建的path相对于原点(0,0),则我们可以给current drawing

context应用一个适当的affie

transform。例如,我想drawing一个形状起始点在(0,0),我可以调用函数CGContextTranslateCTM,并指定水平和垂

直方向的translation值为10。调整graphic

context相对于调整path对象的points是首选的方法,因为我们可以很容易的保存和撤销先前的graphics state。

更新path对象的drawing 属性。当渲染path时,UIBezierPath实例的drawing属性会覆盖graphics context下的属性值。

下面的代码实现了在一个自定义view中实现drawRect:方法中去绘制一个椭圆。椭圆边框矩形的左上角位于视图坐标系统的点(50,50)处。

Drawing a path in a view

- (void)drawRect:(CGRect)rect

{

// Create an oval shape to draw.

UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:

CGRectMake(0, 0, 200, 100)];

// Set the render colors

[[UIColor blackColor] setStroke];

[[UIColor redColor] setFill];

CGContextRef aRef = UIGraphicsGetCurrentContext();

// If you have content to draw after the shape,

// save the current state before changing the transform

//CGContextSaveGState(aRef);

// Adjust the view's origin temporarily. The oval is

// now drawn relative to the new origin point.

CGContextTranslateCTM(aRef, 50, 50);

// Adjust the drawing options as needed.

aPath.lineWidth = 5;

// Fill the path before stroking it so that the fill

// color does not obscure the stroked line.

[aPath fill];

[aPath stroke];

// Restore the graphics state before drawing any other content.

//CGContextRestoreGState(aRef);

}

UIView封装动画:




1.简单说明

UIKit直接将动画集成到UIView类中,当内部的一些属性发生改变时,UIView将为这些改变提供动画支持

执行动画所需要的工作由UIView类自动完成,但仍要在希望执行动画时通知视图,为此需要将改变属性的代码放在[UIViewbeginAnimations:nilcontext:nil]和[UIViewcommitAnimations]之间

常见方法解析:

+ (void)setAnimationDelegate:(id)delegate     设置动画代理对象,当动画开始或者结束时会发消息给代理对象

+ (void)setAnimationWillStartSelector:(SEL)selector   当动画即将开始时,执行delegate对象的selector,并且把beginAnimations:context:中传入的参数传进selector

+ (void)setAnimationDidStopSelector:(SEL)selector  当动画结束时,执行delegate对象的selector,并且把beginAnimations:context:中传入的参数传进selector

+ (void)setAnimationDuration:(NSTimeInterval)duration   动画的持续时间,秒为单位

+ (void)setAnimationDelay:(NSTimeInterval)delay  动画延迟delay秒后再开始

+ (void)setAnimationStartDate:(NSDate *)startDate   动画的开始时间,默认为now

+ (void)setAnimationCurve:(UIViewAnimationCurve)curve  动画的节奏控制

+ (void)setAnimationRepeatCount:(float)repeatCount  动画的重复次数

+ (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses  如果设置为YES,代表动画每次重复执行的效果会跟上一次相反

+ (void)setAnimationTransition:(UIViewAnimationTransition)transitionforView:(UIView *)viewcache:(BOOL)cache  设置视图view的过渡效果, transition指定过渡类型, cache设置YES代表使用视图缓存,性能较好

2.代码示例:

执行结果:

打印动画块的位置:

3.UIView封装的动画与CALayer动画的对比

使用UIView和CALayer都能实现动画效果,但是在真实的开发中,一般还是主要使用UIView封装的动画,而很少使用CALayer的动画。

CALayer核心动画与UIView动画的区别:

UIView封装的动画执行完毕之后不会反弹。即如果是通过CALayer核心动画改变layer的位置状态,表面上看虽然已经改变了,但是实际上它的位置是没有改变的。

代码示例:

打印结果:

二、block动画

1.简单说明

+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion

参数解析:

duration:动画的持续时间

delay:动画延迟delay秒后开始

options:动画的节奏控制

animations:将改变视图属性的代码放在这个block中

completion:动画结束后,会自动调用这个block

转场动画

+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion

参数解析:

duration:动画的持续时间

view:需要进行转场动画的视图

options:转场动画的类型

animations:将改变视图属性的代码放在这个block中

completion:动画结束后,会自动调用这个block

+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toViewduration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion

方法调用完毕后,相当于执行了下面两句代码:

// 添加toView到父视图

[fromView.superview addSubview:toView];

// 把fromView从父视图中移除

[fromView removeFromSuperview];

参数解析:

duration:动画的持续时间

options:转场动画的类型

completion:动画结束后,会自动调用这个block

2.代码示例

打印结果:

提示:self.customView.layer.position和self.customView.center等价,因为position的默认值为(0.5,0.5)。

三、补充

1.UIImageView的帧动画

UIImageView可以让一系列的图片在特定的时间内按顺序显示

相关属性解析:

animationImages:要显示的图片(一个装着UIImage的NSArray)

animationDuration:完整地显示一次animationImages中的所有图片所需的时间

animationRepeatCount:动画的执行次数(默认为0,代表无限循环)

相关方法解析:

- (void)startAnimating; 开始动画

- (void)stopAnimating;  停止动画

- (BOOL)isAnimating;  是否正在运行动画

2.UIActivityIndicatorView

是一个旋转进度轮,可以用来告知用户有一个操作正在进行中,一般用initWithActivityIndicatorStyle初始化

方法解析:

- (void)startAnimating; 开始动画

- (void)stopAnimating;  停止动画

- (BOOL)isAnimating;  是否正在运行动画

UIActivityIndicatorViewStyle有3个值可供选择:

UIActivityIndicatorViewStyleWhiteLarge   //大型白色指示器

UIActivityIndicatorViewStyleWhite      //标准尺寸白色指示器

UIActivityIndicatorViewStyleGray    //灰色指示器,用于白色背景

封装转场动画:

.h代码:

#import

#import

@interface ZciotAnimation : NSObject

+ (void)ZciotAnimation:(int)type ForView:(UIView *)view Subtype:(int)subtype BackgroundImage:(NSString *)image;

@end

.m代码:

#import "ZciotAnimation.h"

#define DURATION 0.7f

typedef enum : NSUInteger {

Fade = 1,                  //淡入淡出

Push,                      //推挤

Reveal,                    //揭开

MoveIn,                    //覆盖

Cube,                      //立方体

SuckEffect,                //吮吸

OglFlip,                    //翻转

RippleEffect,              //波纹

PageCurl,                  //翻页

PageUnCurl,                //反翻页

CameraIrisHollowOpen,      //开镜头

CameraIrisHollowClose,      //关镜头

CurlDown,                  //下翻页

CurlUp,                    //上翻页

FlipFromLeft,              //左翻转

FlipFromRight,              //右翻转

} AnimationType;

@implementation ZciotAnimation

+ (void)ZciotAnimation:(int)type ForView:(UIView *)view Subtype:(int)subtype BackgroundImage:(NSString *)image

{

AnimationType animationType = type;

NSString *subtypeString;

switch (subtype) {

case 0:

subtypeString = kCATransitionFromLeft;

break;

case 1:

subtypeString = kCATransitionFromBottom;

break;

case 2:

subtypeString = kCATransitionFromRight;

break;

case 3:

subtypeString = kCATransitionFromTop;

break;

default:

break;

}

//    _subtype += 1;

//    if (_subtype > 3) {

//        _subtype = 0;

//    }

switch (animationType) {

case Fade:

[self transitionWithType:kCATransitionFade WithSubtype:subtypeString ForView:view];

break;

case Push:

[self transitionWithType:kCATransitionPush WithSubtype:subtypeString ForView:view];

break;

case Reveal:

[self transitionWithType:kCATransitionReveal WithSubtype:subtypeString ForView:view];

break;

case MoveIn:

[self transitionWithType:kCATransitionMoveIn WithSubtype:subtypeString ForView:view];

break;

case Cube:

[self transitionWithType:@"cube" WithSubtype:subtypeString ForView:view];

break;

case SuckEffect:

[self transitionWithType:@"suckEffect" WithSubtype:subtypeString ForView:view];

break;

case OglFlip:

[self transitionWithType:@"oglFlip" WithSubtype:subtypeString ForView:view];

break;

case RippleEffect:

[self transitionWithType:@"rippleEffect" WithSubtype:subtypeString ForView:view];

break;

case PageCurl:

[self transitionWithType:@"pageCurl" WithSubtype:subtypeString ForView:view];

break;

case PageUnCurl:

[self transitionWithType:@"pageUnCurl" WithSubtype:subtypeString ForView:view];

break;

case CameraIrisHollowOpen:

[self transitionWithType:@"cameraIrisHollowOpen" WithSubtype:subtypeString ForView:view];

break;

case CameraIrisHollowClose:

[self transitionWithType:@"cameraIrisHollowClose" WithSubtype:subtypeString ForView:view];

break;

case CurlDown:

[self animationWithView:view WithAnimationTransition:UIViewAnimationTransitionCurlDown];

break;

case CurlUp:

[self animationWithView:view WithAnimationTransition:UIViewAnimationTransitionCurlUp];

break;

case FlipFromLeft:

[self animationWithView:view WithAnimationTransition:UIViewAnimationTransitionFlipFromLeft];

break;

case FlipFromRight:

[self animationWithView:view WithAnimationTransition:UIViewAnimationTransitionFlipFromRight];

break;

default:

break;

}

[self addBgImageWithImageName:image BackgroundView:view];

}

#pragma CATransition动画实现

+ (void)transitionWithType:(NSString *)type WithSubtype:(NSString *)subtype ForView:(UIView *)view

{

//创建CATransition对象

CATransition *animation = [CATransition animation];

//设置运动时间

animation.duration = DURATION;

//设置运动type

animation.type = type;

if (subtype != nil) {

//设置子类

animation.subtype = subtype;

}

//设置运动速度

animation.timingFunction = UIViewAnimationOptionCurveEaseInOut;

[view.layer addAnimation:animation forKey:@"animation"];

}

#pragma UIView实现动画

+ (void) animationWithView : (UIView *)view WithAnimationTransition : (UIViewAnimationTransition) transition

{

[UIView animateWithDuration:DURATION animations:^{

[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];

[UIView setAnimationTransition:transition forView:view cache:YES];

}];

}

#pragma 给View添加背景图

+ (void)addBgImageWithImageName:(NSString *)imageName BackgroundView:(UIView *)view

{

view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:imageName]];

}

@end

封装好了之后,你在需要使用动画的地方调用api就行了:

部分动画效果图:

总结:

到这了就基本上介绍完了iOS动画部分,有空我会将封装的demo传到github上。

参考优秀博客:

http://www.cnblogs.com/wendingding/p/3802830.html

http://www.mamicode.com/info-detail-500488.html

http://www.cnblogs.com/moyunmo/p/3600091.html?utm_source=tuicool&utm_medium=referral

http://www.jianshu.com/p/8c1c1697c0ce

http://www.cnblogs.com/wengzilin/p/4250957.html

http://www.cocoachina.com/ios/20141226/10775.html

博主的微博、CocoaChina博客、CSDN博客同步更新,欢迎关注:

新浪微博:http://weibo.com/p/1005052308506177/home?from=page_100505_profile&wvr=6&mod=data&is_all=1#place

CocoaChina:http://blog.cocoachina.com/477998

CSDN:http://blog.csdn.net/czkyes


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容