TextKit框架详细解析 (三) —— 一个简单布局示例(一)

版本记录

版本号 时间
V1.0 2018.08.30

前言

TextKit框架是对Core Text的封装,用简洁的调用方式实现了大部分Core Text的功能。 TextKit是一个偏上层的开发框架,在iOS7以上可用,使用它可以方便灵活处理复杂的文本布局,满足开发中对文本布局的各种复杂需求。TextKit实际上是基于CoreText的一个上层框架,其是面向对象的。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. TextKit框架详细解析 (一) —— 基本概览和应用场景(一)
2. TextKit框架详细解析 (二) —— 基本概览和应用场景(二)

开始

注意!注意!注意!:本文的写作背景比较老,是iOS7,现在已经没人使用了,但是之所以这样还是放出来,就想说一下TextKit框架的体系和应用范围,个别人可以忽略本文。

随着Apple增加更多功能和特性,iOS呈现文本的方式在过去几年中变得越来越强大。 iOS 7的发布带来了一些最重要的文本呈现更改。 现在,iOS 8以后以这种能力为基础,使其更易于使用。

在iOS 6之前的过去,Web视图通常是使用混合样式呈现文本的最简单方法,例如粗体,斜体或甚至带有颜色。

2012年,iOS 6为许多UIKit控件添加了属性字符串支持。 这使得在不使用渲染HTML的情况下实现这种类型的布局变得更加容易。

在iOS 6中,UIKit基于WebKitCore Graphics的字符串绘制函数控制其文本功能,如下面的分层图所示:

注意:在这个图中,有什么事情让你觉得奇怪吗? 没错 - UITextView使用WebKit。 iOS 6将文本视图上的属性字符串呈现为HTML,这一事实对于尚未深入深入框架的开发人员来说并不明显。

iOS 6中的属性字符串确实对许多用例有用。 但是,对于高级布局和多行渲染文本,Core Text仍然是唯一真正的选择 - 相对靠近底层且繁琐的框架。

但是,从iOS 7开始,有一种更简单的方法。 凭借当前简约的设计重点,避开多余装饰并更多地关注排版 - 例如剥离所有边框和阴影的UIButton,只留下文本 - iOS 7添加了一个用于处理文本和文本属性的全新框架并不奇怪: Text Kit

现在架构更加整洁,所有基于文本的UIKit控件(除了UIWebView)现在都使用Text Kit,如下图所示:

Text Kit构建于Core Text之上,继承了Core Text框架的全部功能,并且令所有开发人员高兴,将其包含在改进的面向对象的API中。

在这个Text Kit教程中,您将探索Text Kit的各种功能,因为您为iPhone创建了一个简单但功能丰富的笔记记录应用程序,该应用程序具动态文本大小调整和动态文本样式。

下面我们打开建立的工程并运行,界面应该如下所示:

该应用程序创建一个初始的Note实例数组,并将它们呈现在table view控制器中。sb和segue在table view中检测单元格选择,并处理到视图控制器的转换,用户可以在其中编辑选定的note。

浏览源代码并稍微使用应用程序,以了解应用程序的结构及其运行方式。 完成后,请转到下一部分,其中讨论了应用中动态类型的使用。


Dynamic Type - 动态类型

Dynamic Type是iOS 7中改变游戏规则最多的功能之一,它将选择权放在您的应用程序上,以符合用户选择的字体大小和权重。

在iOS 7中,打开Settings应用并导航到General/AccessibilityGeneral/Text Size以查看影响应用显示文本方式的设置:

在iOS 8中,打开Settings应用并导航到General/Accessibility/Larger Text以访问Dynamic Type文本大小。

iOS 7提供了通过增加字体权重来增强文本易读性的功能,以及为支持动态文本的应用程序设置首选字体大小的选项。

注意:当Apple在WWDC 2013上发布Text Kit时,他们强烈建议开发人员采用动态类型。在WWDC 2014上,苹果公司走得更远了。他们强调所有内置应用都支持动态类型。此外,他们使得动态类型在iOS 8中更容易使用。WWDC 2014会议226表 What’s New in Tables and Collection Views,涵盖了对table views和collections的iOS 8动态类型支持。建议你观看它!用户希望为iOS 7及更高版本编写的应用程序能够遵守这些设置。

为了使用动态类型,您需要使用styles指定字体,而不是明确说明字体名称和大小。 iOS 7为UIFont添加了一个新方法preferredFontForTextStyle,它使用用户的字体首选项为给定样式创建字体。

下图给出了六种不同字体样式的示例:

左侧的文本使用最小的用户可选文本大小,中心的文本使用最大的文本,右侧的文本显示启用可访问性粗体文本功能的效果。


Basic Support - 基本支持

实现对动态文本的基本支持相对简单。 而不是在应用程序中使用显式字体,而是请求特定样式的字体。 在运行时,应用程序根据给定的样式和用户的文本首选项选择合适的字体。

使用iOS 8,Apple实现动态类型比在iOS 7中更容易。特别是,table views中的默认标签自动支持动态类型! 尽管如此,您可能希望支持iOS 7,和/或您可能希望在table views中使用自定义标签。 首先,您将学习如何处理iOS 7的动态类型。然后,您将了解Apple如何在iOS 8中让您的生活更轻松。


Why iOS 7 is Great, but iOS 8 is Even Greater

初始化项目的deployment设置为iOS 8,在继续之前,构建并运行应用程序并尝试将默认文本大小更改为各种值。您将发现table view列表中的文本大小和单元格高度都会相应更改。而且你不需要做任何事情!但是请注意,notes本身并不反映文本大小设置的更改。

尽管不是很精彩,但iOS 7中的内容仍然相当不错。对于本文的大部分内容,如果您使用的是iOS 7或iOS 8(只是确保您使用的是Xcode 6!),现在,将应用程序的deployment level设置为iOS 7,然后在iOS模拟器中进行操作。以下大部分内容在iOS 8中也有用,所以即使你不打算支持早于iOS 8的iOS版本,它也是值得的。

注意:要在Xcode 6中将部署级别设置为iOS 7,请选择View/Navigators/Show Project Navigator。在右侧面板中,选择项目,单击info并在iOS Deployment Target弹出菜单中选择iOS 7。此外,在右侧面板中选择目标,并将部署目标设置为iOS 7。确保模拟器充当iOS 7设备也很重要。所以在iOS模拟器中选择Hardware/Device/iOS 7/iPhone 5s

现在您已经准备好作为iOS 7应用程序运行,继续构建并运行。像以前一样玩文字大小设置,你会发现遗憾的是,应用程序忽略了你的设置。现在,您将做一些事情来使其在iOS 7中运行。

打开NoteEditorViewController.swift并将以下内容添加到viewDidLoad的末尾:

textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

请注意,您没有指定Helvetica Neue等确切字体。 相反,您要求使用UIFontTextStyleBody文本样式常量为正文添加适当的字体。

接下来,打开NotesListViewController.swift并在返回调用之前将以下内容添加到tableView(_:cellForRowAtIndexPath :)方法:

cell.textLabel?.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

同样,您指定了文本样式,iOS将返回适当的字体。

使用字体名称的语义方法(例如UIFontTextStyleSubHeadline)有助于避免代码中的硬编码字体名称和样式 - 并确保您的应用程序能够按预期正确响应用户定义的排版设置。

再次构建并运行应用程序,您会注意到table view和note屏幕现在支持当前文本大小;两者之间的差异显示在下面的屏幕截图中:

这看起来很不错 - 但是敏锐的读者会注意到这只是解决方案的一半。 返回General/Text Size下的Settings应用,然后再次修改文本大小。 这一次,切换回SwiftTextKitNotepad - 无需重新启动应用程序 - 您会注意到您的应用程序没有响应新的文本大小。


Responding to Updates - 响应更新

打开NoteEditorViewController.swift并将以下代码添加到viewDidLoad的末尾

NSNotificationCenter.defaultCenter().addObserver(self, 
    selector: "preferredContentSizeChanged:", 
    name: UIContentSizeCategoryDidChangeNotification,
    object: nil)

上面的代码注册该类以在首选内容大小更改时接收通知,并在发生此事件时传入要调用的方法(preferredContentSizeChanged)

接下来,将以下方法添加到类中:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
}

这只是根据新的首选大小设置文本视图字体。

注意:您可能想知道为什么看起来您将字体设置为之前的相同值。 当用户更改其首选字体大小时,您必须再次请求首选字体;它不会自动更新。 更改字体首选项时,通过preferredFontForTextStyle返回的字体将有所不同。

打开NotesListViewController.swift并通过向类中添加以下代码来覆盖viewDidLoad函数:

override func viewDidLoad() {
  super.viewDidLoad()
  NSNotificationCenter.defaultCenter().addObserver(self,
      selector: "preferredContentSizeChanged:", 
      name: UIContentSizeCategoryDidChangeNotification, 
      object: nil)
}

嘿,是不是你刚刚添加到NoteEditorViewController.swift的代码? 是的,它是 - 但你会以稍微不同的方式处理首选的字体更改。

将以下方法添加到类中:

func preferredContentSizeChanged(notification: NSNotification) {
  tableView.reloadData()
}

上面的代码只是指示table view重新加载其可见单元格,这会更新每个单元格的外观。 这将触发对preferredFontForTextStyle()的调用并刷新字体选择。

构建并运行您的应用程序;更改文本大小设置,并验证您的应用是否正确响应新用户首选项。


Changing Layout - 改变布局

这部分看起来效果很好,但是当你选择一个非常小的字体大小时,你的table view看起来有点稀疏,如右下图所示:

这是动态类型的棘手方面之一(在iOS 7中)。 为了确保您的应用程序在各种字体大小中看起来很好,您的布局需要响应用户的文本设置。 自动布局为您解决了很多问题,但这是您必须自己解决的一个问题。

您的表行高度需要随着字体大小的变化而变化。 实现tableView(_:heightForRowAtIndexPath :)委托方法很好地解决了这个问题。

将以下代码添加到NotesListViewController.swift,在标记为Table view data source的部分中:

let label: UILabel = {
  let temporaryLabel = UILabel(frame: CGRect(x: 0, y: 0, width: Int.max, height: Int.max))
  temporaryLabel.text = "test"
  return temporaryLabel
}()

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
  label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  label.sizeToFit()
  return label.frame.height * 1.7
}

上面的代码创建了一个UILabel的共享实例,table view用它来计算单元格的高度。 然后,在tableView(_:heightForRowAtIndexPath :)中,将标签的字体设置为表视图单元格使用的相同字体。 然后它在标签上调用sizeToFit,强制标签的frame紧紧围绕文本,并导致frame高度与表格行高度成比例。

构建并运行您的应用程序;再次修改文本大小设置,表格行现在动态调整大小以适合文本大小,如下面的屏幕截图所示:

如果您愿意,现在可以在本教程的其余部分将deployment重置为iOS 8。


Letterpress Effect - 凸版印刷效果

凸版印刷效果为文本添加了细微的阴影和高光,使其具有深度感 - 就像文字略微压入屏幕一样。

注意:术语“凸版印刷 - letterpress”是对早期印刷机的一种认可,它印有一组刻在块上的字母并将它们压入页面。 这些字母经常在页面上留下一个小缩进 - 这是一种意想不到但视觉上令人愉悦的效果,这种效果在今天的数字排版中经常被复制。

打开NotesListViewController.swift并使用以下实现替换tableView(_:cellForRowAtIndexPath :):

override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell? {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

  let note = notes[indexPath.row]
  let font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  let textColor = UIColor(red: 0.175, green: 0.458, blue: 0.831, alpha: 1)
  let attributes = [
    NSForegroundColorAttributeName : textColor,
    NSFontAttributeName : font,
    NSTextEffectAttributeName : NSTextEffectLetterpressStyle
  ]
  let attributedString = NSAttributedString(string: note.title, attributes: attributes)

  cell.textLabel?.attributedText = attributedString

  return cell
}

上面的代码使用凸版印刷样式为表格单元格的标题创建一个属性字符串。

构建并运行您的应用程序;您的table view现在将显示具有良好凸版效果的文本,如下所示:

Letterpress是一种微妙的效果 - 但这并不意味着你应该过度使用它! 视觉效果可能会使您的文字更有趣,但它们并不一定能让您的文字更清晰。


Exclusion Paths - 路径排除

围绕图像或其他对象的流动文本是大多数文字处理器的标准特征。 Text Kit允许您使用排除路径- exclusion paths在复杂路径和形状周围渲染文本。

告诉用户note的创建日期是很方便的;您将在显示此信息的note的右上角添加一个小的曲线视图。

您将首先添加视图本身 - 然后您将创建一个排除路径以使文本环绕它。

1. Adding the View - 添加视图

打开NoteEditorViewController.swift并将以下属性声明添加到类中:

var timeView: TimeIndicatorView!

顾名思义,这里有时间指示器子视图。

接下来,将此代码添加到viewDidLoad的最后:

timeView = TimeIndicatorView(date: note.timestamp)
textView.addSubview(timeView)

这只是创建新视图的实例并将其添加为子视图。

TimeIndicatorView计算自己的大小,但不会自动执行此操作。 当视图控制器布局子视图时,您需要一种机制来调用updateSize

最后,将以下两个方法添加到类中:

override func viewDidLayoutSubviews() {
  updateTimeIndicatorFrame()
}

func updateTimeIndicatorFrame() {
  timeView.updateSize()
  timeView.frame = CGRectOffset(timeView.frame, textView.frame.width - timeView.frame.width, 0)
}

viewDidLayoutSubviews调用updateTimeIndicatorFrame,它执行两项操作:调用updateSize设置子视图的大小,并将子视图放在文本视图的右上角。

剩下的就是当视图控制器收到内容大小已更改的通知时调用updateTimeIndicatorFrame。 将preferredContentSizeChanged的实现替换为以下内容:

func preferredContentSizeChanged(notification: NSNotification) {
  textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
  updateTimeIndicatorFrame()
}

构建并运行您的项目,点击列表项,时间指示器视图将显示在项目视图的右上角,如下所示:

修改设备文本大小首选项,视图将自动调整为适合。

但是,有些事情看起来不太对劲。 note的文本在时间指示器视图后面呈现,而不是整齐地围绕它流动。 幸运的是,这是排除路径旨在解决的确切问题。

打开TimeIndicatorView.swift并查看curvePathWithOrigin()。 时间指示器视图在填充背景时使用此代码,但您也可以使用它来确定文本流动的路径。 啊哈 - 这就是为什么贝塞尔曲线的计算被分解成自己的方法!

剩下的就是定义排除路径本身。 打开NoteEditorViewController.swift并将以下代码块添加到updateTimeIndicatorFrame的最后:

let exclusionPath = timeView.curvePathWithOrigin(timeView.center)
textView.textContainer.exclusionPaths = [exclusionPath]

上面的代码基于在时间指示器视图中创建的Bezier路径创建排除路径,但是具有相对于文本视图的原点和坐标。

构建并运行项目并从列表中选择一个项目;现在,文本在时间指示器视图周围很好地流动。

这个简单的例子只是划分了排除路径的能力。 您可能会注意到exclusionPaths属性需要一个路径数组,这意味着每个容器都可以支持多个排除路径。

此外,排除路径可以根据需要简单或复杂。 需要渲染星形或蝴蝶形的文字吗? 只要您可以定义路径,exclusionPaths就会毫无问题地处理它!

当文本容器在排除路径更改时通知布局管理器时,您可以实现动态或甚至动画排除路径 - 只是不要指望您的用户欣赏在他们尝试阅读时在屏幕上移动的文本!


Dynamic Text Formatting and Storage - 动态文本格式和存储

您已经看到Text Kit可以根据用户的文本大小首选项动态调整字体。 但是,如果字体可以根据实际文本本身动态更新,那会不会很酷?

例如,如果您想自动创建此应用程序,该怎么办:

  • 使用波形符(〜)包围的任何文本都是一个花哨的字体
  • 使用下划线字符(_)斜体包围任何文本
  • 将短划线字符( - )包围的任何文本划掉
  • 将所有大写字母的文字设为红色

通过利用Text Kit框架的强大功能,这正是您在本节中所要做的!

为此,您需要了解Text Kit中的文本存储系统的工作原理。 这是一个图表,显示用于存储,呈现和显示文本的“Text Kit stack”

在幕后,Apple会在您创建UITextViewUILabelUITextField时自动为您创建这些类。在您的应用中,您可以使用这些默认实现,也可以自定义任何部分以获得自己的行为。让我们来看看每个类:

  • NSTextStorage将要呈现的文本存储为属性字符串,并通知布局管理器文本内容的任何更改。您可能希望子类化NSTextStorage,以便在文本更新时动态更改文本属性。
  • NSLayoutManager获取存储的文本并在屏幕上呈现它,它在您的应用中充当布局“引擎”。
  • NSTextContainer描述应用呈现文本的屏幕区域的几何形状。每个文本容器通常与UITextView相关联。您可能希望子类化NSTextContainer以定义要在其中呈现文本的复杂形状。

要在此应用程序中实现动态文本格式设置功能,您需要子类化NSTextStorage,以便在用户在文本中键入时动态添加文本属性。

一旦您创建了自定义NSTextStorage,您将用您自己的实现替换UITextView的默认文本存储实例。让我们试一试!


Subclassing NSTextStorage - 子类化NSTextStorage

右键单击项目导航器中的SwiftTextKitNotepad组,选择New File ...,然后选择iOS / Source / Cocoa Touch Class并单击Next

将类命名为SyntaxHighlightTextStorage,使其成为NSTextStorage的子类,并确认语言设置为Swift。 单击Next,然后单击Create

打开SyntaxHighlightTextStorage.swift并在类声明中添加一个新属性:

let backingStore = NSMutableAttributedString()

文本存储子类必须提供自己的持久性,因此使用NSMutableAttributedString后备存储 - 稍后将对此进行更多介绍。

接下来将以下内容添加到类中:

override var string: String {
  return backingStore.string
}

override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [NSObject : AnyObject] {
  return backingStore.attributesAtIndex(index, effectiveRange: range)
}

这两个声明中的第一个覆盖string计算属性,推迟到后备存储。 同样,attributesAtIndex方法也委托给后备存储。

最后将剩余的强制覆盖添加到同一个文件中:

override func replaceCharactersInRange(range: NSRange, withString str: String) {
  println("replaceCharactersInRange:\(range) withString:\(str)")

  beginEditing()
  backingStore.replaceCharactersInRange(range, withString:str)
  edited(.EditedCharacters | .EditedAttributes, range: range, changeInLength: (str as NSString).length - range.length)
  endEditing()
}

override func setAttributes(attrs: [NSObject : AnyObject]!, range: NSRange) {
  println("setAttributes:\(attrs) range:\(range)")

  beginEditing()
  backingStore.setAttributes(attrs, range: range)
  edited(.EditedAttributes, range: range, changeInLength: 0)
  endEditing()
}

同样,这些方法委托给后备存储。 但是,它们还包含对beginEditingeditedendEditing的调用。 文本存储类需要这三种方法,以便在进行编辑时通知其关联的布局管理器。

您可能已经注意到,为了子类化文本存储,您需要编写相当多的代码。 由于NSTextStorage是类集群的公共接口,因此您不能仅将其子类化并覆盖一些方法来扩展其功能。 相反,您必须自己实现某些要求,例如属性字符串数据的后备存储。

注意:类集群是Apple整个框架中常用的设计模式。类集群只是Abstract Factory模式的Objective-C实现,它提供了一个通用接口,用于创建相关或依赖对象的族,而无需指定具体类。 NSArrayNSNumber等熟悉的类实际上是类集群的公共接口。

Apple使用类集群将私有具体子类封装在公共抽象超类下,并且它是这个抽象超类,它声明客户端必须使用的方法才能创建其私有子类的实例。客户端也完全不知道工厂正在分配哪个私有类,因为它只与公共接口进行交互。

使用类集群肯定简化了界面,使学习和使用类变得更加容易,但重要的是要注意在可扩展性和简单性之间进行权衡。创建集群的抽象超类的自定义子类通常要困难得多。

现在你有了一个自定义的NSTextStorage,你需要创建一个使用它的UITextView

后记

本篇主要讲述了一个简单布局示例,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容