Flutter了解之入门篇5-2(常用组件2)

目录

  1. 进度指示器(LinearProgressIndicator、CircularProgressIndicator)
  2. 日期组件
  3. 宽高比组件AspectRatio
  4. 卡片组件Card
  5. 剪裁
  6. 标签 Chip、ActionChip、FilterChip、ChoiceChip
  7. 表格 DataTable、PaginatedDataTable
  8. 分割线Divider
  9. ListTile
  10. ButtonBarTheme
  11. Material
  Spacer
  Visibility
  IndexedStack
  CircleAvatar

1. 进度指示器

LinearProgressIndicator、CircularProgressIndicator
  精确进度指示(可计算/预估,如:文件下载)、模糊进度指示(如:下拉刷新、数据提交)。
  没提供尺寸参数(以父容器尺寸作为绘制边界),可通过ConstrainedBox、SizedBox等尺寸限制类组件来指定尺寸。
  内部是通过CustomPainter来实现外观绘制的。

自定义指示器(通过CustomPainter自定义绘制)
  1. LinearProgressIndicator(线性/条状进度条)
  LinearProgressIndicator({
    Key key,
    double value,  // 当前进度,取值范围为[0,1],如果为null则会执行一个循环动画(模糊进度)。
    Color backgroundColor,  // 背景色
    Animation<Color> valueColor,  // 进度条颜色,可以指定动画,也可以AlwaysStoppedAnimation指定固定颜色
    String semanticsLabel,
    String semanticsValue,
  })

示例

// 模糊进度条(会执行一个循环动画:蓝色条一直在移动)
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
// 进度条显示50%
LinearProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5, 
)

示例(一个进度条在3秒内从灰色变成蓝色的动画)

import 'package:flutter/material.dart';
class ProgressRoute extends StatefulWidget {
  @override
  _ProgressRouteState createState() => _ProgressRouteState();
}
class _ProgressRouteState extends State<ProgressRoute>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  @override
  void initState() {
    // 动画执行时间3秒  
    _animationController =
        new AnimationController(vsync: this, duration: Duration(seconds: 3));
    _animationController.forward();
    _animationController.addListener(() => setState(() => {}));
    super.initState();
  }
  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
            Padding(
            padding: EdgeInsets.all(16),
            child: LinearProgressIndicator(
              backgroundColor: Colors.grey[200],
              valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
                .animate(_animationController), // 从灰色变成蓝色
              value: _animationController.value,
            ),
          );
        ],
      ),
    );
  }
}
  1. CircularProgressIndicator(圆形进度条)
  CircularProgressIndicator({
    Key key,
    double value,
    Color backgroundColor,
    Animation<Color> valueColor,
    this.strokeWidth = 4.0,  // 圆形进度条的粗细
    String semanticsLabel,
    String semanticsValue,
  })

示例

// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
),
// 进度条显示50%,会显示一个半圆
CircularProgressIndicator(
  backgroundColor: Colors.grey[200],
  valueColor: AlwaysStoppedAnimation(Colors.blue),
  value: .5,
),

示例(自定义尺寸)

// 线性进度条高度指定为3
SizedBox(
  height: 3,
  child: LinearProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .5,
  ),
),
// 圆形进度条直径指定为100。如果CircularProgressIndicator显示空间的宽高不同,则会显示为椭圆。
SizedBox(
  height: 100,
  width: 100,
  child: CircularProgressIndicator(
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation(Colors.blue),
    value: .7,
  ),
),
  1. 自定义指示器(通过CustomPainter自定义绘制)
  1. flutter_spinkit三方库

示例(风车)

import 'dart:math';
import 'package:flutter/material.dart';
class WindmillIndicator extends StatefulWidget {
  final double size; // 大小
  final double speed; // 转速,默认2秒转一圈。
  final bool isClockwise; // 是否顺时针
  WindmillIndicator({
    Key key,
    this.size = 50.0,
    this.speed = 0.5,
    this.isClockwise = true,
  })  : assert(speed > 0),
        assert(size > 20),
        super(key: key);
  @override
  _WindmillIndicatorState createState() => _WindmillIndicatorState();
}
class _WindmillIndicatorState extends State<WindmillIndicator>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  @override
  void initState() {
    super.initState();
    int milliseconds = 1000 ~/ widget.speed;
    controller = AnimationController(
        duration: Duration(milliseconds: milliseconds), vsync: this);
    controller.repeat();
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedWindmill(
      animation: controller,
      size: widget.size,
      isClockwise: widget.isClockwise,
    );
  }
  @override
  void dispose() {
    if (controller.status != AnimationStatus.completed &&
        controller.status != AnimationStatus.dismissed) {
      controller.stop();
    }
    controller.dispose();
    super.dispose();
  }
}
// 组装4个叶片
class AnimatedWindmill extends AnimatedWidget {
  final double size;
  final bool isClockwise;
  AnimatedWindmill({
    Key key,
    @required Animation<double> animation,
    this.isClockwise = true,
    this.size = 50.0,
  }) : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    final rotationAngle = isClockwise
        ? 2 * pi * animation.value
        : -2 * pi * animation.value;
    return Stack(
      alignment: Alignment.topCenter,
      children: [
        WindmillWing(
          size: size,
          color: Colors.blue,
          angle: 0 + rotationAngle,
        ),
        WindmillWing(
          size: size,
          color: Colors.yellow,
          angle: pi / 2 + rotationAngle,
        ),
        WindmillWing(
          size: size,
          color: Colors.green,
          angle: pi + rotationAngle,
        ),
        WindmillWing(
          size: size,
          color: Colors.red,
          angle: -pi / 2 + rotationAngle,
        ),
      ],
    );
  }
}
// 单个叶片
class WindmillWing extends StatelessWidget {
  final double size; // 叶片所占据的正方形区域的边长
  final Color color; // 叶片颜色
  final double angle; // 叶片旋转角度
  const WindmillWing({
    Key key,
    @required this.size,
    @required this.color,
    @required this.angle,
  });
  @override
  Widget build(BuildContext context) {
    return Container(
      transformAlignment: Alignment.bottomCenter,
      // 旋转后风车会下移,这里向上补偿size / 2
      transform: Matrix4.translationValues(0, -size / 2, 0)..rotateZ(angle),
      // 将正方形剪裁成叶片
      child: ClipPath(
        child: Container(
          width: size,
          height: size,
          alignment: Alignment.center,
          color: color,
        ),
        clipper: WindwillClipPath(),
      ),
    );
  }
}
class WindwillClipPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    // 2弧线闭合
    var path = Path()
      ..moveTo(size.width / 3, size.height)
      ..arcToPoint(
        Offset(0, size.height * 2 / 3),
        radius: Radius.circular(size.width / 2),
      )
      ..arcToPoint(
        Offset(size.width, 0),
        radius: Radius.circular(size.width),
      )
      ..lineTo(size.width / 3, size.height);
    return path;
  }
  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}

示例(在圆环内滚动的小球)

// 动画控制设置
controller =
  AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
  parent: controller,
  curve: Curves.slowMiddle,
))
..addListener(() {
  setState(() {});
});
// 绘制和动画控制方法
_drawLoadingCircle(Canvas canvas, Size size) {
  var paint = Paint()..style = PaintingStyle.stroke
    ..color = Colors.blue[400]!
    ..strokeWidth = 2.0;
  var path = Path();
  final radius = 40.0;
  var center = Offset(size.width / 2, size.height / 2);
  path.addOval(Rect.fromCircle(center: center, radius: radius));
  canvas.drawPath(path, paint);
  
  var innerPath = Path();
  final ballRadius = 4.0;
  innerPath.addOval(Rect.fromCircle(center: center, radius: radius - ballRadius));
  var metrics = innerPath.computeMetrics();
  paint.color = Colors.red;
  paint.style = PaintingStyle.fill;
  for (var pathMetric in metrics) {
    var tangent = pathMetric.getTangentForOffset(pathMetric.length * animationValue);
    canvas.drawCircle(tangent!.position, ballRadius, paint);
  }
}

示例(两横纵向圆环上滚动的小球)

  controller =
      AnimationController(duration: const Duration(seconds: 2), vsync: this);
  animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
    parent: controller,
    curve: Curves.easeInOutSine,
  ))
    ..addListener(() {
      setState(() {});
    });
_drawTwinsCircle(Canvas canvas, Size size) {
  var paint = Paint()
    ..style = PaintingStyle.stroke
    ..color = Colors.blue[400]!
    ..strokeWidth = 2.0;
  final radius = 50.0;
  final ballRadius = 6.0;
  var center = Offset(size.width / 2, size.height / 2);
  var circlePath = Path()
    ..addOval(Rect.fromCircle(center: center, radius: radius));
  paint.style = PaintingStyle.stroke;
  paint.color = Colors.blue[400]!;
  canvas.drawPath(circlePath, paint);
  var circleMetrics = circlePath.computeMetrics();
  for (var pathMetric in circleMetrics) {
    var tangent = pathMetric
        .getTangentForOffset(pathMetric.length * animationValue);

    paint.style = PaintingStyle.fill;
    paint.color = Colors.blue;
    canvas.drawCircle(tangent!.position, ballRadius, paint);
  }
  paint.style = PaintingStyle.stroke;
  paint.color = Colors.green[600]!;
  var ovalPath = Path()
    ..addOval(Rect.fromCenter(center: center, width: 3 * radius, height: 40));
  canvas.drawPath(ovalPath, paint);
  var ovalMetrics = ovalPath.computeMetrics();
  for (var pathMetric in ovalMetrics) {
    var tangent =
        pathMetric.getTangentForOffset(pathMetric.length * animationValue);
    paint.style = PaintingStyle.fill;
    canvas.drawCircle(tangent!.position, ballRadius, paint);
  }
}

示例(钟摆运动)

1. 绘制顶部的横线,代表悬挂的顶点;
2. 绘制运动的圆弧路径,以便让球沿着圆弧运动;
3. 绘制实心圆代表球,并通过动画控制沿着一条圆弧运动;
4. 用一条顶端固定,末端指向球心的直线代表绳子;
5. 当球运动到弧线的终点后,通过动画反转(reverse)控制球 返回;到起点后再正向(forward) 运动就可以实现来回运动的效果了。

controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
  parent: controller,
  curve: Curves.easeInOutQuart,
))
  ..addListener(() {
    setState(() {});
  }
  ..addStatusListener((status) {
   if (status == AnimationStatus.completed) {
     controller.reverse();
   } else if (status == AnimationStatus.dismissed) {
     controller.forward();
   }
 });
_drawPendulum(Canvas canvas, Size size) {
  var paint = Paint()
    ..style = PaintingStyle.stroke
    ..color = Colors.blue[400]!
    ..strokeWidth = 2.0;
  final ceilWidth = 60.0;
  final pendulumHeight = 200.0;
  var ceilCenter =
      Offset(size.width / 2, size.height / 2 - pendulumHeight / 2);
  var ceilPath = Path()
    ..moveTo(ceilCenter.dx - ceilWidth / 2, ceilCenter.dy)
    ..lineTo(ceilCenter.dx + ceilWidth / 2, ceilCenter.dy);
  canvas.drawPath(ceilPath, paint);
  var pendulumArcPath = Path()
    ..addArc(Rect.fromCircle(center: ceilCenter, radius: pendulumHeight),
        3 * pi / 4, -pi / 2);
  paint.color = Colors.white70;
  var metrics = pendulumArcPath.computeMetrics();
  for (var pathMetric in metrics) {
    var tangent =
        pathMetric.getTangentForOffset(pathMetric.length * animationValue);
    canvas.drawLine(ceilCenter, tangent!.position, paint);
    paint.style = PaintingStyle.fill;
    paint.color = Colors.blue;
    paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0);
    canvas.drawCircle(tangent.position, 16.0, paint);
  }
}

示例(一个有趣的Loading组件)

参数
    1. 前景色:绘制图形的前景色;
    2. 背景色:绘制图形的背景色;
    3. 图形尺寸:绘制图形的尺寸;
    4. 加载文字:可选,有就显示,没有就不显示。

class LoadingAnimations extends StatefulWidget {
  final Color bgColor;
  final Color foregroundColor;
  String? loadingText;
  final double size;
  LoadingAnimations(
      {required this.foregroundColor,
      required this.bgColor,
      this.loadingText,
      this.size = 100.0,
      Key? key})
      : super(key: key);
  @override
  _LoadingAnimationsState createState() => _LoadingAnimationsState();
}

圆形Loading

多个沿着大圆运动的实心圆,半径依次减小,实心圆的间距随着动画时间逐步拉大。

_drawCircleLoadingAnimaion(
      Canvas canvas, Size size, Offset center, Paint paint) {
  final radius = boxSize / 2;
  final ballCount = 6;
  final ballRadius = boxSize / 15;
  var circlePath = Path()
    ..addOval(Rect.fromCircle(center: center, radius: radius));
  var circleMetrics = circlePath.computeMetrics();
  for (var pathMetric in circleMetrics) {
    for (var i = 0; i < ballCount; ++i) {
      var lengthRatio = animationValue * (1 - i / ballCount);
      var tangent =
          pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);
      var ballPosition = tangent!.position;
      canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
      canvas.drawCircle(
          Offset(size.width - tangent.position.dx,
              size.height - tangent.position.dy),
          ballRadius / (1 + i),
          paint);
    }
  }
}

椭圆运动Loading(渐变效果)

final ballCount = 6;
final ballRadius = boxSize / 15;
var ovalPath = Path()
  ..addOval(Rect.fromCenter(
      center: center, width: boxSize, height: boxSize / 1.5));
paint.shader = LinearGradient(
  begin: Alignment.topLeft,
  end: Alignment.bottomRight,
  colors: [this.foregroundColor, this.bgColor],
).createShader(Offset.zero & size);
var ovalMetrics = ovalPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
  for (var i = 0; i < ballCount; ++i) {
    var lengthRatio = animationValue * (1 - i / ballCount);
    var tangent =
        pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);
    var ballPosition = tangent!.position;
    canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
    canvas.drawCircle(
        Offset(size.width - tangent.position.dx,
            size.height - tangent.position.dy),
        ballRadius / (1 + i),
        paint);
  }
}

贝塞尔曲线Loading

首先是构建贝塞尔曲线Path
var bezierPath = Path()
  ..moveTo(size.width / 2 - boxSize / 2, center.dy)
  ..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy - boxSize / 4,
      size.width / 2, center.dy)
  ..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy + boxSize / 4,
      size.width / 2 + boxSize / 2, center.dy)
  ..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy - boxSize / 4,
      size.width / 2, center.dy)
  ..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy + boxSize / 4,
      size.width / 2 - boxSize / 2, center.dy);
实心圆
var ovalMetrics = bezierPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
  for (var i = 0; i < ballCount; ++i) {
    var lengthRatio = animationValue * (1 - i / ballCount);
    var tangent =
        pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);
    var ballPosition = tangent!.position;
    canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
    canvas.drawCircle(
        Offset(size.width - tangent.position.dx,
            size.height - tangent.position.dy),
        ballRadius / (1 + i),
        paint);
  }
}
/*
改变运动方向
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
    pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);
var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(Offset(tangent.position.dy, tangent.position.dx),
    ballRadius / (1 + i), paint);
*/

使用

class _LoadingDemoState extends State<LoadingDemo> {
  var loaded = false;
  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 5), () {
      setState(() {
        loaded = true;
      });
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Loading 使用'),
      ),
      body: Center(
        child: loaded
            ? Image.asset(
                'images/beauty.jpeg',
                width: 100.0,
              )
            : LoadingAnimations(
                foregroundColor: Colors.blue,
                bgColor: Colors.white,
                size: 100.0,
              ),
      ),
    );
  }

2. 日期组件

var now=new DateTime.now();  // 当前时间。2050-01-01 00:00:01.001
var time=now.millisecondsSinceEpoch;  // 13位时间戳,ms。日期转时间戳
var date=DateTime.fromMillisecondsSinceEpoch(time);  // 时间戳转日期,2050-01-01 00:00:01.001

intl 国际化包

DateFormat.yMMMd().format(DateTime.now())

date_format 三方库(日期格式化)

// 2050年01月01
formatDate(DateTime.now(),[yyyy,'年',mm,'月',dd]);  
Android
iOS

日历组件、时间组件

日历组件

  var _datetime=DateTime.now();
  _showDatePicker() async{  // 返回Future<void>
    var date=await showDatePicker(
      context: context,
      initialDate: _datetime, // 当前日期(初始日期)
      firstDate: DateTime(1900), // 最早日期
      lastDate: DateTime(2100), // 最晚日期。_datetime.add(Duration(days: 30),), // 未来30天可选
      // locale: Locale('zh'),    // 当前环境不是中文时,强制使用中文。需要首先国家化Material组件。
    );  // 选择完后才会继续向下执行
    if(!date)return;
    print(date);
    setState(() {
      _datetime=date;
    });
  }

时间组件

  var _nowTIme=TimeOfDay(hour: 8,minute: 0);
  _showTimePicker() async{
    var time=await showTimePicker(
      context: context,
      initialTime: _nowTIme, // 当前日期(初始日期)
    );
    if(!time)return;
    print(time);
    setState(() {
      _nowTIme=time;
    });
  }
  // _nowTIme.format(context) 

iOS风格的日历选择器需要使用showCupertinoModalPopup方法和CupertinoDatePicker组件来实现:

Future<DateTime> _showDatePicker2() {
  var date = DateTime.now();
  return showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
      return SizedBox(
        height: 200,
        child: CupertinoDatePicker(
          mode: CupertinoDatePickerMode.dateAndTime,
          minimumDate: date,
          maximumDate: date.add(
            Duration(days: 30),
          ),
          maximumYear: date.year + 1,
          onDateTimeChanged: (DateTime value) {
            print(value);
          },
        ),
      );
    },
  );
}

三方组件(flutter_cupertino_date_picker)

需要先依赖包

  var _datetime=DateTime.now();
  _showDatePicker(){
    DatePicker.showDatePicker(
      context,
      picherTheme: DateTimePickerTheme(
        showTitle: true, // 是否显示标题
        confirm: Text('确定',style:TextStyle(color: Colors.red)),
        cancel: Text('取消',style:TextStyle(color: Colors.blue)),
      ),
      minDateTime: DateTime.parse('1900-01-01') , // 最早日期
      maxDateTime: DateTime.parse('2100-01-01') , // 最晚日期
      initialDateTime: _datetime, // 当前日期(初始日期)
      dateFormat:"yyyy-MM-dd",  // 年月日
      // dateFormat:"yyyy-MM-dd EEE,H时:m分,",  // 年月日周时分秒
      // pickerMode: DateTimePickerMode.datetime,  //  年月日周时分秒
      locale:DateTimePickerLocale.zh_cn,
      onCancel:(){

      },
      onChange:(dateTime,List<int> index){
        setDate((){
          _datetime=dateTime;
        });
      },
      onConfirm:(dateTime,List<int> index){
        setDate((){
          _datetime=dateTime;
        });
      },
    );  
  }

3. AspectRatio 宽高比组件

设置子元素宽高比(宽度尽可能扩展,高度由宽高比决定)。

  AspectRatio(
    aspectRatio: 2.0/1.0,   // 宽高比
    child: Container( // 子组件
      color: Colors.blue,
    ),
  )

4. Card 卡片组件(Meterial组件库)

内容不能滚动,需要在MeterialApp内使用。

  Card({
    Key? key,
    this.color,    // 背景色
    this.shadowColor,  // 阴影色
    this.elevation, // 阴影
    this.shape,  // 形状
    this.borderOnForeground = true,
    this.margin,  // 外边距
    this.clipBehavior,
    this.child,
    this.semanticContainer = true,
  })

示例

  Card(
    margin: EdgeInsets.all(10.0), // 外边距
    child: Column( // 子组件
      children: <Widget>[
        ListTile(
          title: Text("张三"),
          subtitle: Text("男"),
        )
      ],
    ),
    // shape: ,  // 默认圆角阴影
  )

5. 剪裁

用于对子组件进行剪裁

1. ClipOval
  圆形(正方形时),椭圆(矩形时)
2. ClipRRect    
  圆角矩形
3. ClipRect 
  溢出部分剪裁(剪裁子组件到实际占用的矩形大小)
4.ClipPath
  按照自定义路径剪裁
  继承CustomClipper<Path>自定义剪裁类,重写getClip方法返回自定义Path
  ClipOval({Key? key, this.clipper, this.clipBehavior = Clip.antiAlias, Widget? child})

  ClipRRect({
    Key? key,
    this.borderRadius = BorderRadius.zero,
    this.clipper,
    this.clipBehavior = Clip.antiAlias,
    Widget? child,
  })

  ClipRect({ Key? key, this.clipper, this.clipBehavior = Clip.hardEdge, Widget? child })

示例

import 'package:flutter/material.dart';
class ClipTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 头像  
    Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
    return Center(
      child: Column(
        children: <Widget>[
          avatar, // 不剪裁
          ClipOval(child: avatar), // 剪裁为圆形
          ClipRRect( // 剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ), 
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Align(
                alignment: Alignment.topLeft,
                widthFactor: .5,// 宽度设为原来宽度一半,另一半会溢出
                child: avatar,
              ),
              Text("你好世界", style: TextStyle(color: Colors.green),)
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(  // 将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5,  // 宽度设为原来宽度一半
                  child: avatar,
                ),
              ),
              Text("你好世界",style: TextStyle(color: Colors.green))
            ],
          ),
        ],
      ),
    );
  }
}
运行结果

示例(自定义剪裁区域)

截取图片中部40×30像素的范围

1. 首先,自定义一个继承自CustomClipper的子类:
class MyClipper extends CustomClipper<Rect> {
  @override
  //  getClip()是用于获取剪裁区域的接口,由于图片大小是60×60,计算之后即图片中部40×30像素的范围。
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);
  @override
  // shouldReclip() 接口决定是否重新剪裁。
  // 如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}

2. 然后,通过ClipRect来执行剪裁,为了看清图片实际所占用的位置,设置一个红色背景:
DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
      clipper: MyClipper(), // 使用自定义的clipper
      child: avatar
  ),
)
剪裁成功了,但是图片所占用的空间大小仍然是60×60(红色区域),这是因为剪裁是在layout完成后的绘制阶段进行的,所以不会影响组件的大小,这和Transform原理是相似的。
运行结果

6. 标签 Chip、ActionChip、FilterChip、ChoiceChip

标签
  Chip({
    Key? key,
    this.avatar,  // 在左侧显示
    required this.label,  // 文本
    this.labelStyle,  //
    this.labelPadding,  // 
    this.deleteIcon,  // 右侧删除图标
    this.onDeleted,    // 点击右侧删除图标后回调
    this.deleteIconColor,  // 右侧删除图标颜色
    this.useDeleteButtonTooltip = true,  // 长按右侧删除图标是否提示
    this.deleteButtonTooltipMessage,  // 长按右侧删除图标的提示文本
    this.side,
    this.shape,
    this.clipBehavior = Clip.none,
    this.focusNode,
    this.autofocus = false,
    this.backgroundColor,  // 背景色
    this.padding,
    this.visualDensity,
    this.materialTapTargetSize,
    this.elevation,
    this.shadowColor,
  }) 

可点击的标签
  ActionChip({
    Key? key,
    this.avatar,
    required this.label,
    this.labelStyle,
    this.labelPadding,
    required this.onPressed,  // 点击后回调
    this.pressElevation,
    this.tooltip,
    this.side,
    this.shape,
    this.clipBehavior = Clip.none,
    this.focusNode,
    this.autofocus = false,
    this.backgroundColor,
    this.padding,
    this.visualDensity,
    this.materialTapTargetSize,
    this.elevation,
    this.shadowColor,
  })

选中后左侧出现对勾
  FilterChip({
    Key? key,
    this.avatar,
    required this.label,  // 文本
    this.labelStyle,  //
    this.labelPadding,  //
    this.selected = false,    // 是否选中
    required this.onSelected,  // 选中状态改变后回调
    this.pressElevation,
    this.disabledColor,
    this.selectedColor,  // 选中背景色
    this.tooltip,
    this.side,
    this.shape,
    this.clipBehavior = Clip.none,
    this.focusNode,
    this.autofocus = false,
    this.backgroundColor,
    this.padding,
    this.visualDensity,
    this.materialTapTargetSize,
    this.elevation,
    this.shadowColor,  // 阴影色
    this.selectedShadowColor,// 选中后的阴影色
    this.showCheckmark,
    this.checkmarkColor,
    this.avatarBorder = const CircleBorder(),
  }) 

单选标签
  const ChoiceChip({
    Key? key,
    this.avatar,
    required this.label,  // 文本
    this.labelStyle,  //
    this.labelPadding,  // 
    this.onSelected,  // 选中状态改变后回调
    this.pressElevation,
    required this.selected,  // 是否选中
    this.selectedColor,  // 选中背景色
    this.disabledColor,
    this.tooltip,
    this.side,
    this.shape,
    this.clipBehavior = Clip.none,
    this.focusNode,
    this.autofocus = false,
    this.backgroundColor,
    this.padding,
    this.visualDensity,
    this.materialTapTargetSize,
    this.elevation,
    this.shadowColor,
    this.selectedShadowColor,
    this.avatarBorder = const CircleBorder(),
  })

7. 表格 DataTable、PaginatedDataTable

  DataTable({
    Key? key,
    required this.columns,  // 顶部的栏目内容
    this.sortColumnIndex,  // 排序栏的索引号(右侧出现箭头)
    this.sortAscending = true,  // 排序方式,true升序
    this.onSelectAll,
    this.decoration,
    this.dataRowColor,
    this.dataRowHeight,
    this.dataTextStyle,
    this.headingRowColor,
    this.headingRowHeight,
    this.headingTextStyle,
    this.horizontalMargin,
    this.columnSpacing,
    this.showCheckboxColumn = true,
    this.showBottomBorder = false,
    this.dividerThickness,
    required this.rows,  // 每一行内容
  })

 栏目
  DataColumn({
    required this.label,  // 文本
    this.tooltip,
    this.numeric = false,
    this.onSort,  // 点击后回调,进行排序,参数(index,isAscending)
  })

 一行
  DataRow({
    this.key,
    this.selected = false,    // 是否选中,左侧会有选择框
    this.onSelectChanged,  // 选中状态改变后回调
    this.color,  // 背景色
    required this.cells,  // 
  }) 
  DataRow.byIndex({
    int? index,
    this.selected = false,
    this.onSelectChanged,
    this.color,
    required this.cells,
  })

 单元格
 DataCell(
    this.child, {
    this.placeholder = false,
    this.showEditIcon = false,
    this.onTap,
  })
分页表格

  PaginatedDataTable({
    Key? key,
    this.header,  // 表格标题
    this.actions,
    required this.columns,  // 顶部栏目
    this.sortColumnIndex,  // 
    this.sortAscending = true,  // 
    this.onSelectAll,
    this.dataRowHeight = kMinInteractiveDimension,
    this.headingRowHeight = 56.0,
    this.horizontalMargin = 24.0,
    this.columnSpacing = 56.0,
    this.showCheckboxColumn = true,
    this.initialFirstRowIndex = 0,
    this.onPageChanged,  // 页面改变后回调
    this.rowsPerPage = defaultRowsPerPage,
    this.availableRowsPerPage = const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
    this.onRowsPerPageChanged,
    this.dragStartBehavior = DragStartBehavior.start,
    required this.source,  // 创建类,继承DataTableSource,实现相关方法。
  })

8. 分割线Divider

  Divider({
    Key? key,
    this.height,    // 高度
    this.thickness,
    this.indent,  // 缩进
    this.endIndent,
    this.color,  // 背景色
  })

9. ListTile

1. leading
  头部widget
2. trailing
  尾部widget
3. minLeadingWidth
  头部最小宽(默认40.0)
4. title
  标题
5. subtitle
  副标题
6. minVerticalPadding
  最小的纵向间距(默认4.0)
7. horizontalTitleGap
  标题距离头部、尾部的距离(默认16.0)
8. isThreeLine

9. tileColor
  未选中的背景色
10. selectedTileColor
  选中时的背景色
11. selected
  是否选中(默认false)
12. hoverColor
  指针悬停时的背景色
13. focusColor
  获取焦点时的背景色
14. autofocus
  是否自动获取焦点(默认false)
15. focusNode
  焦点 
16. mouseCursor
  在内部或悬停时的鼠标样式
17. shape
  形状
18. visualDensity
  紧凑程度
19. dense

20. contentPadding
  内部边距
21. onTap
  点击回调
22. onLongPress
  长按回调
23. enableFeedback
  是否提供听觉/触觉反馈
24. enabled
  是否可交互(默认true)

示例

ListTile(
    leading: const Icon(Icons.add),
    title: const Text('Add account'),
),

10. ButtonBarTheme

继承自InheritedWidget

  ButtonBarTheme({
    Key? key,
    required this.data,
    required Widget child,
  })

  const 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>[],
  })

11. Material

Material({
    Key? key,
    this.type = MaterialType.canvas,
    this.elevation = 0.0,    // 阴影
    this.color,    // 
    this.shadowColor,  // 阴影色
    this.textStyle,  // 文本样式
    this.borderRadius,  // 圆角
    this.shape,  // 形状
    this.borderOnForeground = true,
    this.clipBehavior = Clip.none,
    this.animationDuration = kThemeChangeDuration,
    this.child,
  })

Spacer

Visibility

IndexedStack


CircleAvatar(圆形头像)

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

推荐阅读更多精彩内容