Flutter TabBar 在實際項目中的運用

Tabs在實際的項目開發中,運用的十分廣泛,此文根據在實際項目中使用整理了一個demo.再此開源,純屬技術交流,歡迎評論交流.

TabBar是flutter中非常常用的一個組件,Flutter提供的TabBar幾乎可以滿足我們大部分的業務需求,而且實現非常簡單,我們可以僅用幾行代碼,就完成一個Tab滑動效果。
關于TabBar的基本使用,我這里就不介紹了,不熟悉的朋友可以自行百度看看,有很多的Demo。

下面我們針對TabBar在平時的開發中遇到的一些問題,來看下如何解決。

一. 解決漢字滑動抖動的問題

首先,我們來看下TabBar的抖動問題,這個問題發生在我們設置labelStyleunselectedLabelStyle字體大小不一致時,這個需求其實在實際的開發當中也很常見,當我們選中一個Tab時,當然希望選中的標題能夠放大,突出一些,但是FlutterTabBar居然會在滑動過程中抖動,開始以為是Debug包的問題,后來發現Release也一樣。

Flutter的Issue中,其實已經有這樣的問題了。不過到目前為止,這個問題也沒修復,可能在老外的設計中,從來沒有這種設計吧。不過Issue中也提到了很多方案來修復這個問題,其中比較好的一個方案,就是通過修改源碼來實現,在TabBar源碼的_TabStylebuild函數中,將實現改為下面的方案。

///根據前后字體大小計算縮放倍率
final double _magnification = labelStyle!.fontSize! / unselectedLabelStyle!.fontSize!;
final double _scale = (selected ? lerpDouble(_magnification, 1, animation.value) : lerpDouble(1, _magnification, animation.value))!;

return DefaultTextStyle(
  style: textStyle.copyWith(
    color: color,
    fontSize: unselectedLabelStyle!.fontSize,
  ),
  child: IconTheme.merge(
    data: IconThemeData(
      size: 24.0,
      color: color,
    ),
    child: Transform.scale(
      scale: _scale,
      child: child,
    ),
  ),
);

這個方案的確可以修復這個問題,不過卻需要修改源碼,所以,有一些使用成本,那么有沒有其它方案呢,其實,Issue中已經給出了問題的來源,實際上就是Text在計算Scala的過程中,由于Baseline不對齊導致的抖動,所以,我們可以換一種思路,將labelStyleunselectedLabelStyle的字體大小設置成一樣的,這樣就不會抖動啦。

當然,這樣的話需求也就滿足不了了。

其實,我們是將Scala的效果,放到外面來實現,在TabBartabs中,我們將滑動百分比傳入,借助隱式動畫來實現Scala效果,就可以解決抖動的問題了。

AnimatedScale(
  scale: 1 + progress * 0.3,
  duration: const Duration(milliseconds: 100),
  child: Text(tabName),
),
最終效果圖
解決漢字滑動抖動
二. 自定義下標寬度和位置

在實際的開發中,TabBar 往往和indicator 配合在一起進行使用,現在Appindicator設計的也是五花八門,有很多的樣式。而在flutterindicator 寬度默認是不能修改的,所以可以支持修改寬度indicator 也是很必要的。flutterUnderlineTabIndicatorTab的默認實現,我們可以將UnderlineTabIndicator源碼復制出來然后取一個自己的名字如MyUnderlineTabIndicator在這個類里面修改寬度。代碼如下

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

class MyUnderlineTabIndicator extends Decoration {
  const MyUnderlineTabIndicator({
    this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
    this.insets = EdgeInsets.zero, required this.wantWidth,
  }) : assert(borderSide != null),
        assert(insets != null);

  final BorderSide borderSide;
  final EdgeInsetsGeometry insets;
  final double wantWidth;

  @override
  Decoration? lerpFrom(Decoration? a, double t) {
    if (a is MyUnderlineTabIndicator) {
      return MyUnderlineTabIndicator(
        wantWidth:5,
        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  Decoration? lerpTo(Decoration? b, double t) {
    if (b is MyUnderlineTabIndicator) {
      return MyUnderlineTabIndicator(
        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, wantWidth: 5,
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  _UnderlinePainter createBoxPainter([ VoidCallback? onChanged ]) {
    return _UnderlinePainter(this, onChanged);
  }

  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
    assert(rect != null);
    assert(textDirection != null);
    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
    //希望的寬度
    double cw = (indicator.left + indicator.right) / 2;
    return Rect.fromLTWH(cw - wantWidth / 2,
        indicator.bottom - borderSide.width, wantWidth, borderSide.width);
  }

  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(_indicatorRectFor(rect, textDirection));
  }
}

class _UnderlinePainter extends BoxPainter {
  _UnderlinePainter(this.decoration, VoidCallback? onChanged)
      : assert(decoration != null),
        super(onChanged);

  final MyUnderlineTabIndicator decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
    final Rect rect = offset & configuration.size!;
    final TextDirection textDirection = configuration.textDirection!;
    final Rect indicator = decoration._indicatorRectFor(rect, textDirection).deflate(decoration.borderSide.width / 2.0);
    final Paint paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round;
    canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
  }
}

修改indicator位置

indicatorWeight: 4,
indicatorPadding: EdgeInsets.symmetric(vertical: 8),

如果你想要indicator在垂直距離上更接近,那么可以使用indicatorPadding參數,如果你想讓indicator更細,那么可以使用indicatorWeight參數。

最終效果圖
自定義下標寬度和位置
三. 自定義下標樣式

在實際的開發中很多時候都需要自定義Indicator的樣式,剛剛修改Indicator 樣式時是將源碼UnderlineTabIndicator拷貝出來進行修改,最定義也是一樣的道理。
在源碼最后的BoxPainter,就是我們繪制Indicator的核心,在這里根據Offset和ImageConfiguration,就可以拿到當前Indicator的參數,就可以進行繪制了。

例如我們最簡單的,把Indicator繪制成一個圓,實際上只需要修改最后的draw函數,代碼如下所示。

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

class CustomUnderlineTabIndicator extends Decoration {

  const CustomUnderlineTabIndicator({
    this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
    this.insets = EdgeInsets.zero,
  }) : assert(borderSide != null),
        assert(insets != null);

  final BorderSide borderSide;

  final EdgeInsetsGeometry insets;

  @override
  Decoration? lerpFrom(Decoration? a, double t) {
    if (a is CustomUnderlineTabIndicator) {
      return CustomUnderlineTabIndicator(
        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  Decoration? lerpTo(Decoration? b, double t) {
    if (b is CustomUnderlineTabIndicator) {
      return CustomUnderlineTabIndicator(
        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
    return _UnderlinePainter(this, onChanged);
  }

  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
    assert(rect != null);
    assert(textDirection != null);
    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
    return Rect.fromLTWH(
      indicator.left,
      indicator.bottom - borderSide.width,
      indicator.width,
      borderSide.width,
    );
  }

  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(_indicatorRectFor(rect, textDirection));
  }
}

class _UnderlinePainter extends BoxPainter {
  _UnderlinePainter(this.decoration, VoidCallback? onChanged)
      : assert(decoration != null),
        super(onChanged);

  final CustomUnderlineTabIndicator decoration;
  final Paint _paint = Paint()
    ..color = Colors.orange
    ..style = PaintingStyle.fill;
  final radius = 6.0;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    assert(configuration != null);
    assert(configuration.size != null);
    final Rect rect = offset & configuration.size!;
    canvas.drawCircle(
      Offset(rect.bottomCenter.dx, rect.bottomCenter.dy - radius),
      radius,
      _paint,
    );
  }
}
最終效果圖
自定義下標樣式
四. 自定義背景塊樣式

在開發中有時候會遇到帶背景塊的tabbar,很簡單flutter提供有這個類ShapeDecoration可以用來實現這個效果。

indicator: ShapeDecoration(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(8),
  ),
  color: Colors.cyan.shade200,
)
最終效果圖
自定義背景塊樣式
五. 動態獲取tab

在實際項目開發中,一般這些tab都是通過后臺接口返回的,重點是接口返回是異步的,需要在數據未返回時進行判斷返回一個空的Widget。不難實現,直接上代碼了。

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../config/Http_service.dart';
import '../model/cook_info_model.dart';
import 'my_underline_tabIndicator.dart';

class DynamicDataTab extends StatefulWidget {
  final String titleStr;
  const DynamicDataTab({Key? key, required this.titleStr}) : super(key: key);

  @override
  State<DynamicDataTab> createState() => _DynamicDataTabState();
}

class _DynamicDataTabState extends State<DynamicDataTab>
    with SingleTickerProviderStateMixin {
  TabController? _tabController;
  List<CookInfoModel> _cookInfoList = CookInfoModelList([]).list;

  // 獲取數據
  Future _getRecommendData() async {
    EasyLoading.show(status: 'loading...');
    try {
      Map<String, dynamic> result =
          await HttpService.getHomeRecommendData(page: 1, pageSize: 10);
      EasyLoading.dismiss();
      List list = [];
      for (Map item in result['result']['list']) {
        list.add(item['r']);
        print(item['r']);
      }
      CookInfoModelList infoList = CookInfoModelList.fromJson(list);
      setState(() {
        _tabController =
            TabController(length: infoList.list.length, vsync: this);
        _cookInfoList = infoList.list;
      });
    } catch (e) {
      print(e);
      EasyLoading.dismiss();
    } finally {
      EasyLoading.dismiss();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.titleStr),
      ),
      body: Container(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        padding: const EdgeInsets.only(top: 20),
        color: Colors.white,
        child: Column(
          children: [
            _cookInfoList.isEmpty
                ? PreferredSize(
                    preferredSize: const Size(0, 0), child: Container())
                : TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.blue,
                    indicatorWeight: 18,
                    isScrollable: true,
                    indicatorPadding: const EdgeInsets.symmetric(vertical: 6),
                    indicator: const MyUnderlineTabIndicator(
                        wantWidth: 30.0,
                        borderSide: BorderSide(
                            width: 6.0,
                            color: Color.fromRGBO(36, 217, 252, 1))),
                    tabs: getTabs()
                        .asMap()
                        .entries
                        .map(
                          (entry) => AnimatedBuilder(
                            animation: _tabController!.animation!,
                            builder: (ctx, snapshot) {
                              final forward = _tabController!.offset > 0;
                              final backward = _tabController!.offset < 0;
                              int _fromIndex;
                              int _toIndex;
                              double progress;
                              // Tab
                              if (_tabController!.indexIsChanging) {
                                _fromIndex = _tabController!.previousIndex;
                                _toIndex = _tabController!.index;
                                progress = (_tabController!.animation!.value -
                                            _fromIndex)
                                        .abs() /
                                    (_toIndex - _fromIndex).abs();
                              } else {
                                // Scroll
                                _fromIndex = _tabController!.index;
                                _toIndex = forward
                                    ? _fromIndex + 1
                                    : backward
                                        ? _fromIndex - 1
                                        : _fromIndex;
                                progress = (_tabController!.animation!.value -
                                        _fromIndex)
                                    .abs();
                              }
                              var flag = entry.key == _fromIndex
                                  ? 1 - progress
                                  : entry.key == _toIndex
                                      ? progress
                                      : 0.0;
                              return buildTabContainer(
                                  entry.value.text ?? '', flag);
                            },
                          ),
                        )
                        .toList(),
                  ),
            Expanded(
                child: _cookInfoList.isEmpty
                    ? PreferredSize(
                        preferredSize: const Size(0, 0), child: Container())
                    : TabBarView(
                        controller: _tabController, children: getWidgets()))
          ],
        ),
      ),
    );
  }

  List<Tab> getTabs() {
    List<Tab> widgetList = [];
    for (int i = 0; i < _cookInfoList.length; i++) {
      CookInfoModel model = _cookInfoList[i];
      if(model.stdname!.length > 5){
        model.stdname = model.stdname?.substring(0,5);
      }
      widgetList.add(Tab(text: model.stdname!.isNotEmpty ? model.stdname : '暫無數據'));
    }
    return widgetList;
  }

  List<Widget> getWidgets() {
    List<Widget> widgetList = [];
    for (int i = 0; i < _cookInfoList.length; i++) {
      CookInfoModel model = _cookInfoList[i];
      widgetList.add(
        Container(
          padding: const EdgeInsets.only(left: 20, right: 20, top: 10),
          child: SingleChildScrollView(
            child: Column(
              children: [
                Container(
                  width: MediaQuery.of(context).size.width,
                  clipBehavior: Clip.hardEdge,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(6),
                    color: Colors.white,
                  ),
                  child: CachedNetworkImage(
                    imageUrl: model.img??"",
                    width: MediaQuery.of(context).size.width,
                    fit: BoxFit.fitWidth,
                  ),
                ),
                Text(
                  model.n ?? '',
                  style: const TextStyle(fontSize: 14, color: Colors.black54),
                )
              ],
            ),
          )
        ),
      );
    }
    return widgetList;
  }

  buildTabContainer(String tabName, double alpha) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1),
      child: AnimatedScale(
        scale: 1 + double.parse((alpha * 0.2).toStringAsFixed(2)),
        duration: const Duration(milliseconds: 100),
        child: Text(
          tabName,
          style: const TextStyle(
              fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black),
        ),
      ),
    );
  }
}
最終效果圖
動態獲取tab
六. 動態獲取tab和tab懸停

動態獲取tab同案例五一樣,懸停是通過NestedScrollViewSliverAppBar來實現的,原理不復雜,不直接上代碼。

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../widget/banner.dart';
import '../config/Http_service.dart';
import '../model/banner_model.dart';
import '../model/cook_info_model.dart';
import 'my_underline_tabIndicator.dart';

class DynamicDataHover extends StatefulWidget {
  final String titleStr;
  const DynamicDataHover({Key? key, required this.titleStr}) : super(key: key);

  @override
  State<DynamicDataHover> createState() => _DynamicDataHoverState();
}

class _DynamicDataHoverState extends State<DynamicDataHover>
    with SingleTickerProviderStateMixin {
  TabController? _tabController;
  List<CookInfoModel> _cookInfoList = CookInfoModelList([]).list;

  /// 輪播圖數據
  List<BannerModel> _bannerList = BannerModelList([]).list;

  // 獲取數據
  Future _getRecommendData() async {
    EasyLoading.show(status: 'loading...');
    try {
      Map<String, dynamic> result =
          await HttpService.getHomeRecommendData(page: 1, pageSize: 10);
      EasyLoading.dismiss();

      /// 輪播圖數據
      BannerModelList bannerModelList =
          BannerModelList.fromJson(result['result']['banner']);
      print('哈哈哈哈哈或$result');
      List list = [];
      for (Map item in result['result']['list']) {
        list.add(item['r']);
        print(item['r']);
      }
      CookInfoModelList infoList = CookInfoModelList.fromJson(list);
      setState(() {
        _tabController =
            TabController(length: infoList.list.length, vsync: this);
        _cookInfoList = infoList.list;
        _bannerList = bannerModelList.list;
      });
    } catch (e) {
      print(e);
      EasyLoading.dismiss();
    } finally {
      EasyLoading.dismiss();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.titleStr),
      ),
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              backgroundColor: Colors.white,
              elevation: 0,
              pinned: true,
              floating: true,
              /// 去掉返回按鈕
              leading: const Text(''),
              expandedHeight: 180,
              flexibleSpace: FlexibleSpaceBar(
                collapseMode: CollapseMode.pin,
                background: Container(
                  color: Colors.white,
                  height: double.infinity,
                  child: Column(
                    children: <Widget>[
                      Container(
                        height: 120,
                        width: MediaQuery.of(context).size.width,
                        color: Colors.blue,
                        child: BannerView(
                          bannerList: _bannerList,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              bottom: _cookInfoList.isEmpty
                  ? PreferredSize(
                      preferredSize: const Size(0, 0), child: Container())
                  : TabBar(
                      controller: _tabController,
                      indicatorColor: Colors.blue,
                      indicatorWeight: 18,
                      isScrollable: true,
                      indicatorPadding:
                          const EdgeInsets.symmetric(vertical: 6),
                      indicator: const MyUnderlineTabIndicator(
                          wantWidth: 30.0,
                          borderSide: BorderSide(
                              width: 6.0,
                              color: Color.fromRGBO(36, 217, 252, 1))),
                      tabs: getTabs()
                          .asMap()
                          .entries
                          .map(
                            (entry) => AnimatedBuilder(
                              animation: _tabController!.animation!,
                              builder: (ctx, snapshot) {
                                final forward = _tabController!.offset > 0;
                                final backward = _tabController!.offset < 0;
                                int _fromIndex;
                                int _toIndex;
                                double progress;
                                // Tab
                                if (_tabController!.indexIsChanging) {
                                  _fromIndex = _tabController!.previousIndex;
                                  _toIndex = _tabController!.index;
                                  progress =
                                      (_tabController!.animation!.value -
                                                  _fromIndex)
                                              .abs() /
                                          (_toIndex - _fromIndex).abs();
                                } else {
                                  // Scroll
                                  _fromIndex = _tabController!.index;
                                  _toIndex = forward
                                      ? _fromIndex + 1
                                      : backward
                                          ? _fromIndex - 1
                                          : _fromIndex;
                                  progress =
                                      (_tabController!.animation!.value -
                                              _fromIndex)
                                          .abs();
                                }
                                var flag = entry.key == _fromIndex
                                    ? 1 - progress
                                    : entry.key == _toIndex
                                        ? progress
                                        : 0.0;
                                return buildTabContainer(
                                    entry.value.text ?? '', flag);
                              },
                            ),
                          )
                          .toList(),
                    ),
            )
          ];
        },
        body: _cookInfoList.isEmpty
            ? PreferredSize(
                preferredSize: const Size(0, 0), child: Container())
            : TabBarView(controller: _tabController, children: getWidgets()),
      ),
    );
  }

  List<Tab> getTabs() {
    List<Tab> widgetList = [];
    for (int i = 0; i < _cookInfoList.length; i++) {
      CookInfoModel model = _cookInfoList[i];
      if (model.stdname!.length > 5) {
        model.stdname = model.stdname?.substring(0, 5);
      }
      widgetList.add(Tab(text: model.stdname!.isNotEmpty ? model.stdname : '暫無數據'));
    }
    return widgetList;
  }

  List<Widget> getWidgets() {
    List<Widget> widgetList = [];
    for (int i = 0; i < _cookInfoList.length; i++) {
      CookInfoModel model = _cookInfoList[i];
      widgetList.add(
        Container(
            padding: const EdgeInsets.only(left: 20, right: 20, top: 10,bottom: 15),
            child: SingleChildScrollView(
              child: Column(
                children: [
                  Container(
                    width: MediaQuery.of(context).size.width,
                    clipBehavior: Clip.hardEdge,
                    height: 200,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(6),
                      color: Colors.white,
                    ),
                    child: CachedNetworkImage(
                      imageUrl: model.img ?? "",
                      width: MediaQuery.of(context).size.width,
                      fit: BoxFit.fitWidth,
                      height: 200,
                    ),
                  ),
                  const SizedBox(height: 15,),
                  Text(
                    model.n ?? '',
                    style: const TextStyle(fontSize: 14, color: Colors.black54),
                  )
                ],
              ),
            )),
      );
    }
    return widgetList;
  }

  buildTabContainer(String tabName, double alpha) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1),
      child: AnimatedScale(
        scale: 1 + double.parse((alpha * 0.2).toStringAsFixed(2)),
        duration: const Duration(milliseconds: 100),
        child: Text(
          tabName,
          style: const TextStyle(
              fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black),
        ),
      ),
    );
  }
}
最終效果圖
動態獲取tab和tab懸停
七. 動態獲取tab+tab懸停+下拉刷新上拉加載
最終效果圖
動態獲取tab+tab懸停+下拉刷新上拉加載
八. tab嵌套tab
最終效果圖
tab嵌套tab
九. tab自由布局
tab自由布局
項目地址請移步: 項目地址
Flutter timer的使用: 項目地址
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容