Android Drawable 资源:VectorDrawable

概述


2014年推出Android Lolliop 带来了全新的设计语言——Material Design,同时也带来VectorDrawable,标志着Android对矢量图的支持开始了。

矢量图的优势主要有两个:

  • 一个是简化Drawable资源的管理。
  • 另一个是丰富多彩的矢量动画。

众所周知,Android运行在屏幕大小多样、像素密度不一的设备上。对于开发人员来说,为了更好的用户体验,通常需要提供各种像素密度对应的图片,甚至需要提供不同设备的对应的图片。一方面提供与管理一个资源的多个版本比较麻烦,另一方面也会导致最后生成的安装包变大。而使用矢量图就可以轻松解决这个问题,矢量图只需要一个xml描述文件描述,而位图需要记录每个像素点的颜色值。同时,矢量图在缩放时不会失真,不需要提供多个资源图片。

矢量图还可以通过简单的方式实现炫酷的矢量图动画。效果可以参考RichPath库提供的效果图片,如下:
矢量图动画效果展示

矢量图的缺陷:虽然矢量图的优点很明显,但是矢量图并不能完全取代位图,对于颜色较多较不规律的图片,矢量图显得力不从心。同时,由于需要在运行时根据xml描述文件绘制图形,需要比位图消耗更多的性能。因此,矢量图最好用在Icon图标上。

Android Lolliop虽然带来了VectorDrawable,但是并不意味着完全支持了矢量图。实际上Android支持的是矢量图标准中的Path部分。

矢量图资源


在android中,矢量图资源是一个用<vector>标签描述xml文件。可以自定义,也可以通过工具转换svg文件。如下图:
新建VectorAsset

然后在Configure Vector Asset界面可以直接点击Icon后的图形选择SDK自带的Material Icon,还可以切换到Local File模式,通过工具转换SVG文件或PSD文件。

注意:

  • Android不完全支持SVG标准,转换时会丢弃部分信息,如渐变色之类的。
  • 在转化某些非path协议时,转化效果也有点细节问题。比如转化svg的ellipse时,它用了一系列的贝塞尔曲线Path来拟合椭圆,更简化的方法应该是使用A命令来生成椭圆。
  • 工具生成的xml内width/height属性直接使用svg的画布尺寸的,单位是dp,这样的drawable可能会导致性能问题,所以要改成自己需要的宽高。

矢量图中path的定义与命令

每个命令大写字母代表后面的参数是绝对坐标,而小写字母表示相对坐标。

类型 功能 命令 描述
移动起始点 —— M (move to) 用法:M X,Y (X,Y)是Canvas上的点的位置,M命令会改变Path的初始点 ,如 M 10,10 或者 M10 10,Path从 (10, 10) 开始绘制。
m 用法:m x,y 假设当前点是 (X0, Y0),m命令会将Path的初始点改为(X0+x, Y0+y),如当前点为 (10, 10), m 10,5 命令后,Path从 (20, 15) 开始绘制。
画直线 水平线 H (horizontal lineto) 用法:H X 从当前位置 (X0, Y0) 绘制直线到 (X, Y0)
h 用法:h x 从当前位置 (X0, Y0) 绘制直线到 (X0+x, Y0)
垂直线 V (vertical lineto) 用法:V Y 从当前位置 (X0, Y0) 绘制直线到 (X0, Y)
v 用法:v y 从当前位置 (X0, Y0) 绘制直线到 (X0, Y+y)
直线 L (lineto) 用法:L X,Y 从当前位置绘制直线到 (X, Y),如 L 10,10 或者 L10 10
l 用法:l x,y 表示从当前位置 (X0, Y0) 绘制直线到 (X0+x, Y0+y)
画曲线 椭圆圆弧 A (elliptical Arc) 用法:绘制椭圆圆弧的参数比较复杂,如下:A rx ry x-axis-rotation large-arc-flag sweep-flag X Y,表示绘制一个椭圆圆弧经过(X,Y)点。
a 用法:绘制椭圆圆弧的参数比较复杂,如下:a rx ry x-axis-rotation large-arc-flag sweep-flag x y,表示从当前位置 (X0, Y0) 绘制一个椭圆圆弧经过 (X0+x, Y0+y) 点。
二次贝塞尔曲线 Q (quadratic Bézier curveto) 用法:Q X1,Y1 X,Y,从当前点到 (X, Y) 绘制一条控制点是 (X1, Y1) 的二次贝塞尔曲线,如Q 6,4 10,5,控制点是 (6, 4),最终点是(10,5)。
q 用法:q x1,y1 x,y,从当前点 (X0, Y0) 到 (X0+x, Y0+y) 绘制一条控制点是 (X0+x1, Y0+y1) 的二次贝塞尔曲线,如q 6,4 10,5 当前点是 (5, 5),控制点是 (11, 9),最终点是(15,10)。
T (smooth quadratic Bézier curveto) 用法:T X,Y 如果前面是一个Q/T命令,则自动计算一个保证起始点平滑的对称控制点 (X1,Y1),从当前点到 (X, Y) 绘制一条控制点是(X1,Y1)的二次贝塞尔曲线。其它与上面Q命令相同。 如果T前面是一个非Q/T命令,则无法计算一个对称的控制点,则从当前点到(X,Y)绘制一条直线(降阶特性)。
t 用法:t x,y 功能与T命令相同 ,从当前点 (X0, Y0) 到 (X0+x, Y0+y) 绘制一条二次贝塞尔曲线
三次贝塞尔曲线 C (cubic bezier) 用法:C X1,Y1 X2,Y2 X,Y 从当前点到 (X, Y) 点绘制一条控制点是(X1,Y1)、(X2,Y2)的三次贝塞尔曲线,如C 6,4, 7,4, 10,5,控制点是(6,4)、(7,4),最终点是(10,5)。
c 用法:c x1,y1 x2,y2 x,y 从当前点 (X0,Y0) 到 (X0+x, Y0+y) 点绘制一条控制点是(X0+x1, Y0+y1)、(X0+x2, Y0+y2) 的三次贝塞尔曲线
S (smooth cubicto) 用法:S X2,Y2 X,Y 如果前面是一个C/S命令,则自动计算一个保证起始点平滑的对称控制点 (X1,Y1),从当前点到 (X,Y) 绘制一条控制点是 (X1,Y1)、(X2, Y2) 的三次贝塞尔曲线。其它与上面C命令相同。如果S前面是一个非C/S命令,则无法计算第一个对称的控制点,则从当前点到 (X, Y) 绘制一条控制点是 (X2, Y2) 的二次贝塞尔曲线(降阶特性)。
s s x2,y2 x,y 功能与S命令相同,从当前点 (X0, Y0) 到 (X0+x, Y0+y) 绘制一条控制点是 (X1,Y1)、(X0+x2, Y0+y2) 的三次贝塞尔曲线
闭合图形 —— Z(close) / z 用法:一般Z命令用在一条Path的最末尾,但其实在Z命令后还可以再继续新的路径,不过Z命令不改变Path的初始点。闭合命令没有参数,所以大小写形式效果相同
  • 椭圆圆弧详解

椭圆圆弧 A rx ry x-axis-rotation large-arc-flag sweep-flag X Y

  • rx:椭圆横轴半径

  • ry:椭圆竖轴半径

  • x-axis-rotation:椭圆横轴相对于CanvasX轴的偏移角度

  • large-arc-flag:在前面三个参数确定的情况下,满足当前点到指定点(X,Y)位置条件的圆弧总是有四条,此值取0表示绘制小弧度,取值1表示绘制大弧度

  • sweep-flag:在前面三个参数确定的情况下,满足当前点到指定点(X,Y)位置条件的圆弧总是有四条,去掉上面large-arc-flag标识后还有两个,sweep-flag 取值0表示绘制逆时针方向的圆弧,取值1表示绘制顺时针方向的圆弧。

  • X Y 绘制一个椭圆圆弧经过点 (X, Y)

  • 示意图:
  • 二次贝塞尔曲线详解

二次贝塞尔曲线 Q X1,Y1 X,Y

  • 示意图
    二次贝塞尔曲线演示动画,t在[0,1]区间

光滑二次贝塞尔曲线 T X,Y

  • T命令在Q命令之后:起始点为 (X0, Y0),Q X1,Y1 X,Y T Xm,Ym。从起始点 (X0, Y0) 到 (X, Y) 绘制一条控制点是 (X1, Y1) 的二次贝塞尔曲线。然后以 (X, Y) 为中心取 (X1, Y1) 的对称点 (X', Y'),并以 (X', Y') 为控制点绘制一条从 (X, Y) 到 (Xm,Ym) 的二次贝塞尔曲线。(X', Y') 等于 (2X-X1, 2Y-Y1) 。
    光滑二次贝塞尔曲线
  • T命令不在Q命令之后:因为无法计算一个对称的控制点,所以以 (X, Y) 为控制点,相当于从当前点到 (X, Y) 绘制一条直线(降阶特性)。
  • 三次贝塞尔曲线详解

三次贝塞尔曲线 C X1,Y1 X2,Y2 X,Y

  • 示意图
    三次贝塞尔曲线演示动画,t在[0,1]区间

平滑三次贝塞尔曲线 S X2,Y2 X,Y

  • S命令在C命令之后:起始点为 (X0, Y0),C X1,Y1 X2,Y2 X,Y S X3,Y3 Xm,Ym。从起始点 (X0, Y0) 到 (X, Y) 绘制一条控制点是 (X1, Y1) 、(X2, Y2)的三次贝塞尔曲线。然后以 (X, Y) 为中心取 (X2, Y2) 的对称点 (X', Y'),并以 (X', Y') 、(X3, Y3)为控制点绘制一条从 (X, Y) 到 (Xm,Ym) 的三次贝塞尔曲线。(X', Y') 等于 (2X-X2, 2Y-Y2) 。
  • S命令不在C命令之后:因为无法计算第一个对称的控制点,则以当前点为第一个控制点,相当于从当前点到 (X, Y) 绘制一条控制点是 (X2, Y2) 的二次贝塞尔曲线(降阶特性)。

Android中的VectorDrawable的属性

VectorDrawable在XML文件中以类似SVG格式定义一个静态的图像资源, 每个VectorDrawable都是通过group和path对象组成的树状结构。path用来描述几何图形,而group用来描述几何图形的变形。所有在xml文件中定义的path都会绘制出来。

结构示意图:
VectorDrawable结构
  • 根结点<vector>的属性
属性名 描述
android:name 定义矢量图形的名称
android:width 定义Drawable的宽度,支持所有dimension单位,一般使用dp。drawable的宽度不一定是最终绘制宽度,比如给ImageView设置backgroud则Drawable绘制宽度等于ImageView的宽度,给ImageView设置src则在ImageView大于Drawable宽度时,Drawable绘制宽度等于自己定义的宽度。
android:height 定义Drawable的宽度,支持所有dimension单位,一般是dp。其它同上。
android:viewportWidth 定义矢量图形的视图(viewport)空间的宽度,viewport是一个虚拟的canvas,后面所有的path都在该坐标系上绘制。坐标系左上方为(0,0),横轴从左向右,纵轴从上到下。横轴可视区域就是0~viewportWidth。
android:viewportHeight 定义矢量图形的可视区域的高度。其它见上。[0,0]~[viewportWidth,viewportHeight]定义了虚拟canvas的可视区域。
android:tint 作为染色(tint)的色彩应用到drawable上。默认不应用tint。
android:tintMode tint颜色的Porter-Duff混合模式,默认是src_in。
android:autoMirrored 如果drawable布局方向是RTL(right-to-left)时,drawable绘制是否需要镜像化(镜像化就是绕自身x轴中线旋转180度)。
android:alpha drawble的透明度,取值0~1

注意

  • VectorDrawable内存有一个bitmap缓存,如果矢量图可以确定要用于不同的图像大小的场景,需要创建多个VectorDrawable,不能复用同一个VectorDrawable,否则会有性能问题。同时,android:width与android:height属性指定的大小也应该是需要使用的目标大小。
  • 组结点<group>的属性
    组结点定义其包含图形的转换信息(transformation information)。转换信息定义在vector指定的视图区域内(与viewport坐标系相同)。定义的应用转换的顺序是缩放-->旋转-->平移,所以同时定义的这些属性最先应用scaleX/scaleY属性,最后应用translateX/translateY属性。
属性名 描述
android:name 定义group的名称
android:rotation group对应矢量图形的旋转角度,取值是360度制。
android:pivotX Group旋转和缩放时的中心点的X轴坐标。取值基于viewport视图的坐标系,不能使用百分比。
android:pivotY Group旋转和缩放时的中心点的Y轴坐标。取值基于viewport视图的坐标系,不能使用百分比。
android:scaleX Group在X轴上的缩放比例,最先应用到图形上。
android:scaleY Group在Y轴上的缩放比例,最先应用到图形上。
android:translateX Group在X轴的平移距离,取值基于viewport视图的坐标系。最后应用到图形上。
android:translateY Group在Y轴的平移距离,取值基于viewport视图的坐标系。最后应用到图形上。
  • 路径结点<path>的属性
    路径结点定义一个几何图形及其填充颜色。
属性名 描述
android:name 定义路径的名称
android:pathData 定义路径的数据,路径由多条命令组成,格式与SVG标准的path data的d属性完全相同,路径命令的参数定义在viewport视图的坐标系。具体可以看上面介绍的矢量图中path的定义与命令
android:fillColor 指定填充路径的颜色,一般是一个颜色值,在SDK24及以上,可以指定一个颜色状态列表或者一个渐变的颜色。如果在此属性上做渐变动画,新的属性值会覆盖此值。如果不指定,则path不被填充。
android:strokeColor 指定路径线条的颜色,一般是一个颜色值,在SDK24及以上,可以指定一个颜色状态列表或者一个渐变的颜色。如果在此属性上做渐变动画,新的属性值会覆盖此值。如果不指定,则path的线条不会绘制出来。
android:strokeWidth 指定路径线条的宽度,基于viewport视图的坐标系(不要dp/px之类的结尾)。
android:strokeAlpha 指定路径线条的透明度。
android:fillAlpha 指定填充区域的透明度。
android:trimPathStart 取值从0到1,表示路径从哪里开始绘制。0~trimPathStart区间的路径不会被绘制出来。
android:trimPathEnd 取值从0到1,表示路径绘制到哪里。trimPathEnd~1区间的路径不会被绘制出来。
android:trimPathOffset 平移可绘制区域,取值从0到1,线条从(trimPathOffset+trimPathStart绘制到trimPathOffset+trimPathEnd),注意:trimPathOffset+trimPathEnd如果超过1,其实也是绘制的的,绘制的是0~trimPathOffset+trimPathEnd-1的位置。
android:strokeLineCap 设置线条首尾的外观,三个值:butt(默认,向线条的每个末端添加平直的边缘), round(向线条的每个末端添加圆形线帽), square(向线条的每个末端添加正方形线帽。)。
android:strokeLineJoin 设置当两条线条交汇时,创建什么样的边角(线段连接类型):三个值:miter(默认,创建尖角),round(创建圆角),bevel(创建斜角) 。
android:strokeMiterLimit 设置设置最大斜接长度,斜接长度指的是在两条线交汇处内角和外角之间的距离。只有当 lineJoin 属性为 "miter" 时,miterLimit 才有效。
android:fillType 设置路径的填充类型,与SVG格式的fill-rule属性相同。
  • 裁切路径结点<clip-path>的属性
    裁切路径只能用于当前group和其子元素,只有在裁切路径内的元素才会被显示出来。clip-path定义后才会影响后面path的绘制,如果一个group内有多个path,clip-path定义在第三位,则前面两个path不受其影响,后面的path受其影响。如果希望clip-path对整个group都生效,应放在第一位。
属性名 描述
android:name 定义路径的名称
android:pathData 定义裁切路径,取值与pathData相同。

矢量动画


矢量动画本质上是属性动画系统的一个应用。矢量动画可以有多种动画效果:

  • group属性的动画: 对应的旋转/缩放/平移等效果是传统的动画效果。
  • path属性的动画: 对应的属性可以做出很多绚丽的效果。比如改变pathData属性,可以做出形状变化的动画;改变trimPathStart/trimPathEnd可以做出绘制曲线路径的效果;改变strokeColor可以做出线条颜色变化的效果。
  • clip-path属性的动画: 对应的pathData属性可以通过变化做出各种形状的揭开和遮挡效果。

定义矢量动画 AnimatedVectorDrawable

  • 第一步:定义矢量图 VectorDrawable 资源文件 vd.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="64dp"
   android:width="64dp"
   android:viewportHeight="600"
   android:viewportWidth="600" >
   <group
      android:name="rotationGroup"
      android:pivotX="300.0"
      android:pivotY="300.0"
      android:rotation="45.0" >
      <path
         android:name="vectorPath"
         android:fillColor="#000000"
         android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
   </group>
</vector>
  • 第二步 : 定义动画资源文件 rotation.xml 和 path_morph.xml
    旋转动画 rotation.xml
<objectAnimator
   android:duration="6000"
   android:propertyName="rotation"
   android:valueFrom="0"
   android:valueTo="360" />

再定义一个属性动画 path_morph.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
   <objectAnimator
      android:duration="3000"
      android:propertyName="pathData"
      android:valueFrom="M300,70 l 0,-70 70,70 0,0   -70,70z"
      android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
      android:valueType="pathType"/>
</set> 

该属性动画是对属性名为pathData、属性值类型为pathType的对象属性做插值。因此需要两者具有可比性,valueFrom和valueTo的值内的命令列表必须一一对应(每条命令的参数个数也必须相同),插值工作才能进行。

每次插值的结果,都会被设置到矢量图形标签的pathData属性中,这样界面刷新时,矢量图形指定path绘制的图案就不断的刷新,从而产生动画效果。

注意:
矢量动画要求初始帧的路径命令序列(valueFrom)与结束帧的路径命令序列(valueTo)内的命令必须一一对应,只有参数值可以不同,这样才能插值,从而矢量动画才能执行。否则编译后运行时将会崩溃。

  • 第三步:定义矢量动画 AnimatedVectorDrawable 资源文件 avd.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:drawable="@drawable/vd" >
     <target
         android:name="rotationGroup"
         android:animation="@anim/rotation" />
     <target
         android:name="vectorPath"
         android:animation="@anim/path_morph" />
</animated-vector>

可以看出,一个矢量动画,包含了多个标签,每个target标签其实就是对上面定义的矢量图形的整体或者局部指定动画效果,如何确定对那块图形做动画,就靠上面定义的矢量图形块中定义的名称(android:name)字段了。对group和path的命名,帮助系统在动画执行前从矢量图形内找到它们。

很明显一个矢量动画就是在为一个矢量图的结点绑定定义好的动画效果。如果觉得像上面那样一个一个资源文件来定义比较烦琐,也可以在一个文件中完成定义。如下:

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <aapt:attr name="android:drawable">
        <vector
            android:width="24dp"
            android:height="24dp"
            android:viewportWidth="24"
            android:viewportHeight="24">
            <path
                android:name="root"
                android:strokeWidth="2"
                android:strokeLineCap="square"
                android:strokeColor="?android:colorControlNormal"
                android:pathData="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7" />
        </vector>
    </aapt:attr>
    <target android:name="root">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:propertyName="pathData"
                android:valueFrom="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7"
                android:valueTo="M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4"
                android:duration="300"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:valueType="pathType" />
        </aapt:attr>
    </target>
</animated-vector>

引用矢量动画

矢量动画定义好之后,便可以如其他图像资源(Drawable)一样的方式使用了。然后在Java中,通过取到AnimatedVectorDrawable,执行动画:

AnimatedVectorDrawable animatedVectorDrawable = (AnimatedVectorDrawable) mImageView.getDrawable();
if(animatedVectorDrawable.isRunning()) {
    animatedVectorDrawable.stop();
} else {
    animatedVectorDrawable.start();
}

动态更改矢量动画

矢量图与矢量动画的 pathData 都是不可读写的,官方的API没有提供读写的接口。这里介绍一个非常好的工具库 RichPath。通过这个库完全控制Path和VectorDrawable的属性。

兼容性问题


// TODO 矢量图兼容方案、矢量动画兼容状况

参考文章

Android动画总结系列(6)——矢量图形与矢量动画
深度掌握SVG路径path的贝塞尔曲线指令
SVG 可伸缩矢量图形 简介 Path路径

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

推荐阅读更多精彩内容