Flutter了解之入门篇5(常用组件---文本、按钮、图片)

目录
  1. 文本(Text组件、TextSpan组件)
  2. 按钮
  3. 图片、ICON
  4. MaterialApp(用于快速搭建APP框架)
  5. Scaffold (用于快速搭建页面)

1. 文本

  1. Text (用于显示文本)
  // 属于基础组件库
  Text(
    String this.data, {
    Key? key,
    // 文本的显示样式(如颜色、字体、粗细、背景等 见下面注释)
    this.style,
/*
TextStyle
  1. height:行高=fontSize*height行高因子。
  2. fontFamily :不同平台默认支持的字体集不同,指定字体时需在各平台测试。
  3. fontSize:该属性和Text的textScaleFactor都用于控制字体大小。区别:
    1. fontSize可以精确指定字体大小,而textScaleFactor只能通过缩放比例来控制。
    2. fontSize用于单个文本且字体大小不会随系统字体大小变化,而textScaleFactor用于系统字体大小设置改变时对应用进行全局调整。
  4. color:字体颜色
  5. fontWeight:字体粗细。FontWeight.w800
  6. fontStyle:字体样式。FontStyle.italic 倾斜
  7. decoration:文本装饰线(none没有线,lineThrough删除线,overline上划线,underline下滑线)
  8. decorationColor:文本装饰线颜色
  9. decorationStyle:文本装饰线样式([dashed,dotted]虚线  double两根线  soild一根实现  wavy波浪线)
  10. wordSpacing: 单词间隙(负值则紧凑)
*/
    this.strutStyle,
/*
  文本的对齐方式。
  TextAlign枚举类型:left左对齐(默认)、right右对齐、center居中、justfy两端对齐。
  对齐的参考系是Text组件本身,只有Text宽度大于文本内容长度时才有意义。
*/
    this.textAlign,
    // 文本的方向。ltr从左至右,rtl从右至左。
    this.textDirection,
    this.locale,
    this.softWrap,
    // 对超出显示的文本指定截断方式。
    // TextOverflow.clip直接截断,TextOverflow.ellipsis将多余文本截断后加省略号...。
    this.overflow,
    // 文本的缩放因子(相对于当前系统字体大小)
    // 默认值可以通过MediaQueryData.textScaleFactor获得,如果没有MediaQuery,那么会默认值将为1.0。
    this.textScaleFactor,
    // 文本的最大显示行数。默认情况下文本自动折行。
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
    this.textHeightBehavior,
  }) 

示例

Text("Hello world! I'm Jack. "*4,
  textAlign: TextAlign.left,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
  textScaleFactor: 1.5,
);
Text("Hello world",
  style: TextStyle(
    color: Colors.blue,
    fontSize: 18.0,
    height: 1.2,  
    fontFamily: "Courier",
    background: new Paint()..color=Colors.yellow,
    decoration:TextDecoration.underline,
    decorationStyle: TextDecorationStyle.dashed
  ),
);
  1. TextSpan (富文本:对文本内容的不同部分按照不同的样式显示)
TextSpan({
  TextStyle style,  // 样式
  Sting text, // 内容
  List<TextSpan> children, // 一个TextSpan的数组,即TextSpan可以包括其他TextSpan
  GestureRecognizer recognizer, // 手势识别处理
});

示例

通过TextSpan实现了一个基础文本片段和一个链接片段,然后通过Text.rich将TextSpan添加到Text(RichText(显示富文本的widget)的一个包装)中。

Text.rich(TextSpan(
    children: [
     TextSpan(
       text: "Home: "
     ),
     TextSpan(
       text: "https://flutterchina.club",
       style: TextStyle(
         color: Colors.blue
       ),  
       recognizer: _tapRecognizer
     ),
    ]
))
/*
Text.rich(
    InlineSpan this.textSpan, {
    Key? key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
    this.textHeightBehavior,
  })

Text.rich的另一种用法

RichText({
    Key? key,
    required this.text,
    this.textAlign = TextAlign.start,
    this.textDirection,
    this.softWrap = true,
    this.overflow = TextOverflow.clip,
    this.textScaleFactor = 1.0,
    this.maxLines,
    this.locale,
    this.strutStyle,
    this.textWidthBasis = TextWidthBasis.parent,
    this.textHeightBehavior,
  })
*/
  1. DefaultTextStyle 继承父辈文本样式

在Widget树中,子孙类文本组件未指定具体样式时会使用父级DefaultTextStyle组件设置的默认样式,除非【显示指定不继承样式】inherit: false。

  DefaultTextStyle({
    Key? key,
    required this.style,
    this.textAlign,
    this.softWrap = true,
    this.overflow = TextOverflow.clip,
    this.maxLines,
    this.textWidthBasis = TextWidthBasis.parent,
    this.textHeightBehavior,
    required Widget child,
  })

示例(DefaultTextStyle、inherit)

DefaultTextStyle(
  // 1.设置文本默认样式  
  style: TextStyle(
    color:Colors.red,
    fontSize: 20.0,
  ),
  textAlign: TextAlign.start,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text("hello world"),
      Text("I am Jack"),
      Text("I am Jack",
        style: TextStyle(
          inherit: false, // 2.不继承默认样式
          color: Colors.grey
        ),
      ),
    ],
  ),
);
  1. animated_text_kit三方库(文字动画)

波浪式填充文字

Widget liquidText(String text) {
    return SizedBox(
      width: 320.0,
      child: TextLiquidFill(
        text: text,
        waveColor: Colors.blue[400]!,
        boxBackgroundColor: Colors.redAccent,
        textStyle: TextStyle(
          fontSize: 80.0,
          fontWeight: FontWeight.bold,
        ),
        boxHeight: 300.0,
        loadUntil: 0.7,  // 控制波浪最终停留的高度,取值是0-1.0
      ),
    );
  }

文字按波浪式跳动

Widget wavyText(List<String> texts) {
  return DefaultTextStyle(
    style: const TextStyle(
      color: Colors.blue,
      fontSize: 20.0,
    ),
    child: AnimatedTextKit(
      animatedTexts: texts.map((e) => WavyAnimatedText(e)).toList(),
      isRepeatingAnimation: true,
      repeatForever: true,  // 是否一直重复
      onTap: () {
        print("文字点击事件");
      },
    ),
  );
}

一道彩虹滑过文字,最终留下渐变效果。

Widget rainbowText(List<String> texts) {
  const colorizeColors = [
    Colors.purple,
    Colors.blue,
    Colors.yellow,
    Colors.red,
  ];
  const colorizeTextStyle = TextStyle(
    fontSize: 36.0,
    fontWeight: FontWeight.bold,
  );
  return SizedBox(
    width: 320.0,
    child: AnimatedTextKit(
      animatedTexts: texts
          .map((e) => ColorizeAnimatedText(
                e,
                textAlign: TextAlign.center,
                textStyle: colorizeTextStyle,
                colors: colorizeColors,
              ))
          .toList(),
      isRepeatingAnimation: true,
      repeatForever: true,
      onTap: () {
        print("文字点击事件");
      },
    ),
  );
}

向下轮播滚动广告牌

Widget rotateText(List<String> texts) {
  return SizedBox(
    width: 320.0,
    height: 100.0,
    child: DefaultTextStyle(
      style: const TextStyle(
        fontSize: 36.0,
        fontFamily: 'Horizon',
        fontWeight: FontWeight.bold,
        color: Colors.blue,
      ),
      child: AnimatedTextKit(
        animatedTexts: texts.map((e) => RotateAnimatedText(e)).toList(),
        onTap: () {
          print("点击事件");
        },
        repeatForever: true,
      ),
    ),
  );
}

一个一个打字打出来

Widget typerText(List<String> texts) {
  return SizedBox(
    width: 320.0,
    child: DefaultTextStyle(
      style: const TextStyle(
        fontSize: 30.0,
        color: Colors.blue,
      ),
      child: AnimatedTextKit(
        animatedTexts: texts
            .map((e) => TyperAnimatedText(
                  e,
                  textAlign: TextAlign.start,
                  speed: Duration(milliseconds: 300),
                ))
            .toList(),
        onTap: () {
          print("文字点击事件");
        },
        repeatForever: true,
      ),
    ),
  );
}

其他效果

    渐现效果(Fade)
    打字机效果(Typewriter)
    缩放效果(Scale)
    闪烁效果(Flicker)

自定义效果

只需要动效类继承AnimatedText,然后重载以下方法:
  1. 构造方法:通过构造方法配置动效参数
  2. initAnimation:初始化 Animation 对象,并将其与 AnimationController 绑定;
  3. animatedBuilder:动效组件构建方法,根据 AnimationController 的值构建当前状态的组件;
  4. completeText:动画完成后的组件,默认是返回一个具有样式修饰的文字。

2. 按钮

Material组件库
  1. RaisedButton    凸起的按钮(已弃用)
      使用ElevatedButton替代
  2. FlatButton      扁平化按钮(已弃用)
      使用TextButton替代
  3. OutlineButton   线框按钮(已弃用)
      使用OutlinedButton替代
  4. IconButton      图标按钮
  5. ButtonBar       按钮组
  6. 带图标的按钮
  7. InkWell         没有样式的带水波纹的按钮
  
所有Material库中的按钮:
    1. 都直接或间接继承自RawMaterialButton组件
    2. 按下时都会有水波动画/涟漪动画(点击时按钮上会出现水波荡漾的动画)。
    3. onPressed属性可设置点击回调,如果不提供该回调则按钮会处于禁用状态(不响应用户点击)。
  1. RaisedButton (已弃用)

默认带有阴影和灰色背景。按下后,阴影会变大

RaisedButton(
  child: Text("按钮"),
  color: Colors.blue,    // 背景色, Color(0x000000)透明
  textColor: Colors.white,  // 文本色
  disabledColor: Colors.gray,  // 禁用时的背景色
  disabledTextColor: Colors.gray,  // 禁用时的文本色
  splashColor: Colors.yellow,  // 点击按钮时的水波纹的颜色
  highligthColor: Colors.blue,    // 点击时的背景色
  elevation: 2.0,  // 正常状态下的阴影,值越大越明显
  highlightElevation: 8.0,  // 按下时的阴影
  disabledElevation:0.0, // 禁用时的阴影
  shape: RoundedRectangleBorder(  
    borderRadius: BorderRadius.circular(10.0)    // 圆角
  ),    // 设置shape为CircleBorder(side:BorderSide(color:Colors.white))  圆形按钮
  onPressed: () {
  },
);
通过在外层加Container:设置宽高。
通过在外层加Container和Expanded:自适应宽或高。
  1. FlatButton(已弃用)

默认背景透明并不带阴影。按下后,会有背景色。

  FlatButton({
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    ValueChanged<bool>? onHighlightChanged,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? disabledColor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    Brightness? colorBrightness, // 按钮主题,默认是浅色主题 
    EdgeInsetsGeometry? padding, // 内边距
    VisualDensity? visualDensity,
    ShapeBorder? shape,
    Clip clipBehavior = Clip.none,
    FocusNode? focusNode,
    bool autofocus = false,
    MaterialTapTargetSize? materialTapTargetSize,
    required Widget child,
    double? height,
    double? minWidth,
  }) 

FlatButton.icon() 
基本同上,没有visualDensity、child,多了2个必选参数icon、label。

示例

FlatButton(
  child: Text("normal"),
  colorBrightness: Brightness.dark, 
  padding: 10,  
  onPressed: () {},
)
  1. OutlineButton(已弃用)

默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)

  OutlineButton({
    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    MouseCursor? mouseCursor,
    ButtonTextTheme? textTheme,
    Color? textColor,
    Color? disabledTextColor,
    Color? color,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    Color? splashColor,
    double? highlightElevation,
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
    EdgeInsetsGeometry? padding,
    VisualDensity? visualDensity,
    ShapeBorder? shape,
    Clip clipBehavior = Clip.none,
    FocusNode? focusNode,
    bool autofocus = false,
    MaterialTapTargetSize? materialTapTargetSize,
    Widget? child,
  })
  BorderSide({
    this.color = const Color(0xFF000000),
    this.width = 1.0,
    this.style = BorderStyle.solid,
  })

ElevatedButton、TextButton、OutlinedButton

    Key? key,
    required VoidCallback? onPressed,
    VoidCallback? onLongPress,
    ButtonStyle? style,  // 
    FocusNode? focusNode,
    bool autofocus = false,
    Clip clipBehavior = Clip.none,
    required Widget? child,
/*
例(ButtonStyle)
        ButtonStyle(
          foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
          backgroundColor:
              MaterialStateProperty.all<Color>(Theme.of(context).primaryColor),
        )
*/
  1. IconButton

一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景

IconButton({
    Key? key,
    this.iconSize = 24.0,
    this.visualDensity,
    this.padding = const EdgeInsets.all(8.0),
    this.alignment = Alignment.center,
    this.splashRadius,
    required this.icon,
    this.color,
    this.focusColor,
    this.hoverColor,
    this.highlightColor,
    this.splashColor,
    this.disabledColor,
    required this.onPressed,
    this.mouseCursor = SystemMouseCursors.click,
    this.focusNode,
    this.autofocus = false,
    this.tooltip,
    this.enableFeedback = true,
    this.constraints,
  })
  1. ButtonBar
  ButtonBar({
    Key? key,
    this.alignment,
    this.mainAxisSize,
    this.buttonTextTheme,
    this.buttonMinWidth,
    this.buttonHeight,
    this.buttonPadding,
    this.buttonAlignedDropdown,
    this.layoutBehavior,
    this.overflowDirection,
    this.overflowButtonSpacing,
    this.children = const <Widget>[],  // 放入RaisedButton等类型按钮
  })
  1. 带图标的按钮

前面图片后面文本的按钮

// ElevatedButton
RaisedButton.icon(
  icon: Icon(Icons.send),
  label: Text("发送"),
  onPressed: _onPressed,
),
// OutlinedButton
OutlineButton.icon(
  icon: Icon(Icons.add),
  label: Text("添加"),
  onPressed: _onPressed,
),
// TextButton
FlatButton.icon(
  icon: Icon(Icons.info),
  label: Text("详情"),
  onPressed: _onPressed,
),
  1. InkWell
  InkWell({
    Key? key,
    Widget? child,
    GestureTapCallback? onTap,
    GestureTapCallback? onDoubleTap,
    GestureLongPressCallback? onLongPress,
    GestureTapDownCallback? onTapDown,
    GestureTapCancelCallback? onTapCancel,
    ValueChanged<bool>? onHighlightChanged,
    ValueChanged<bool>? onHover,
    MouseCursor? mouseCursor,
    Color? focusColor,
    Color? hoverColor,
    Color? highlightColor,
    MaterialStateProperty<Color?>? overlayColor,
    Color? splashColor,
    InteractiveInkFeatureFactory? splashFactory,
    double? radius,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    bool? enableFeedback = true,
    bool excludeFromSemantics = false,
    FocusNode? focusNode,
    bool canRequestFocus = true,
    ValueChanged<bool>? onFocusChange,
    bool autofocus = false,
  })
  1. PopupMenuButton

菜单按钮

  PopupMenuButton({
    Key? key,
    required this.itemBuilder,  // itemBuilder
    this.initialValue,  // 初识选中值
    this.onSelected,  // 选中某项后回调,参数为选中值
    this.onCanceled,  // 取消后回调
    this.tooltip,  
    this.elevation,  
    this.padding = const EdgeInsets.all(8.0),
    this.child,
    this.icon,
    this.offset = Offset.zero,
    this.enabled = true,
    this.shape,
    this.color,
  })
  PopupMenuItem({
    Key? key,
    this.value,  // 值
    this.enabled = true,
    this.height = kMinInteractiveDimension,
    this.textStyle,
    this.mouseCursor,
    required this.child,
  })

3. 图片、ICON

  1. 图片(Image组件,支持gif)
  Image({
    Key? key,
    required this.image,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    this.isAntiAlias = false,
    this.filterQuality = FilterQuality.low,
  })
1. image: (必选参数)
对应一个ImageProvider(抽象类,定义了获取图片数据的接口load)。
AssetImage从Asset中加载图片、NetworkImage从网络加载图片。
// 数据源可以是asset、文件、内存以及网络。
// Flutter框架对加载过的图片是有缓存的,默认最大缓存数量是1000,最大缓存空间为100M。

2. width、height:
图片的宽、高
当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小。当只设置width、height的其中一个时,另一个属性默认会按比例缩放,可以通过fit属性来指定适应规则。

3. fit:
缩放模式
用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。缩放模式是在BoxFit中定义,它是一个枚举类型,有如下值:
    fill:不按比例填充。
    cover:按比例填充,超出显示空间部分会被剪裁。
    contain:默认,按比例填充,会有留白。
    fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
    none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。

4. color和 colorBlendMode:
在图片绘制时可以对每一个像素进行颜色混合处理
color指定混合色,而colorBlendMode指定混合模式(如:Blend.screen)

5. repeat:
重复方式
当图片本身大小小于显示空间时,指定图片的重复规则。
  ImageRepeat.repeat 横纵向重复
  ImageRepeat.repeatX 横向重复
  ImageRepeat.repeatY 纵向重复

6. alignment
对齐方式 Alignment.center居中

示例(colorBlendMode、repeat)

Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0,
  color: Colors.blue,
  colorBlendMode: BlendMode.difference,
);
Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0,
  height: 200.0,
  repeat: ImageRepeat.repeatY ,
)


示例(从asset中加载图片)

1. 在工程根目录下创建一个images目录,并将图片avatar.png拷贝到该目录。

2. 在pubspec.yaml中的flutter部分添加如下内容:(由于 yaml 文件对缩进严格,所以必须严格按照每一层两个空格的方式进行缩进,此处assets前面应有两个空格。)
flutter:
  assets:
    - images/avatar.png

3. 加载该图片
Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0
);
或(常用)
Image.asset("images/avatar.png",
  width: 100.0,
)

示例(从网络加载图片)

Image(
  image: NetworkImage(
      "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
  width: 100.0,
)
或
Image.network(
  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
  width: 100.0,
)

无法对图片进行缓存

示例(fit)

import 'package:flutter/material.dart';
class ImageAndIconRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var img=AssetImage("imgs/avatar.png");
    return SingleChildScrollView(
      child: Column(
        children: <Image>[
          Image(
            image: img,
            height: 50.0,
            width: 100.0,
            fit: BoxFit.fill,
          ),
          Image(
            image: img,
            height: 50,
            width: 50.0,
            fit: BoxFit.contain,
          ),
          Image(
            image: img,
            width: 100.0,
            height: 50.0,
            fit: BoxFit.cover,
          ),
          Image(
            image: img,
            width: 100.0,
            height: 50.0,
            fit: BoxFit.fitWidth,
          ),
          Image(
            image: img,
            width: 100.0,
            height: 50.0,
            fit: BoxFit.fitHeight,
          ),
          Image(
            image: img,
            width: 100.0,
            height: 50.0,
            fit: BoxFit.scaleDown,
          ),
          Image(
            image: img,
            height: 50.0,
            width: 100.0,
            fit: BoxFit.none,
          ),
          Image(
            image: img,
            width: 100.0,
            color: Colors.blue,
            colorBlendMode: BlendMode.difference,
            fit: BoxFit.fill,
          ),
          Image(
            image: img,
            width: 100.0,
            height: 200.0,
            repeat: ImageRepeat.repeatY ,
          )
        ].map((e){
          return Row(
            children: <Widget>[
              Padding(
                padding: EdgeInsets.all(16.0),
                child: SizedBox(
                  width: 100,
                  child: e,
                ),
              ),
              Text(e.fit.toString())
            ],
          );
        }).toList()
      ),
    );
  }
}

例(placeholder,淡入。依赖transparent_image包)

import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
void main() {
  runApp(new MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'Fade in images';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new Stack(
          children: <Widget>[
            new Center(child: new CircularProgressIndicator()),
            new Center(
              child: new FadeInImage.memoryNetwork(
                placeholder: kTransparentImage,
                image:
                    'https://github.com/flutter/website/blob/master/_includes/code/layout/lakes/images/lake.jpg?raw=true',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

例(缓存图片,cached_network_image包)

支持缓存、占位符和淡入淡出图片
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
void main() {
  runApp(new MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'Cached Images';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new Center(
          child: new CachedNetworkImage(
            placeholder: new CircularProgressIndicator(),
            imageUrl:
                'https://github.com/flutter/website/blob/master/_includes/code/layout/lakes/images/lake.jpg?raw=true',
            errorWidget: (context, url, error) => Image.asset('images/image-failed.png'),
            // CircularProgressIndicator圆形
            progressIndicatorBuilder: (context, url, downloadProgress) => LinearProgressIndicator(value: downloadProgress.progress),
          ),
        ),
      ),
    );
  }
}
  1. ICON

Flutter可以像Web开发一样使用iconfont(字体图标),将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。

在字体文件中,每一个字符都对应一个位码,而每一个位码对应一个显示字形,不同的字体就是指字形不同,即字符对应的字形是不同的。
在iconfont中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标。

iconfont和图片相比有如下优势:
    1. 体积小:可以减小安装包大小。
    2. 矢量:iconfont都是矢量图标,放大不会影响其清晰度。
    3. 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
    4. 可以通过TextSpan和文本混用。
  1. 使用内置的Material Design字体图标。在图标官网搜索
Flutter默认内置了一套Material Design的字体图标,在pubspec.yaml文件中的配置如下
  flutter:
    uses-material-design: true
Icons类中包含了所有Material Design图标的IconData静态变量定义。

例:
String icons = "";
// accessible: &#xE914; or 0xE914 or E914
icons += "\uE914";
// error: &#xE000; or 0xE000 or E000
icons += " \uE000";
// fingerprint: &#xE90D; or 0xE90D or E90D
icons += " \uE90D";
Text(icons,
  style: TextStyle(
      fontFamily: "MaterialIcons",
      fontSize: 24.0,
      color: Colors.green
  ),
);
使用图标就像使用文本一样,但是这种方式需要提供每个图标的码点,这对开发者并不友好,所以,Flutter封装了IconData和Icon来专门显示字体图标,上面的例子也可以用如下方式实现:
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.accessible,color: Colors.green,),
    Icon(Icons.error,color: Colors.green,),
    Icon(Icons.fingerprint,color: Colors.green,),
  ],
)
  1. 使用自定义字体图标
iconfont.cn上有很多字体图标素材,可以选择自己需要的图标打包下载后,会生成一些不同格式的字体文件,在Flutter中使用ttf格式即可。

例
1. 导入字体图标文件;
和导入字体文件相同,假设字体图标文件保存在项目根目录下,路径为"fonts/iconfont.ttf":
  fonts:
    - family: myIcon  #指定一个字体名
      fonts:
        - asset: fonts/iconfont.ttf
2. 定义一个MyIcons类便于使用。
功能和Icons类一样:将字体文件中的所有图标都定义成静态变量:
class MyIcons{
  // book 图标
  static const IconData book = const IconData(
      0xe614, 
      fontFamily: 'myIcon', 
      matchTextDirection: true
  );
  // 微信图标
  static const IconData wechat = const IconData(
      0xec7d,  
      fontFamily: 'myIcon', 
      matchTextDirection: true
  );
}
3.使用
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(MyIcons.book,color: Colors.purple,),
    Icon(MyIcons.wechat,color: Colors.green,),
  ],
)

常用三方库

  1. 加载网络图片(cached_network_image三方库)
1. 
import 'package:cached_network_image/cached_network_image.dart';

2.
    // 展位图、出错时Widget
    CachedNetworkImage(
        imageUrl: "http://dd.com/350x150",
        placeholder: (context, url) => CircularProgressIndicator(),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),
    // 进度图片时的加载指示器
    CachedNetworkImage(
        imageUrl: imageUrl,
        // LinearProgressIndicator是线型指示器,CircularProgressIndicator 是圆形指示器
        progressIndicatorBuilder: (context, url, downloadProgress) =>
            LinearProgressIndicator(value: downloadProgress.progress),
        errorWidget: (context, url, error) =>
            Image.asset('images/image-failed.png'),
      ),
  1. 获取相机/相册中的图片(image_picker、multi_image_picker、multi_image_picker2、wechat_assets_picker)
image_picker支持单张图片选择、multi_image_picker支持多图选择。
  二者均支持相机或从相册选择图片。
  multi_image_picker默认语言是英文,需要自己配置本地语言。
/*
访问权限
iOS(info.plist)
  <key>NSPhotoLibraryUsageDescription</key>
  <string>需要相册权限</string>
Android(AndroidManifest.xml)
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
*/
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class _MyHomePageState extends State<MyHomePage> {
  File _image;
  final picker = ImagePicker();
  Future getImage() async {
    final pickedFile = await picker.getImage(source: ImageSource.camera);
    setState(() {
      if (pickedFile != null) {
        _image = File(pickedFile.path);
      } else {
        print('No image selected.');
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Picker Example'),
      ),
      body: Center(
        child: _image == null
            ? Text('No image selected.')
            : Image.file(_image),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: getImage,
        tooltip: 'Pick Image',
        child: Icon(Icons.add_a_photo),
      ),
    );
  }
}

4. MaterialApp

用于快速搭建APP框架(名称、主题、语言、首页及路由列表等)。

1. home
  首页
2. initialRoute
  使用命名路由时的首页路径(默认是'/')
3. routes
  命名路由列表
4. theme
  主题
5. title
  应用名
5. debugShowCheckedModeBanner 
  是否显示右上角debug角标

示例

      MaterialApp(
    title: ProjectConfig.packageInfo.appName,
    theme: ProjectTheme.theme,
    routes: {
      '/':(context)=>BootstrapPage(),
      '/login':(context)=>LoginPage(),
      '/register':(context)=>RegisterPage(),
      '/tab':(context)=>TabPage(),
    },
      )

5. Scaffold 页面骨架

用于快速搭建页面(导航栏、底部TabBar、悬浮按钮、抽屉等)。

// 属于Material组件库
1. appBar
  导航栏(AppBar)
2. body
  页面内容
3. bottomNavigationBar
  底部tabbar(BottomNavigationBar)
4. floatingActionButton
  悬浮按钮(FloatingActionButton)
5. floatingActionButtonLocation
  悬浮按钮位置
6. backgroundColor  
  背景色
7. drawer
  左侧抽屉
8. endDrawer
  右侧抽屉

示例

实现一个页面,包含:
    一个导航栏
    导航栏右边有一个分享按钮
    有一个抽屉菜单
    有一个底部导航
    右下角有一个悬浮的动作按钮

import 'package:flutter/material.dart';
void main() {
  runApp(new MaterialApp(
    title: 'Flutter Tutorial',
    home: new ScaffoldRoute(),
  ));
}
class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}
class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 1;
  @override
  Widget build(BuildContext context) {
    // Material必须有Scaffold
    return Scaffold(
      appBar: AppBar( // 导航栏
        title: Text("App Name"), 
        actions: <Widget>[ // 导航栏右侧菜单
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
        // leading: new IconButton(
        //   icon: new Icon(Icons.menu),
        //   tooltip: 'Navigation menu',
        //   onPressed: null,
        // ),
      ),
      // body: this._pageList[this._selectedIndex],
      drawer: new MyDrawer(), // 抽屉
      bottomNavigationBar: BottomNavigationBar( // 底部导航
        items: <BottomNavigationBarItem>[  // items
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
        ],
        currentIndex: _selectedIndex,    // 当前选中
        fixedColor: Colors.blue,  // 选种颜色
        onTap: _onItemTapped,  // 点击后调用
        // iconSize:35,  // 图标大小
        // type: BottomNavigationBarType.fixed 按钮过多时允许设置多个
      ),
      floatingActionButton: FloatingActionButton( // 悬浮按钮
          child: Icon(Icons.add),
          onPressed:_onAdd    // 可为null
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){}
}
实现效果

抽屉效果

AppBar (导航栏)

一个Material风格的导航栏(导航栏标题、导航栏菜单、导航栏底部的Tab标题等)。

AppBar({
  Key key,
  // 导航栏最左侧Widget(常见为抽屉菜单按钮或返回按钮,在首页一般显示logo)。Icon、IconButton
  this.leading,         
  this.automaticallyImplyLeading = true, // 如果leading为null,是否自动实现默认的leading按钮
  this.title,           // 页面标题Widget
  this.actions,   // 导航栏右侧Widget列表,在右边 从左至右显示
  this.bottom,  // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle,     // 标题是否居中 
  this.backgroundColor,  // 背景色
  iconTheme  // 图标样式
  textTheme  // 文本样式
  centerTitle  // 标题是否居中
})

leading

如果给Scaffold添加了抽屉菜单,默认情况下Scaffold会自动将AppBar的leading设置为菜单按钮, 点击它便可打开抽屉菜单。

如果想自定义菜单图标,可以手动来设置leading,如:
Scaffold(
  appBar: AppBar(
    title: Text("App Name"),
    leading: Builder(builder: (context) {
      return IconButton(
        icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标
        onPressed: () {
          // 打开抽屉菜单  Scaffold.of(context)可以获取父级最近的Scaffold 组件的State对象。
          Scaffold.of(context).openDrawer(); 
        },
      );
    }),
    ...  
  )
替换左侧菜单图标

TabBar组件(Tab菜单栏)

// 属于Material组件库
1. tabs  
  Tab列表
2. controller
  TabController对象
3. isScrollable
  是否可滚动
4. indicatorColor
  指示器颜色
5. indicatorWeight
  指示器高度
6. indicatorPadding
  指示器Padding
7. indicatorSize
  指示器大小,TabBarIndicatorSize.label 与文本同宽
8. indicator
  自定义指示器
9. labelColor
  选中文本色
10. labelStyle
  选中文本样式
11. labelPadding
  文本Padding
12. unselectedLabelColor
  未选中文本色
13. unselectedLabelStyle
  未选中文本样式

Tab

Tab({
  Key key,
  this.text, // 菜单文本
  this.icon, // 菜单图标
  this.child, // 自定义组件样式
})

示例(方式1: with SingleTickerProviderStateMixin)

class _ScaffoldRouteState extends State<ScaffoldRoute>
    with SingleTickerProviderStateMixin {
  TabController _tabController; // 需要定义一个Controller
  List tabs = ["新闻", "历史", "图片"];
  @override
  void initState() {
    super.initState();
    // 创建Controller  ,用于控制/监听Tab菜单切换
    _tabController = TabController(length: tabs.length, vsync: this);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        ... //省略无关代码
        bottom: TabBar(   // 生成Tab菜单
          controller: _tabController,
          tabs: tabs.map((e) => Tab(text: e)).toList()
        ),
      ),
      ... //省略无关代码
     );
  }
Tabbar

通过TabBar只能生成一个静态的菜单,真正的Tab页(通过TabBarView来实现)还没有实现。

由于Tab菜单和Tab页的切换需要同步,需要通过TabController去监听Tab菜单的切换去切换Tab页

在initState方法中,创建完成后,添加监听
_tabController.addListener((){  
  switch(_tabController.index){
    case 1: ...;
    case 2: ... ;   
  }
});

如果Tab页可以滑动切换的话,还需要在滑动过程中更新TabBar指示器的偏移!显然,要手动处理这些是很麻烦的。
为此,Material库提供了一个TabBarView组件,通过它不仅可以轻松的实现Tab页,而且可以非常容易的配合TabBar来实现同步切换和滑动状态同步:

Scaffold(
  appBar: AppBar(
    ... //省略无关代码
    bottom: TabBar(
      controller: _tabController,   // 配置controller
      tabs: tabs.map((e) => Tab(text: e)).toList()),
    ),
  ),
  drawer: new MyDrawer(),
  body: TabBarView(
    controller: _tabController,  // 配置controller
    children: tabs.map((e) { // 创建3个Tab页,分别对应TabBar中的tabs
      return Container(
        alignment: Alignment.center,
        child: Text(e, textScaleFactor: 5),
      );
    }).toList(),
  ),
  ... // 省略无关代码  
);

现在,无论是点击导航栏Tab菜单还是在页面上左右滑动,Tab页面都会切换,并且Tab菜单的状态和Tab页面始终保持同步。
TabBar和TabBarView正是通过同一个controller来实现菜单切换和滑动状态同步的。

示例(方式2:DefaultTabController)

  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Home'),
          centerTitle: true,
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.search),
              onPressed: _searchFunc,
            )
          ],
          bottom: TabBar(
            tabs: <Widget>[
              Tab(text: '热门'),
              Tab(text: '推荐')
            ],
          ),
        ),
        drawer: Drawer(
          child: Text('左侧抽屉'),
        ),
        endDrawer: Drawer(
          child: Text('右侧抽屉'),
        ),
        body: TabBarView(
          children: <Widget>[
            ListView(
              children: <Widget>[
                ListTile(
                  title: Text('Hot1'),
                ),
                ListTile(
                  title: Text('Hot2'),
                ),
              ],
            ),
            ListView(
              children: <Widget>[
                ListTile(
                  title: Text('Reconmend1'),
                ),
                ListTile(
                  title: Text('Reconmend2'),
                ),
              ],
            )
          ],
        ),
      ),
    );
  }
  void _searchFunc(){
  }

抽屉菜单(Drawer)

Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。

路由跳转
  Navigator.pop();  // 隐藏侧边栏
  Navigator.pushNamed(context, "/new_page");    // 跳转到新页面

示例(一个左抽屉菜单)

class MyDrawer extends StatelessWidget {
  const MyDrawer({
    Key key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        // 移除抽屉菜单顶部默认留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
抽屉菜单通常将Drawer组件作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding可以移除Drawer默认的一些留白(比如Drawer默认顶部会留和手机状态栏等高的留白)

DrawerHeader 抽屉头部

1. child
  子组件
2. decoration
  装饰
3. padding 
  内边距
4. margin
  外边距

UserAccountsDrawerHeader 抽屉头部

左侧依次显示头像、名称、邮箱

1. decoration
  装饰
2. margin
  外边距
3. currentAccountPicture
  账户头像
4. accountName
  账户名
5. accountEmial
  账户邮箱
6. otherAccountsPictures
  右侧图片列表(和头像顶部对齐)

FloatingActionButton 浮动按钮

悬浮在页面的某一个位置作为某种常用动作的快捷入口。
可以通过Scaffold的floatingActionButton属性来设置一个FloatingActionButton,同时通过Scaffold的floatingActionButtonLocation属性来指定其在页面中悬浮的位置

  FloatingActionButton({
    Key? key,
    this.child,  // 
    this.tooltip,
    this.foregroundColor,  
    this.backgroundColor,
    this.focusColor,
    this.hoverColor,
    this.splashColor,
    this.heroTag = const _DefaultHeroTag(),
    this.elevation,  // 阴影
    this.focusElevation,
    this.hoverElevation,
    this.highlightElevation,
    this.disabledElevation,
    required this.onPressed,  // 点击后的回调
    this.mouseCursor,
    this.mini = false,
    this.shape,
    this.clipBehavior = Clip.none,
    this.focusNode,
    this.autofocus = false,
    this.materialTapTargetSize,
    this.isExtended = false,
  }) 

示例

      floatingActionButton: FloatingActionButton(
        onPressed: (){
        },
        child: Icon(Icons.add,color: Colors.black,size: 55),  
        tooltip: 'hello',    // 长按时显示
        backgroundColor: Colors.blue,  // 背景色
        elevation: 4.0,  // 未点击时的阴影
        highlightElevation: 12.0,  // 点击时的阴影,默认12.0
        shape:CircularNotchedRectangle(),  // 形状
        mini:    // 是否是min类型,默认false
      ), 
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, // centerFloat底部中间,centerDocked底部中间略向下(打洞),默认在底部右侧,

通过在外层添加Container设置大小。Container中设置margin移动位置。

Tab底部导航栏(TabBar、BottomAppBar、BottomNavigationBar)

可以通过Scaffold的bottomNavigationBar属性来设置底部导航

BottomAppBar({
    Key? key,
    this.color,  // 背景色
    this.elevation,  // 阴影
    this.shape,
    this.clipBehavior = Clip.none,
    this.notchMargin = 4.0,
    this.child,  // 
  })

// 可配合IndexedStack(index: _index,children: [DynamicPage(),MessagePage()])实现tab页面切换
BottomNavigationBar({
    Key? key,
    required this.items,  // items(BottomNavigationBarItem)
    this.onTap,    // 点击后的回调
    this.currentIndex = 0,  // 默认显示第几个页面
    this.elevation,
    this.type,  // 布局类型(BottomNavigationBarType:fixed图片固定;shifting图片漂移)
    Color? fixedColor,  // 
    this.backgroundColor,  // 背景色
    this.iconSize = 24.0,  // 图标大小
    Color? selectedItemColor,  // 选中色
    this.unselectedItemColor,  // 未选中色
    this.selectedIconTheme,
    this.unselectedIconTheme,
    this.selectedFontSize = 14.0,     // 选中字体
    this.unselectedFontSize = 12.0, // 未选中字体
    this.selectedLabelStyle,  // 选中字体样式
    this.unselectedLabelStyle,  // 未选中字体样式
    this.showSelectedLabels,  
    this.showUnselectedLabels,
    this.mouseCursor,
  })
  BottomNavigationBarItem({
    required this.icon,
    this.title,  // 弃用
    this.label,
    Widget? activeIcon,
    this.backgroundColor,
  })

示例(打洞)(BottomAppBar)

BottomAppBar组件可以和FloatingActionButton配合也可以实现“打洞”效果,源码如下:

bottomNavigationBar: BottomAppBar(
  color: Colors.white,
  shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
  child: Row(
    children: [
      IconButton(icon: Icon(Icons.home)),
      SizedBox(), // 中间位置空出
      IconButton(icon: Icon(Icons.business)),
    ],
    mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
  ),
)

上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton的位置,上面FloatingActionButton的位置为:
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
所以打洞位置在底部导航栏的正中间。

BottomAppBar的shape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,也可以自定义外形。
打洞

示例(BottomNavigationBar)

bottomNavigationBar: BottomNavigationBar( // 底部导航
    items: <BottomNavigationBarItem>[  // items
        BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
        BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
        BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
    ],
)

示例(BottomNavigationBar)

import 'package:flutter/material.dart';
import 'dynamic.dart';
import 'message.dart';
import 'category.dart';
import 'mine.dart';
class AppHomePage extends StatefulWidget {
  AppHomePage({Key key}) : super(key: key);
  @override
  _AppHomePageState createState() => _AppHomePageState();
}
class _AppHomePageState extends State<AppHomePage> {
  int _index = 0;
  List<Widget> _homeWidgets = [
    DynamicPage(),
    MessagePage(),
    CategoryPage(),
    MinePage(),
  ];
  void _onBottomNagigationBarTapped(index) {
    setState(() {
      _index = index;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('hello'),
      ),
      body: IndexedStack(  // 管理页面显示层级,index指定最上层
        index: _index,
        children: _homeWidgets,
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: _index,
        onTap: _onBottomNagigationBarTapped,
        items: [
          _getBottomNavItem(
              '动态', 'images/dynamic.png', 'images/dynamic-hover.png', 0),
          _getBottomNavItem(
              ' 消息', 'images/message.png', 'images/message-hover.png', 1),
          _getBottomNavItem(
              '分类浏览', 'images/category.png', 'images/category-hover.png', 2),
          _getBottomNavItem(
              '个人中心', 'images/mine.png', 'images/mine-hover.png', 3),
        ],
      ),
    );
  }
  BottomNavigationBarItem _getBottomNavItem(
      String title, String normalIcon, String pressedIcon, int index) {
    return BottomNavigationBarItem(
      icon: _index == index
          ? Image.asset(
              pressedIcon,
              width: 32,
              height: 28,
            )
          : Image.asset(
              normalIcon,
              width: 32,
              height: 28,
            ),
      label: title,
    );
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容