Flutter 新闻详情页二——WebView和列表竖直滚动

新闻阅读界面,主要有新闻内容和评论列表或者一些相关推荐新闻。在具体实现时采用WebView和ListView来组合实现。但是WebView的垂直方向的滚动和ListView的垂直滚动会有冲突,解决起来十分麻烦,下面是我的一个方案,希望有读者能提出更好的方案。

在Android开发的时候,解决ListView(或者 RecyclerView)嵌套WebView的方法主要是,使用WebView自适应高度功能(设置高度wrap_content),让ListView完全接管上下滑动。这样滑动十分顺畅,但有个限制WebView必须使用使用loadData() 来加载文本内容形式的HTML数据。

然而,在Flutter中,使用AndroidView功能嵌入原生的WebView,须为WebView指定高度。不然高度会默认为0,看不到网页(显示不出来)。因此,在固定WebView高度的前提下, 要实现新闻页面的整体滚动,就必须自己分发滚动事件, 在合适的时机让应该滚动的控件滚动,达到整个页面滚动的效果。

Flutter中的触摸事件也是按照Layout树来冒泡传递的。查找了一些相关代码和API文档,目前还没有发现类似于Android中ViewGroup的事件拦截控制的方法(onInterceptTouchEvent); 只提供了事件的回调方法和一些监听。例如Listener widget, GestureDetector widget;
详情参考API文档:https://flutter.io/docs/development/ui/advanced/gestures

调查Flutter中能滚动的控件(ListView, GridView, CustomScrollView)之后,发现他们都自动强制拦截了Move事件,子控件根本不能获取到Move事件。但是Flutter也提供了设置禁止滚动的功能 (修改physics属性,设置 NeverScrollableScrollPhysics),提供了滚动控制和回调方式ScrollController。而且在禁止滚动之后,WebView就能够顺畅的滑动了。所以可以采用一种动态修改physics属性的方式来实现整个页面的滚动。

实现步骤:

  1. 网页放在首页,大小为铺满整个屏幕。先加载网页。
    2.列表等其他组件放在网页下面。
    3.先让WebView滚动,即整体滚动先设置为NeverScrollableScrollPhysics,禁止滚动;当WebView滚动到底的时候设置为ScrollPhysics,开启滚动。
    4.当ScrollView滚动到顶部的时候再设置回NeverScrollableScrollPhysics,让WebView滚动。
    5.就是判断时机的问题。目前加在触摸回调Listener中。监听onPointerMove和onPointerUp,在move事件中确定滚动方向, 在up事件时重新确定状态,为下次事件准备。

代码实现:
1.整个布局结构。(注意:CustomScrollView的sliver子项不要做多了,不然滑动到底部的时候会回收WebView,会丢失WebView的状态。因此采用 SliverList 来加载其他的列表数据)

@override
  Widget build(BuildContext context) {
    var physics = _physics;
    return new Scaffold(
      appBar: AppBar(
        title: Text('news details'),
      ),
      body: Listener(
        onPointerMove: (PointerMoveEvent event) {
          moveToUp = event.delta.dy < 0;
        },
        onPointerUp: (PointerUpEvent event) {
          resetScrollState();
          //惯性滑动
          Future.delayed(new Duration(milliseconds: 400), () {
            resetScrollState();
          });
        },
        child: CustomScrollView(
          physics: physics,
          slivers: <Widget>[
            SliverToBoxAdapter(
              child: htmlBodyWidget,
            ),
            SliverList(delegate: new SliverChildListDelegate(widgetList))
          ],
          controller: _scrollController,
        ),
      ),
      floatingActionButton: IconButton(
          icon: Icon(Icons.call),
          color: Colors.yellow,
          onPressed: () {
            setState(() {
              if (_physics is NeverScrollableScrollPhysics) {
                _physics = new ScrollPhysics();
              } else {
                _physics = NeverScrollableScrollPhysics();
              }
            });
          }),
    );
  }

2.滑动重新设置方法方法:

void resetScrollState() {
    print("moveToUp ---- $moveToUp");
    if (moveToUp) {
      widget.detailsWeb.canScrollDown().then((value) {
        print("moveToUp ---  canScrollDown --- $value");
        print('moveToUp ---- _physics === ${_physics.toString()}');
        if (!value) {
          if ((_physics is NeverScrollableScrollPhysics)) {
            setState(() {
              _physics = ScrollPhysics();
            });
          }
        }
      });
    } else {
      bool isScrollViewTop = _scrollController.offset <= 0;
      print(
          "moveToUp ---- isScrollViewTop = ${isScrollViewTop}");
      if (isScrollViewTop) {
        widget.detailsWeb.canScrollUp().then((value) {
          print("moveToUp ---  canScrollUp --- $value");
          print('moveToUp ---- _physics === ${_physics.toString()}');
          if (value) {
            if (!(_physics is NeverScrollableScrollPhysics)) {
              setState(() {
                _physics = NeverScrollableScrollPhysics();
              });
            }
          }
        });
      }
    }
  }
  1. 判断CustomScrollView滚动到顶部方式:
bool isScrollViewTop = _scrollController.offset <= 0;

4.判断WebView是否滚动到顶部和顶部要采用原生的判断,所以必须是异步返回。

widget.detailsWeb.canScrollUp() 返回WebView是否能向上滚动,
 widget.detailsWeb.canScrollDown() 返回webView是否能向下滚动

Android 的判断WebView还能不能滚动的源码:(参考下拉刷新的开源库)

    public static boolean canChildScrollUp(View view) {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (view instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) view;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                        .getTop() < absListView.getPaddingTop());
            } else {
                return view.getScrollY() > 0;
            }
        } else {
            return view.canScrollVertically(-1);
        }
    }

    public static boolean canChildScrollDown(View view) {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (view instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) view;
                return absListView.getChildCount() > 0
                        && (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1
                        || absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
            } else if (view instanceof ScrollView) {
                ScrollView scrollView = (ScrollView) view;
                if (scrollView.getChildCount() == 0) {
                    return false;
                } else {
                    return scrollView.getScrollY() < scrollView.getChildAt(0).getHeight() - scrollView.getHeight();
                }
            } else {
                return false;
            }
        } else {
            return view.canScrollVertically(1);
        }
    }

最终效果:


news_details.gif

完整代码:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

class NewsDetailsPage extends StatefulWidget {
  final int nid;
  DetailsWeb detailsWeb;

  NewsDetailsPage(
    int this.nid, {
    Key key,
    DetailsWeb this.detailsWeb,
  }) : super(key: key);

  @override
  NewsDetailsPageState createState() {
    return new NewsDetailsPageState();
  }
}

abstract class DetailsWeb {
  Widget createHtmlWidget(String body, List<Widget> pageWidgetContainer);

  Future<bool> canScrollUp();

  Future<bool> canScrollDown();
}

class NewsDetailsPageState extends State<NewsDetailsPage> {
  List<Widget> widgetList = [];

  Widget htmlBodyWidget;

  @override
  void initState() {
    super.initState();
    _getNewsDetails();
    _init();
  }

  @override
  void dispose() {
    super.dispose();
  }

  void _getNewsDetails() async {
    String url = 'http://www.wsrtv.com.cn/services/node/${widget.nid}.json';
    var res = await http.get(url);
    var resJson = json.decode(res.body);
    try {
      List<Widget> tempList = [];
      String title = resJson['title'];

      String titlehtml = '<h1>$title</h1>';
      Padding titleWidget = new Padding(
        padding: EdgeInsets.all(10.0),
        child: new Text(
          title,
          style: TextStyle(
            color: Colors.black,
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
          ),
        ),
      );

      int dateTimestamp = int.parse(resJson['changed']);
      print('time ---- $dateTimestamp');
      var dateTime = DateTime.fromMillisecondsSinceEpoch(dateTimestamp * 1000);
      String date = '${dateTime.year}-${dateTime.month}-${dateTime.day}';
      String count = resJson['totalcount'];

      String dateTimeInfoStr =
          '<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; color: rgb(0, 0, 0); font-size: 12px;"> $date      浏览量$count</p>';

      Widget newsDateInfo = new IntrinsicHeight(
        child: Padding(
          padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
          child: new Row(
            mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              IntrinsicWidth(
                child: Text(
                  date,
                  style: TextStyle(color: Colors.black, fontSize: 13.0),
                ),
              ),
              Expanded(
                child: new Padding(
                  padding: EdgeInsets.only(left: 20.0),
                  child: Text(
                    '浏览量$count',
                    style: TextStyle(color: Color(0xff999999), fontSize: 13.0),
                  ),
                ),
              ),
            ],
          ),
        ),
      );

      Widget headerWidget = Container(
        color: Colors.white,
        child: new Column(
          children: <Widget>[titleWidget, newsDateInfo],
        ),
      );
//      tempList.add(headerWidget);

//      try {
//        String videoUrl = resJson['field_news_video_app']['und'][0]['value'];
//        print('videoUrl === $videoUrl');
//        if (videoUrl != null && videoUrl.isNotEmpty) {
//          final playerWidget = new Chewie(
//            new VideoPlayerController.network(
//                'https://flutter.github.io/assets-for-api-docs/videos/butterfly.mp4'
//            ),
//            aspectRatio: 4 / 3,
//            autoPlay: false,
//            looping: false,
//          );
//          tempList.add(playerWidget);
//        }
//      } catch (e) {
//        print('e === ${e.toString()}');
//      }

      String bodyValue = resJson['body']['und'][0]['value'];
      String tempvalue = titlehtml + dateTimeInfoStr + bodyValue;
      Widget bodyText = widget.detailsWeb == null
          ? Container()
          : widget.detailsWeb.createHtmlWidget(tempvalue, widgetList);

      htmlBodyWidget = bodyText;

//      if (bodyText != null) {
//        tempList.add(bodyText);
//      }

      for (int i = 0; i < 120; i++) {
        tempList.add(AppBar(
          title: Text('$i$i$i$i$i$i$i$i$i$i'),
        ));
      }
      setState(() {
        widgetList = tempList;
      });
    } on Exception {}
  }

  ScrollController _scrollController;
  bool _scrollAble = true;
  ScrollPhysics _physics;

  void _init() {
    _scrollController = new ScrollController();
    _physics = NeverScrollableScrollPhysics();
    _scrollController.addListener(() {
      print('_scrollController-------------${_scrollController.toString()}');
      bool scrollAble = false;
      if (scrollAble != _scrollAble) {
        _scrollAble = scrollAble;
        print('---------------------------$_scrollAble');
        setState(() {
          print('setState ----- scrollAbleController------');
//          widgetList.add(AppBar(title: Text('aaaaaaaaaaaaa')));
//          List<Widget> temp = <Widget>[];
//          for (var item in widgetList) {
//            temp.add(item);
//          }
//          widgetList = temp;
        });
      }
    });
  }

  bool canToUp;
  bool canToDown;

  bool moveToUp = true;

  @override
  Widget build(BuildContext context) {
    var physics = _physics;
    return new Scaffold(
      appBar: AppBar(
        title: Text('news details'),
      ),
      body: Listener(
        onPointerMove: (PointerMoveEvent event) {
          print(
              'event web ---- up == ${widget.detailsWeb.canScrollUp()} ---- down =={${widget.detailsWeb.canScrollDown()}');
          moveToUp = event.delta.dy < 0;
        },
        onPointerUp: (PointerUpEvent event) {
          resetScrollState();
          //惯性滑动
          Future.delayed(new Duration(milliseconds: 400), () {
            resetScrollState();
          });
        },
        child: CustomScrollView(
          physics: physics,
          slivers: <Widget>[
            SliverToBoxAdapter(
              child: htmlBodyWidget,
            ),
            SliverList(delegate: new SliverChildListDelegate(widgetList))
          ],
          controller: _scrollController,
        ),
      ),
      floatingActionButton: IconButton(
          icon: Icon(Icons.call),
          color: Colors.yellow,
          onPressed: () {
            setState(() {
              if (_physics is NeverScrollableScrollPhysics) {
                _physics = new ScrollPhysics();
              } else {
                _physics = NeverScrollableScrollPhysics();
              }
            });
          }),
    );
  }

  void resetScrollState() {
    print("moveToUp ---- $moveToUp");
    if (moveToUp) {
      widget.detailsWeb.canScrollDown().then((value) {
        print("moveToUp ---  canScrollDown --- $value");
        print('moveToUp ---- _physics === ${_physics.toString()}');
        if (!value) {
          if ((_physics is NeverScrollableScrollPhysics)) {
            setState(() {
              _physics = ScrollPhysics();
            });
          }
        }
      });
    } else {
      bool isScrollViewTop = _scrollController.offset <= 0;
      print(
          "moveToUp ---- isScrollViewTop = ${isScrollViewTop}");
      if (isScrollViewTop) {
        widget.detailsWeb.canScrollUp().then((value) {
          print("moveToUp ---  canScrollUp --- $value");
          print('moveToUp ---- _physics === ${_physics.toString()}');
          if (value) {
            if (!(_physics is NeverScrollableScrollPhysics)) {
              setState(() {
                _physics = NeverScrollableScrollPhysics();
              });
            }
          }
        });
      }
    }
  }

  buildSlivers(List<Widget> list) {
    if (list != null) {
      Widget sliver = buildChildLayout(context, list);
      return <Widget>[sliver];
    }
    return const <Widget>[];
  }

  Widget buildChildLayout(BuildContext context, List<Widget> children) {
    return SliverList(
        delegate: new SliverChildListDelegate(
      children,
      addAutomaticKeepAlives: true,
      addRepaintBoundaries: true,
      addSemanticIndexes: true,
    ));
  }

  Widget _detailsWidget(BuildContext context, int position) {
    return widgetList[position];
  }
}

网页部分

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser_example/native_web_view.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as html;
import 'package:path_provider/path_provider.dart';

class NewsDetailsWeb extends StatefulWidget {
  String body;
  List<Widget> widgets;

  NewsDetailsWebState state;

  NewsDetailsWeb(
      {Key key, @required String this.body, List<Widget> this.widgets})
      : super(key: key);

  @override
  NewsDetailsWebState createState() {
    state = NewsDetailsWebState();
    return state;
  }

  Future<bool> canScrollUp() async {
    return state?.canScrollUp();
  }

  Future<bool> canScrollDown() async {
    return state?.canScrollDown();
  }
}

class NewsDetailsWebState extends State<NewsDetailsWeb> {
  final String fileName = 'wenshan_details.html';
  final String fileCssName = 'wenshan_details_css.css';
  String _webUrl = '';
  double top = 156.899;

  @override
  void initState() {
    super.initState();
    _createHtmlContent();
  }

  void _createHtmlContent() async {
    String cssUrl = (await _getLocalCssFile()).uri.toString();
    String cssHead =
        '''<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
    <link rel="stylesheet" type="text/css" href="${cssUrl}" />''';
    String newHtml = cssHead + widget.body;
    dom.Document doc = html.parse(newHtml);
    String htmlContent = doc.outerHtml;
    print('htmlContent === $htmlContent');
    _writeDataFile(htmlContent);
  }

  void _checkCssFile() async {
    File file = await _getLocalCssFile();
    bool isExist = await file.exists();
    int fileLength = isExist ? await file.length() : -1;
    print('csss file length === $fileLength');
    if (!isExist || fileLength <= 0) {
      if (isExist) {
        await file.delete();
      }
      await file.create();
      String cssStr = await DefaultAssetBundle.of(context)
          .loadString('assets/css/main.css');
      print('csss ==== $cssStr');
      await file.writeAsString(cssStr);
    }
  }

  void _writeDataFile(String data) async {
    _checkCssFile();
    File file = await _getLocalHtmlFile();
    File afterFile = await file.writeAsString(data);
    setState(() {
      _webUrl = afterFile.uri.toString();
    });
    print('weburl ==== $_webUrl');
  }

  Future<File> _getLocalCssFile() async {
// 获取本地文档目录
    String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
    return new File('$dir/$fileCssName');
  }

  Future<File> _getLocalHtmlFile() async {
// 获取本地文档目录
    String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
    return new File('$dir/$fileName');
  }

  @override
  Widget build(BuildContext context) {
    return getNativeWeb();
  }

  NativeWebView webView;

  Widget getNativeWeb() {
    webView = _webUrl.isNotEmpty
        ? NativeWebView(
            webUrl: _webUrl,
            webRect: Rect.fromLTWH(
                0.0,
                0.0,
                MediaQuery.of(context).size.width,
                MediaQuery.of(context).size.height -
                    AppBar().preferredSize.height -
                    MediaQuery.of(context).padding.top),
          )
        : null;
    return _webUrl.isNotEmpty
        ? webView
        : new Container(
            height: 300.0,
            color: Colors.yellow,
          );
  }

  Future<bool> canScrollUp() async {
    return webView?.canScrollUp();
  }

  Future<bool> canScrollDown() async {
    return webView?.canScrollDown();
  }
}

//WebView 插件使用flutter_inappbrowser

import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';

class NativeWebView extends StatelessWidget {
  String webUrl;
  final Rect webRect;
  InAppWebViewController webView;

  NativeWebView({Key key, this.webUrl, this.webRect}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    InAppWebView webWidget = new InAppWebView(
        initialUrl: webUrl,
        initialHeaders: {},
        initialOptions: {},
        onWebViewCreated: (InAppWebViewController controller) {
          webView = controller;
        },
        onLoadStart: (InAppWebViewController controller, String url) {
          print("started -------------- $url");
          this.webUrl = url;
        },
        onProgressChanged: (InAppWebViewController controller, int progress) {
          double prog = progress / 100;
          print('prog --------- $prog');
        });

    return Container(
      width: webRect.width,
      height: webRect.height,
      child: webWidget,
    );
  }

  Future<bool> canScrollUp() async {
    if(webView != null) {
      print('webView up ---- ${ await webView.canScrollUp()}');
    }
    return webView == null ? false : webView.canScrollUp();
  }

  Future<bool> canScrollDown() async{
    if(webView != null) {
      print('webView down ---- ${await webView.canScrollDown()}');
    }
    return webView == null ? false : webView.canScrollDown();
  }
}

调用:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';
import 'package:flutter_inappbrowser_example/news_web_page.dart';
import 'package:flutter_inappbrowser_example/news_web_use.dart';

Future main() async {
  runApp(new TestApp());
}

class TestHomeScreen extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Title"),
      ),
      body: new Center(child: new Text("Click Me")),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        backgroundColor: Colors.orange,
        onPressed: () {
          print("Clicked");
          Navigator.push(context, new MaterialPageRoute(builder: (context) {
            return new NewsDetailsPage(
              25266.toInt(),
              detailsWeb: new DetailWebUse(),
            );
          }));
        },
      ),
    );
  }

}

class TestApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: TestHomeScreen(),
    );
  }
}

class DetailWebUse implements DetailsWeb {
  NewsDetailsWeb web;

  @override
  Future<bool> canScrollDown() {
    return web?.canScrollDown();
  }

  @override
  Future<bool> canScrollUp() {
    return web?.canScrollUp();
  }

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

推荐阅读更多精彩内容