Core Animation(上)─── 图层

一. 视图和图层的关系

Core Animation不仅仅是我们认识的“核心动画”,它是一个复合引擎,它的职责是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在图层树的体系中。

我们可能对视图的概念比较熟悉,一个视图就是在屏幕上显示的一个矩形块(UIView,UIButton等)。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。在iOS中,所有的视图都是继承UIView而来的,它可以处理触摸手势,支持基于Core Graphics绘图,可以做仿射变换(例如旋转、缩放等),简单的动画。
图层(CALayer)也是一个被层级关系树管理的矩形块,也可以包含图片、文字等内容,管理子视图的位置,它们也可以做动画和变换。和UIView最大的不同是,CALayer不处理用户的交互。

那CALayer和UIView有什么关系呢?
每一个UIView都有一个CALayer实例的图层属性,视图的职责就是创建并管理这个图层,以确保子视图在层级关系中添加或被移除时,他们关联的图层同样在层级关系树中有相同的操作。UIView本质就是对图层的封装,提供处理用户交互的功能,以及动画的高级接口。实际上,这些UIView背后关联的图层才是真正用来在屏幕上显示和做动画的。
一个视图只有一个相关联的图层(系统自动创建),同时支持添加其他子图层,当然这在开发中似乎没什么意义。如果不是一些特殊的情况(比如使用CALayer的子类,或对性能有高要求),我们都会直接使用图层关联的视图,因为你不仅能使用CALayer的底层特性,也可以使用UIView的高级API(自动排版,布局,事件处理)。

//创建一个宽高100的图层添加到self.view的图层中
- (void)createSimpleLayer{
    CALayer *blueLayer = [[CALayer alloc] init];
    blueLayer.frame = CGRectMake(50, 50, 100, 100);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    
    [self.view.layer addSublayer:blueLayer];
}

图层不能像视图那样处理触摸事件,但是它的很多功能是视图不具备的:
① 阴影,圆角,边框的颜色和宽度
② 3D变换
③ 非矩形范围
④ 透明遮罩
⑤ 复杂动画

二. 图层的认识和应用

CALayer需要认识的属性很多,我们分成三部分来介绍:
① 寄宿图(图层内部)
② 显示效果(图层外表)
③ 变换(位置相关)

① CALayer的寄宿图

CALayer不仅可以有背景色,也可以包含一张图片,而这张图层中的图片就是CALayer的寄宿图。
这边我们先了解CALayer的几个属性:

  • contents
    该类型定义为id是因为对CGImage和NSImage的值都起作用,事实上你要赋值的类型是CGImageRef,一个指向CGImage结构体的指针。

  • contentsGravity
    决定伸缩样式,这是和UIView中contentMode对应的属性,只是它是NSString类型(kCAGravity开头),不是枚举。

  • contentsScale
    定义寄宿图的像素尺寸和视图大小的比例,默认1.0。不过很多时候都被contentsGravity的样式限制了。

  • masksToBounds
    决定是否显示超出的内容,这是和UIView中clipsToBounds对应的属性。

  • contentsRect
    设置图层中显示寄宿图的子域,通俗讲就是要显示寄宿图的哪一部分,默认值是{0, 0, 1, 1},即全部显示,要显示左上部分则设为{0, 0, 0.5, 0.5}

  • contentsCenter
    定义一个固定边框和一个在图层上可拉伸的区域,默认值是{0, 0, 1, 1},即均匀拉伸,效果和UIImage 的resizableImageWithCapInsets:是一样的,还可以在可视化界面直接设置

以下通过例子来认识下寄宿图的各个属性:

- (void)createImageLayer{
    CALayer *imageLayer = [[CALayer alloc] init];
    imageLayer.frame = CGRectMake(50, 50, 200, 200);
    imageLayer.backgroundColor = [UIColor blueColor].CGColor;
    
    //设置寄宿图
    UIImage *image = [UIImage imageNamed:@"person"];
    imageLayer.contents = (__bridge id)image.CGImage;
    //设置伸缩方式
    imageLayer.contentsGravity = kCAGravityResizeAspectFill;
    //为防止在Retina设备显示不正常,要这样设置contentsScale
    imageLayer.contentsScale = [UIScreen mainScreen].scale;
    //裁剪超出部分
    imageLayer.masksToBounds = YES;
    //显示图片的左上部分
    imageLayer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
    //设置图片拉伸区域
    imageLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.5, 0.5);
    
    [self.view.layer addSublayer:imageLayer];
}
contensCenter的例子.png
设置寄宿图除了给CALayer的contens赋值,也可以用UIView的drawRect:方法自定义绘制。

drawRect:方法没有默认的实现,因为寄宿图不是必须的,如果UIView检测到drawRect:方法被调用了,它会为视图分配一个像素尺度等于视图大小乘以contentsScale的寄宿图。如果不需要寄宿图,就不要创建这个方法(即使是空的),也会造成CPU资源和内存的浪费。(会创建一个图形上下文,该上下文内存公式:图层宽 x 图层高 x 4 字节)
drawRect:方法的本质也是底层的layer安排重绘的工作和保存因此产生的图片。获取的上下文就是底层layer的上下文,在渲染的时候,就是把图形渲染到对应的layer上。在执行渲染操作时,本质上它的内部相当于执行的 [self.layer drawInContext:ctx]。

#import "TestView.h"

@implementation TestView

- (void)drawRect:(CGRect)rect {
    // 获取图形上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    CGContextSetLineWidth(ctx, 10.0f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    //渲染时本质是调用[self.layer drawInContext:ctx]
    CGContextStrokeEllipseInRect(ctx, self.bounds);
}
@end
当创建一个单独的CALayer时,也可以通过设置CALayer的delegate,实现 CALayerDelegate 协议来绘图。

当需要被重绘时,CALayer 请它的代理给它寄宿图来显示。首先会调用displayLayer:来直接设置contens,如果没实现该方法就会去调用drawLayer:inContext:进行绘图(实现了displayLayer:就不会实现drawLayer:inContext:)。它会为CALayer分配一个尺寸合适的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics绘制的上下文环境,为绘制寄宿图做准备。
实际上,UIView中的layer的delegate是设置为它自己。当UIView需要重绘时,会调用drawLayer:inContext:方法。而UIView在drawLayer:inContext:方法中又会调用自己的drawRect:方法。在drawRect:中完成的所有绘图都会填入layer的CGContextRef中,然后被拷贝至屏幕。

#import "ViewController.h"
#import "TestView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self drawContent];
}

- (void)drawContent{
    CALayer *contentlayer = [[CALayer alloc] init];
    contentlayer.frame = CGRectMake(50, 50, 100, 100);
    contentlayer.backgroundColor = [UIColor blueColor].CGColor;
    [self.view.layer addSublayer:contentlayer];
    
    //设置代理(CALayerDelegate包含在NSObject中,所以不用显式写出来)
    contentlayer.delegate = self;
    //需要显式设置display(或setNeedDisplay)才会去调用CALayerDelegate代理方法
    [contentlayer display];
}

#pragma mark - <CALayerDelegate>
//用来设置layer的contens
- (void)displayLayer:(CALayer *)layer{
    UIImage *image = [UIImage imageNamed:@"person"];
    layer.contents = (__bridge id)image.CGImage;

}
//用来绘图
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    CGContextSetLineWidth(ctx, 10.0f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
@end

实际上,无论是CALayer还是UIView,创建寄宿图的本质都一样。只不过CALayer不会自动重绘,需要我们显式调用display,而且使用单独的CALayer还得实现CALayerDelegate。所以通常做法就是实现UIView的drawRect:,UIView会帮你做完剩下的工作。

② CALayer的显示效果

CALayer不仅可以简单展示图片,还能通过一些属性改变视觉效果。

  • 圆角(conrnerRadius)
    该属性控制图层角的曲率,这个曲率值只影响背景颜色,不影响背景图片或子图层,所以要把masksToBounds设为YES。

  • 图层边框(borderWidth&borderColor)
    这两个属性控制图层边框,有点需要注意的是边框是在图层边界里面的,而且在所有内容包括子图层之前。

  • 阴影(shadowOpacity、shadowPath等)
    shadowOpacity控制阴影的透明度,shadowColor控制阴影的颜色,shadowOffset控制阴影的方向和距离,shadowRadius控制阴影的模糊度。
    和图层边框不同的是,阴影是根据内容的外形来决定的,所以会把寄宿图或子图层考虑在内。所以使用masksToBounds时要注意。shadowPath是控制阴影形状,根据内容计算阴影也是很耗费资源的,所以知道阴影形状直接指定会提高性能。

- (void)createSimpleLayer{
    CALayer *blueLayer = [[CALayer alloc] init];
    blueLayer.frame = CGRectMake(50, 50, 100, 100);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    [self.view.layer addSublayer:blueLayer];
    
    //设置阴影
    blueLayer.shadowColor = [UIColor redColor].CGColor;//默认黑色
    blueLayer.shadowOpacity = 0.5;//默认是0(完全透明,不显示),1是不透明
    blueLayer.shadowOffset = CGSizeMake(0, 3);//默认是(0.-3)望y的负方向偏移
    blueLayer.shadowRadius = 10;//默认0(边界清晰),值越来越模糊,所以要设为非0才自然
    
    //自定义阴影形状
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, blueLayer.bounds);
    blueLayer.shadowPath = path;
    CGPathRelease(path);
  • 图层蒙板(mask)
    如果想展示的内容不是一个矩形或者圆角矩形,而是一个特殊形状的图形,就要用到图层蒙板mask。mask也是CALayer类型,它相当于一个子图层,不同于那种绘制在父图层的子图层,mask是定义父图层的可见区域。mask图层重要的是轮廓(边界),它就像做饼干的模子。
  • 拉伸(minificationFilter&magnificationFilter)
    当图片显示不同大小时,是有个拉伸过滤算法在起作用。CALayer提供了三种拉伸过滤方法。minificationFilter(缩小图片)和magnificationFilter(方法图片)默认值都是kCAFilterLinear。
kCAFilterLinear;//双线性滤波算法(放大倍数大容易模糊不清)
kCAFilterTrilinear;//三线性滤波算法(跟双线性差不多)
kCAFilterNearest;//最近过滤算法(适用于小图,或差异小,斜线少的大图)
  • 透明(opacity)
    CALayer的opacity(对应UIView的alpha)都是会影响子层级的。透明度的叠加原理是这样,假如一个50%透明度的图层,图层的每一个像素都会一般显示自己的颜色,一般显示图层下面的颜色。
    单独图层还好,要是图层中有子图层就会出现问题(两个图层计算出来的透明度其实不一致)。所以,要想这个图层所在的图层树像一个整体一样的透明效果,就可以设置shouldRasterize属性为YES,在应用透明度之前,图层和子图层会被合成一个整体的图片。不过设置shouldRasterize也要注意rasterizationScale(默认1)去适配屏幕(设为[UIScreen mainScreen].scale),防止在Retina屏幕像素化。
③ CALayer的变换

在谈变换前,我们先来了解CALayer布局的属性。
UIView有三个重要的布局属性:frame、bounds、center,分别对应CALayer的frame、bounds、position。
为什么会有center和position呢?主要是因为CALayer有个anchorPoint(锚点),可以解释为把手(比如旋转时就是绕着该点旋转),默认值是(0.5,0.5)就在中心点。UIView没有anchorPoint可以设置,所以固定在中心点,CALayer通过设置可以在任何地方,包括图层外面(>1的情况)。
另外,与UIView的二维坐标不同,CALayer是三维坐标。所以有另外两个属性:zPosition和anchorPointZ。zPosition除了做仿射变换,最常见的就是改变图层显示顺序,通过提高zPosition(提高很小的值就可以,如0.0001)让该图层前置,不过一点需要注意的是不能改变事件传递的顺序。
说到事件传递,CALayer并不关心任何响应链事件,所以不能直接处理触摸事件和手势,但它有一系列方法帮你处理事件containsPoint:hitTest:

2D变换(仿射变换)

说起变换,我们最常用的就是UIView的transform属性,UIView的transform属性是一个CGAffineTransform类型,用于在二维空间旋转、缩放、平移。以下公式的意思是用图层的每一个CGPoint和CGAffineTransform矩阵的每一行对应元素相乘再相加,就形成一个新的位置。2D变换还有一个特点,变换后图层原本平行的两条线仍然会保持平行。

CGAffineTransform公式.png

这边Core Graphics提供了一系列函数,让我们实现简单的仿射变换。

/*  单位矩阵(不变换)*/
CGAffineTransformIdentity;

/*  简单变换  */
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy);//缩放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty);//平移
CGAffineTransformMakeRotation(CGFloat angle);//旋转

/*  混合变换  */
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy);
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty);
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle);
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
3D变换

CALayer也有个transform属性,它的类型是CATransform3D。CATransform3D和CGAffineTransform类似,也是一个矩阵,不过CATransform3D可以在三维空间做变换。

CATransform3D公式.png

有一点需要注意的是CGAffineTransform是属于Core Graphics框架的,而Core Graphics是2D绘图的API,所以CGAffineTransform仅对2D变换有效。而CATransform3D是属于Core Animation框架,和Core Graphics提供的函数类似,Core Animation也提供了一系列函数来做3D变换,只是多了了坐标轴的参数。

坐标轴和旋转方向.png
/*  单位矩阵(不变换)*/
CATransform3DIdentity;

/*  简单变换  */
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);//缩放
CATransform3DMakeTranslation(CGFloat tx, CGFloat ty, CGFloat tz);//平移
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);//旋转

/*  混合变换  */
CATransform3DScale(CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz);
CATransform3DTranslate(CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz);
CATransform3DRotate(CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
CATransform3DConcat(CATransform3D a, CATransform3D b);

在一些变换时(比如旋转),要先有透视效果(近大远小),需要设置CATransform3D矩阵中的m34,m34用于用于按比例缩放X和Y的值来计算到底离视角多远。m34默认值是0,我们可以设置m34为-1.0/d来应用透视效果,d通常取500-1000。当在透视效果绘图时,物体会越远越小,当远离到某个极限距离,就会缩成一点,该点就是灭点。为了模拟现实效果,Core Animation定义了该点位于变换图层的anchorPoint。所以当改变了图层的position,你也改变了它的灭点,做3D变换时这一点要特别注意。
当在有多个视图或图层,如果要做3D变换,就需要用到CALayer的sublayerTransform,它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的图层。

- (void)createTransform3D{
    CALayer *blueLayer = [[CALayer alloc] init];
    blueLayer.frame = CGRectMake(50, 200, 100, 100);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    blueLayer.transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    [self.view.layer addSublayer:blueLayer];
    
    CALayer *redLayer = [[CALayer alloc] init];
    redLayer.frame = CGRectMake(200, 200, 100, 100);
    redLayer.backgroundColor = [UIColor redColor].CGColor;
    redLayer.transform = CATransform3DMakeRotation(- M_PI_4, 0, 1, 0);
    [self.view.layer addSublayer:redLayer];
    
    //设置统一的灭点
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0/500;
    self.view.layer.sublayerTransform = transform;
}

假如旋转到可以看到背面,我们可以看到背面是一个对称的图形。CALayer有个doubleSided的属性来控制图层背面是否要被绘制,默认值是YES。为了防止浪费资源,应该设置为NO,这样图层正面将要从相机视角消失时,即背面要出来时,背面才不会被绘制。
还有一个比较重要的知识点,每个图层的3D场景其实是扁平化的,即3D场仅仅是绘制在图层表面,而不是你所想象的那个3D。

三. 专用图层(CALayer的子类)

CAShapeLayer

绘制图形,我们可以用Core Graphics在原始的CALayer上绘制,也可以用CAShapeLayer。CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。指定线宽,颜色等属性,用CGPath定义要绘制的图形,CAShapeLayer就会自动渲染出来。所以对比Core Graphics绘制,CAShapeLayer有如下优点:

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比Core Graphics快很多。
  • 高效使用内存。一个CAShapeLayer不需要像普通的CALayer一样创建寄存图,所以无论多大,都不会占用太多内存。
  • 不会被图层边界裁剪。CAShapeLayer可以再边界外绘制,而使用Core Graphics在普通CALayer会被裁剪。
  • 不会出现像素化。(因为没有寄存图)
- (void)createCAShapeLayer{
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;//描线颜色
    shapeLayer.fillColor = [UIColor blueColor].CGColor;//填充颜色
    shapeLayer.lineWidth = 10;//线宽
    shapeLayer.lineCap = kCALineCapRound;//线条结尾样式
    shapeLayer.lineJoin = kCALineJoinRound;//线条结合处样式
    
    //CAShapeLayer还有个好处,就是通过设置path来设置单个倒角
    UIRectCorner *corner = UIRectCornerTopLeft|UIRectCornerTopRight;
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 100, 160, 140) byRoundingCorners:*corner cornerRadii:CGSizeMake(30, 30)];

    shapeLayer.path = path.CGPath;//路径
    [self.view.layer addSublayer:shapeLayer];
}
CAGradientLayer

CAGradientLayer是用来生成两种或多种颜色平滑渐变的。它的优点是绘制时使用了硬件加速。

CATextLayer

如果想在一个图层显示文字,可以像UILabel一样借助图层代理将字符串使用Core Graphics写入图层的内容。想越过代理,直接在图层上操作是十分复杂,除了使用CATextLayer。CATextLayer以图层的形式包含了UILabel几乎所有的绘制特性,并且有一些额外新特性。

CATransformLayer

CATransformLayer不同于普通的CALayer,因为它不能显示自己的内容。它是作为一个容器,把子图层包装成一个整体变换(比如正方体的旋转)。

CAReplicatorLayer

CAReplicatorLayer的目的是为了高效生成多个相似的图层。还有一个常见的用途就是复制出一个负比例的图层来实现反射效果。(可以自定义一个View,通过layerClass方法将普通的CALayer换成CAReplicatorLayer,然后进行设置。)

CAScrollLayer

CAScrollLayer可以实现像UIScrollView一样的功能,可以理解为图层中的ScrollView。

CATiledLayer

CATiledLayer为载入大图出现的性能问题提供一个解决方案,将大图分解的小图按需单独载入。

CAEmitterLayer

CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画(比如烟雾,火,雨)。

CAEAGLLayer

CAEAGLLayer用来显示任意的OpenGL图形。

AVPlayerLayer

AVPlayerLayer不属于Core Animation框架,是属于AVFountation框架,是用来播放视频的,是高级接口如MPMoivePlayer的底层实现。

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

推荐阅读更多精彩内容