Flutter開發(fā)實戰(zhàn)分析-animation_demo瞎復寫

以下代碼基本參考于 flutter_gallery中的animation_demo示例。(可以結合本文看源碼)

題外話:這個demo是最炫酷的了

animation.gif

這里的動畫效果我們看到:

  1. 有一個多頁的滾動
  2. 滑到上下滑到將近一半瞪醋,會有一個粘性效果本砰,吸附到一半刨晴。再往上薇芝,就正撑恚滑動务傲。
    3.一半往上,下面的白色標簽開始發(fā)生位移枣申。一半往下售葡,整個4個卡片發(fā)生位移。

簡單的分析一下

  1. 上下滾動忠藤,并且自定義動畫效果挟伙。嗯。上一遍文章的CustomScrollView

  2. 左右滾動模孩,切換頁面尖阔。嗯。PageView榨咐。
    PageView可以讓像是一頁一頁滑動介却。而且每個頁面的大小是一樣的。
    使用PageController
    來進行控制块茁。

  3. 上下要同時切換筷笨。肯定也需要上下兩個PageView的狀態(tài)同步龟劲。

第一次接觸

  • 先準備好數據胃夏。查看sections.dart〔可以不管仰禀,先復制過來。

  • 初始化布局蚕愤。
    像是大體想象的框架應該是CustomScrollView.然后初始的SliveAppBar的高度應該是屏幕的高度答恶。SliveAppBar的child是PageView
    下面是一個SliveToBoxAdapter里面也放著PageView.

  • 代碼
    按照我們初步的想法,代碼如下

import 'package:flutter/material.dart';
import 'package:flutter_start/demo/animation/sections.dart';

Color _kAppBackgroundColor = const Color(0xFF353662);
Duration _kScrollDuration = const Duration(milliseconds: 400);
Curve _kScrollCurve = Curves.fastOutSlowIn;

class AnimationDemoHome extends StatefulWidget {
  const AnimationDemoHome({Key key}) : super(key: key);

  static const String routeName = '/animation';

  @override
  _AnimationDemoHomeState createState() => new _AnimationDemoHomeState();
}

class _AnimationDemoHomeState extends State<AnimationDemoHome> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: _kAppBackgroundColor,
      body: new Builder(
        // Insert an element so that _buildBody can find the PrimaryScrollController.
        builder: _buildBody,
      ),
    );
  }

  Widget _buildBody(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    return new SizedBox.expand(
      child: new Stack(
        children: <Widget>[
          new CustomScrollView(
            slivers: <Widget>[
              SliverAppBar(
                expandedHeight: height,
                flexibleSpace: LayoutBuilder(builder:
                    (BuildContext context, BoxConstraints constraints) {
                  return PageView(
                    children: allSections.map((Section section) {
                      return _headerItemsFor(section);
                    }).toList(),
                  );
                }),
              ),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 610.0,
                  child: PageView(
                    children: allSections.map((Section section) {
                      return Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: _detailItemsFor(section).toList(),
                      );
                    }).toList(),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Iterable<Widget> _detailItemsFor(Section section) {
    final Iterable<Widget> detailItems =
        section.details.map((SectionDetail detail) {
      return new SectionDetailView(detail: detail);
    });
    return ListTile.divideTiles(context: context, tiles: detailItems);
  }

  Widget _headerItemsFor(Section section) {
    return SectionCard(section: section);
  }
}

class SectionDetailView extends StatelessWidget {
  SectionDetailView({Key key, @required this.detail})
      : assert(detail != null && detail.imageAsset != null),
        assert((detail.imageAsset ?? detail.title) != null),
        super(key: key);

  final SectionDetail detail;

  @override
  Widget build(BuildContext context) {
    final Widget image = new DecoratedBox(
      decoration: new BoxDecoration(
        borderRadius: new BorderRadius.circular(6.0),
        image: new DecorationImage(
          image: new AssetImage(
            detail.imageAsset,
            package: detail.imageAssetPackage,
          ),
          fit: BoxFit.cover,
          alignment: Alignment.center,
        ),
      ),
    );

    Widget item;
    if (detail.title == null && detail.subtitle == null) {
      item = new Container(
        height: 240.0,
        padding: const EdgeInsets.all(16.0),
        child: new SafeArea(
          top: false,
          bottom: false,
          child: image,
        ),
      );
    } else {
      item = new ListTile(
        title: new Text(detail.title),
        subtitle: new Text(detail.subtitle),
        leading: new SizedBox(width: 32.0, height: 32.0, child: image),
      );
    }

    return new DecoratedBox(
      decoration: new BoxDecoration(color: Colors.grey.shade200),
      child: item,
    );
  }
}

class SectionCard extends StatelessWidget {
  const SectionCard({Key key, @required this.section})
      : assert(section != null),
        super(key: key);

  final Section section;

  @override
  Widget build(BuildContext context) {
    return new Semantics(
      label: section.title,
      button: true,
      child: new DecoratedBox(
        decoration: new BoxDecoration(
          gradient: new LinearGradient(
            begin: Alignment.centerLeft,
            end: Alignment.centerRight,
            colors: <Color>[
              section.leftColor,
              section.rightColor,
            ],
          ),
        ),
        child: new Image.asset(
          section.backgroundAsset,
          package: section.backgroundAssetPackage,
          color: const Color.fromRGBO(255, 255, 255, 0.075),
          colorBlendMode: BlendMode.modulate,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}
  • 效果
target-20180814110704.gif

發(fā)現我們的想法還是有一定偏差的萍诱。上面的頭部部分悬嗓,不只是pageView,它需要從一個list然后移動變成pageView.

CustomMultiChildLayout

這個Widget可以完全自己掌控布局的排列。我們需要做的是將它的自組件都傳遞給他裕坊,然后實現它的方法包竹,就可以完全的掌握自己的布局了。
使用它有兩個關鍵點:

  1. 自定義MultiChildLayoutDelegate來自己實現布局
  2. 他的每個child都需要用layoutId來包裹,并且分配給他們的id周瞎,都必須是唯一的苗缩。

按照這個思路,我們希望每一個Page都是能實現這個樣的動畫效果声诸,所以我們自己定義CustomMultiChildLayout作為PageView的child酱讶。
同時,我們還需要將之前的4個SectionsCard用LayoutId包裹后彼乌,傳入其中泻肯。

  • 自定義實現MultiChildLayoutDelegate
class _AllSectionsLayout extends MultiChildLayoutDelegate {
  int cardCount = 4;
  double selectedIndex = 0.0;
  double tColumnToRow = 0.0;
 ///Alignment(-1.0, -1.0) 表示矩形的左上角。
  ///Alignment(1.0, 1.0) 代表矩形的右下角慰照。
  Alignment translation = new Alignment(0 * 2.0 - 1.0, -1.0);
  _AllSectionsLayout({this.tColumnToRow,this.selectedIndex,this.translation});

  @override
  void performLayout(Size size) {
    //初始值
    //豎向布局時
    //卡片的left
    final double columnCardX = size.width / 5.0;
    //卡片的寬度Width
    final double columnCardWidth = size.width - columnCardX;
    //卡片的高度
    final double columnCardHeight = size.height / cardCount;
    //橫向布局時
    final double rowCardWidth = size.width;

    final Offset offset = translation.alongSize(size);

    double columnCardY = 0.0;
    double rowCardX = -(selectedIndex * rowCardWidth);

    for (int index = 0; index < cardCount; index++) {
      // Layout the card for index.
      final Rect columnCardRect = new Rect.fromLTWH(
          columnCardX, columnCardY, columnCardWidth, columnCardHeight);
      final Rect rowCardRect =
          new Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height);
      //  定義好初始的位置和結束的位置灶挟,就可以使用這個lerp函數,輕松的找到中間狀態(tài)值
      //rect 的 shift 焚挠,相當于 offset的translate 
      final Rect cardRect =
          _interpolateRect(columnCardRect, rowCardRect).shift(offset);
      final String cardId = 'card$index';
      if (hasChild(cardId)) {
        layoutChild(cardId, new BoxConstraints.tight(cardRect.size));
        positionChild(cardId, cardRect.topLeft);
      }

      columnCardY += columnCardHeight;
      rowCardX += rowCardWidth;
    }
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    print('oldDelegate=$oldDelegate');
    return false;
  }

  Rect _interpolateRect(Rect begin, Rect end) {
    return Rect.lerp(begin, end, tColumnToRow);
  }

  Offset _interpolatePoint(Offset begin, Offset end) {
    return Offset.lerp(begin, end, tColumnToRow);
  }
}
定義整個動畫過程

整個動畫效果就是膏萧,從豎排的4列,變化成橫排的4列蝌衔。
為每個card定義好

動畫的初始

card的初始狀態(tài)column為前綴的變量榛泛。

  • 高度
    就是按照我們看到的,豎排的情況下噩斟,每個Card的高度是整個appBar高度的4分之一曹锨。
  • left
    統一的位置。
  • 寬度
    去掉left部分的剃允,寬度
  • Offset
    Offset需要確定的位置沛简,需要和選定的坐標協同。選定的Index斥废,畢竟出現在當前位置椒楣。就是他的Offset的x,必須和自己的left相反牡肉,這樣才能在第一個位置捧灰。
    它是用Aligment.alongSize來進行轉換。Alignment(-1.0, -1.0)就代表左上角统锤。Alignment(1.0, 1.0)代表矩形的右下角毛俏。整個Aligment相當于一個邊長為2,中心點在原點的正方形饲窿。
    需要讓index== selectedIndex的card的Aligment為左上角Alignment(1.0, 1.0)的狀態(tài)煌寇。然后其他對應的進行偏移。
動畫的結尾

card的最終狀態(tài)row為前綴的變量

  • 高度
    就是整個的高度

  • left
    就是選中card的偏移量逾雄。

  • 寬度
    就是整個的寬度

  • offset
    同上阀溶。

確定中間狀態(tài)
  • tColumnToRow
    整體的動畫腻脏,在Flutter中有很方便的lerp函數可以確定中間的狀態(tài)。只要傳入我們進度的百分比就可以淌哟。這個百分比可以由滑動的過程中的offset傳入迹卢。

SliverAppBar

//只顯示sliverAppBar部分
 slivers: <Widget>[
            NotificationListener<ScrollNotification>(
              onNotification: (ScrollNotification notification) {
                return _handleScrollNotification(
                    notification, appBarMidScrollOffset);
              },
              child: SliverAppBar(
                backgroundColor: _kAppBackgroundColor,
                expandedHeight: height - statusHeight,
                bottom: PreferredSize(
                  preferredSize:
                      const Size.fromHeight(_kAppBarMinHeight - kToolbarHeight),
                  child: Container(width: 0.0, height: 0.0),
                ),
                pinned: true,
                    //同樣根據上一節(jié)我們學習到的內容辽故,我們可以通過layoutbuilder來獲取變化的約束
                flexibleSpace: LayoutBuilder(builder:
                    (BuildContext context, BoxConstraints constraints) {
                //因為發(fā)現當滾動成column時徒仓,上面有statusBar高度的padding,當變成row時,整個padding就變成0誊垢,所以這里是這個的變化值
                  double t =
                      1.0 - (height - constraints.maxHeight) / (height * 0.3);
                  final Curve statusBarHeightCurve =
                      Interval(0.0, 1.0, curve: Curves.fastOutSlowIn);
                  double extraPaddingTop =
                      statusHeight * statusBarHeightCurve.transform(t.clamp(0.0, 1.0));
                  
                  //這里開始計算 tColumnToRow的比例掉弛。其實就是滾動的距離。
                  final Size size = constraints.biggest;
                  final double tColumnToRow = 1.0 -
                      ((size.height - _kAppBarMidHeight) /
                              (height - statusHeight - _kAppBarMidHeight))
                          .clamp(0.0, 1.0);

                  final List<Widget> sectionCards = <Widget>[];

                  for (int index = 0; index < allSections.length; index++) {
                    Section section = allSections[index];
                    sectionCards.add(_headerItemsFor(section));
                  }
                  List<Widget> children = [];
                  for (int index = 0; index < sectionCards.length; index++) {
                    //這里一定要注意喂走,   CustomMultiChildLayout中的殃饿,子節(jié)點,都必須用LayoutId來包裹S蟪Α:醴肌!
                    children.add(new LayoutId(
                      id: 'card$index',
                      child: sectionCards[index],
                    ));
                  }

                  List<Widget> layoutChildren = [];

                  print('selectedIndex.value=${selectedIndex.value}');
                  for (int index = 0; index < sectionCards.length; index++) {
                    layoutChildren.add(new CustomMultiChildLayout(
                      delegate: _AllSectionsLayout(
                          tColumnToRow: tColumnToRow,
                          translation: new Alignment(
                              (selectedIndex.value - index) * 2.0 - 1.0, -1.0),
                          selectedIndex: selectedIndex.value
                          ),
                      children: children,
                    ));
                  }
                  //將上面的用PageView再包裹一次帖池。
                  return Padding(
                      padding: EdgeInsets.only(top: extraPaddingTop),
                      child: PageView(
                        physics: _headingScrollPhysics,
                        controller: _headingPageController,
                        children: layoutChildren,
                      ),
                    ),
                  );
                }),
              ),
            ),

上面這段代碼奈惑,有下面幾個重點

  • SliverAppBar的bottom
    因為我們使用Pinned屬性。這個屬性會懸浮我們的AppBar在頂部睡汹。但是如果默認情況下肴甸,這時appBar的高度就是有56邏輯像素這樣。所以囚巴,我們需要添加一個bottom原在,讓它,增加到我們想要的最后高度彤叉。

  • 調整整體的padding
    從動畫效果可以看到庶柿,padding有一個從有到無的狀態(tài),當從column變成row的過程中秽浇,所以我們要對其進行計算浮庐。

  • 計算tColumnToRow
    這個值也是根據我們滑動的整體狀態(tài)來計算的。

  • LayoutId
    這個一定要記准婧恰兔辅!
    CustomMultiChildLayout中的,子節(jié)點击喂,都必須用LayoutId來包裹NΑ!懂昂!

然后介时,我還要處理兩個細節(jié)。

一個是當滾動到中間位置后,就不能左右切換了沸柔。
  • 監(jiān)聽
    將NotificationListener包裹在pageView之外循衰,就可以監(jiān)聽PageView的滾動事件了。
//省略代碼...
 NotificationListener<ScrollNotification>(
                    onNotification: (ScrollNotification notification) {
                      return _handlePageNotification(notification,
                          _headingPageController, _detailsPageController);
                    },
                    child: Padding(
                      padding: EdgeInsets.only(top: extraPaddingTop),
                      child: PageView(
                        physics: _headingScrollPhysics,
                        controller: _headingPageController,
                        children: layoutChildren,
                      ),
                    ),
                  );
  • 切換
    這個需要監(jiān)聽褐澎,滾動的事件会钝,當滾動的距離得到一般之后,就將PageView的physics改為NeverScrollableScrollPhysics工三。它將會導致頁面不能滾動迁酸。
    反之,就設置為PageScrollPhysics().像頁面一樣滾動俭正。

 bool _handleScrollNotification(
      ScrollNotification notification, double midScrollOffset) {
    if (notification.depth == 0 && notification is ScrollUpdateNotification) {
      final ScrollPhysics physics =
          _scrollController.position.pixels >= midScrollOffset
              ? const PageScrollPhysics()
              : const NeverScrollableScrollPhysics();
      if (physics != _headingScrollPhysics) {
        setState(() {
          _headingScrollPhysics = physics;
        });
      }
    }
    return false;
  }
當快滾動中間位置時奸鬓,會有一個粘性的效果

這個效果是整個SliverAppBar來提供的。所以設置他的physics掸读。
當滾動的距離大于一辦時串远,判斷對應的滾動反向,來創(chuàng)造對應simulation

class _SnappingScrollPhysics extends ClampingScrollPhysics {
  const _SnappingScrollPhysics({
    ScrollPhysics parent,
    @required this.midScrollOffset,
  })  : assert(midScrollOffset != null),
        super(parent: parent);

  final double midScrollOffset;

  @override
  _SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
    return new _SnappingScrollPhysics(
        parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
  }

  Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
    final double velocity = math.max(dragVelocity, minFlingVelocity);
    return new ScrollSpringSimulation(spring, offset, midScrollOffset, velocity,
        tolerance: tolerance);
  }

  Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
    final double velocity = math.max(dragVelocity, minFlingVelocity);
    return new ScrollSpringSimulation(spring, offset, 0.0, velocity,
        tolerance: tolerance);
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double dragVelocity) {
    final Simulation simulation =
        super.createBallisticSimulation(position, dragVelocity);
    final double offset = position.pixels;

    if (simulation != null) {
      // The drag ended with sufficient velocity to trigger creating a simulation.
      // If the simulation is headed up towards midScrollOffset but will not reach it,
      // then snap it there. Similarly if the simulation is headed down past
      // midScrollOffset but will not reach zero, then snap it to zero.
      final double simulationEnd = simulation.x(double.infinity);
      if (simulationEnd >= midScrollOffset) return simulation;
      if (dragVelocity > 0.0)
        return _toMidScrollOffsetSimulation(offset, dragVelocity);
      if (dragVelocity < 0.0)
        return _toZeroScrollOffsetSimulation(offset, dragVelocity);
    } else {
      // The user ended the drag with little or no velocity. If they
      // didn't leave the offset above midScrollOffset, then
      // snap to midScrollOffset if they're more than halfway there,
      // otherwise snap to zero.
      final double snapThreshold = midScrollOffset / 2.0;
      if (offset >= snapThreshold && offset < midScrollOffset)
        return _toMidScrollOffsetSimulation(offset, dragVelocity);
      if (offset > 0.0 && offset < snapThreshold)
        return _toZeroScrollOffsetSimulation(offset, dragVelocity);
    }
    return simulation;
  }
}

運行效果


target-20180814215213.gif

這樣儿惫,我們就做成很接近最后效果的動畫了澡罚。要實現最后的動畫,只要用相同的辦法去創(chuàng)建titleindicator就行了姥闪。

總結

雖然我們的代碼始苇,和animation_demo源碼中的代碼有所不一樣。但是核心是一樣的筐喳。
這邊文章我們熟悉了

CustomScrollViewMultiChildLayoutDelegate

通過CustomScrollViewMultiChildLayoutDelegateperformLayout方法的實現催式,來完成自定義的多組件之間的布局。

自定義動畫的過程

自定義動畫的過程避归,在Flutter中其實相對簡單荣月。提供了很多幫助的計算方式。需要做的是確定要初始值梳毙,和最終值哺窄,中間的過度變量可以考慮使用lerp就可以完成。

監(jiān)聽事件

之前的文章账锹,我們分析過Flutter中數據的傳遞萌业。需要監(jiān)聽發(fā)送的ScrollEvent,我們只要在我們監(jiān)聽的Widget的外層奸柬,套一層NotificationListener進行監(jiān)聽就好

ScrollView的要素

我們更加熟悉了ScrollView的兩個要素生年。controllerphysics

  • controller
    我們可以得到滾動的狀態(tài)廓奕,和控制滾動的情況抱婉。
  • physics
    滾動的效果档叔。我們可以添加NeverScrollableScrollPhysics。這樣就不滾動了蒸绩。添加PageScrolPhysics衙四,這樣就是按照頁面滾動。添加BounceScrollPhysics,就實現ios中的彈性滾動了患亿。

好的传蹈。這邊文章,我們就暫時到這里窍育。
下一遍文章卡睦,我們先介紹一個Flutter中整體的視圖樹,然后回顧一下我們遇到過的組件宴胧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末漱抓,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子恕齐,更是在濱河造成了極大的恐慌乞娄,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件显歧,死亡現場離奇詭異仪或,居然都是意外死亡,警方通過查閱死者的電腦和手機士骤,發(fā)現死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門范删,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拷肌,你說我怎么就攤上這事到旦。” “怎么了巨缘?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵添忘,是天一觀的道長。 經常有香客問我若锁,道長搁骑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任又固,我火速辦了婚禮仲器,結果婚禮上,老公的妹妹穿的比我還像新娘仰冠。我一直安慰自己乏冀,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布沪停。 她就那樣靜靜地躺著煤辨,像睡著了一般裳涛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上众辨,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天端三,我揣著相機與錄音,去河邊找鬼鹃彻。 笑死郊闯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的蛛株。 我是一名探鬼主播团赁,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谨履!你這毒婦竟也來了欢摄?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笋粟,失蹤者是張志新(化名)和其女友劉穎怀挠,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體害捕,經...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡绿淋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了尝盼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吞滞。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盾沫,靈堂內的尸體忽然破棺而出裁赠,到底是詐尸還是另有隱情,我是刑警寧澤疮跑,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布组贺,位于F島的核電站,受9級特大地震影響祖娘,放射性物質發(fā)生泄漏失尖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一渐苏、第九天 我趴在偏房一處隱蔽的房頂上張望掀潮。 院中可真熱鬧,春花似錦琼富、人聲如沸仪吧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽薯鼠。三九已至择诈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間出皇,已是汗流浹背羞芍。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留郊艘,地道東北人荷科。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓跪妥,卻偏偏與公主長得像讶凉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烦秩,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容

  • 1狞贱、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數據庫組件 SD...
    陽明先生_X自主閱讀 15,982評論 3 119
  • 入門介紹完刻获,今天我們,先來分析幾個官方提供的示例斥滤。 以下代碼基本參考于 flutter_gallery中的pest...
    deep_sadness閱讀 2,795評論 0 11
  • 發(fā)現 關注 消息 iOS 第三方庫将鸵、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,107評論 4 62
  • 今天發(fā)現自己很優(yōu)秀 今天發(fā)現自己裝得很優(yōu)秀 今天發(fā)現自己努力裝得很優(yōu)秀 今天發(fā)現自己努力裝得很優(yōu)秀其實是因為沒有勇...
    Looloo閱讀 178評論 0 2
  • 故事一 01 崗亭外 奶瓶摔落草娜,空氣凝固挑胸。 王娟老公:(憤怒無奈略帶小心語氣)不喝了!家都不要了宰闰!喝什么喝茬贵! 一滴...
    麻花6閱讀 170評論 0 0