Flutter实现气泡提示框学习

前置知识点学习

GlobalKey

`GlobalKey` 是 Flutter 中一个非常重要的概念,它用于唯一标识 widget 树中的特定 widget,并提供对该 widget 的访问。这在需要跨越 widget 树边界进行交互或在 widget 树重建时保持状态时尤其有用。

`GlobalKey` 的作用

  1. 唯一标识:`GlobalKey` 可以在 widget 树中唯一标识一个 widget 实例。这在需要在多个地方引用同一个 widget 时特别有用。
  2. 访问状态:可以通过 `GlobalKey` 访问 `StatefulWidget` 的状态对象。这允许你在 widget 树之外操作该 widget 的状态。
  3. 在重建时保持状态:当 widget 树重建时,`GlobalKey` 可以确保与该 key 关联的 widget 保持其状态不变。通常情况下,Flutter 会根据位置和类型重新创建 widget,但使用 `GlobalKey` 可以避免这种情况。
  4. 跨越 widget 树边界的操作:可以用于在 widget 树的一个部分与另一个部分之间传递信息或触发操作,这在复杂的 UI 中非常有用。

`GlobalKey` 案例

Dart 复制代码
import 'package:flutter/material.dart';

class MyGlobalKeyPage extends StatelessWidget {
  MyGlobalKeyPage({super.key});

  final GlobalKey<_MySimpleWidgetState> _key =
      GlobalKey<_MySimpleWidgetState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("MyBubbleDemoPage"),
        ),
        body: Container(
          width: MediaQuery.sizeOf(context).width,
          height: MediaQuery.sizeOf(context).height,
          margin: const EdgeInsets.all(15),
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                MySimpleWidget(key: _key),
                ElevatedButton(
                  onPressed: () {
                    // 閫氳繃 GlobalKey 璁块棶 MyWidget 鐨勭姸鎬侊紝骞惰皟鐢ㄥ叾鏂规硶
                    _key.currentState?.changeText();
                  },
                  child: Text('Call Method on MySimpleWidget'),
                ),
              ],
            ),
          ),
        ));
  }
}

class MySimpleWidget extends StatefulWidget {
  const MySimpleWidget({super.key});

  @override
  _MySimpleWidgetState createState() {
    return _MySimpleWidgetState();
  }
}

class _MySimpleWidgetState extends State<MySimpleWidget> {
  String _text = "Hello GlobalKey";

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

  void changeText() {
    setState(() {
      _text = "Hello TEXT has changed";
    });
  

`GlobalKey`在示例中的作用

  • 标识和访问:`GlobalKey` 用于标识 `MySimpleWidget` 实例,并通过 `_key.currentState` 访问其状态和方法。
  • 跨 widget 树操作:通过 `ElevatedButton`,我们可以从 `MyGlobalKeyPage
    ` 中访问并操作 `MySimpleWidget` 的状态。
注意事项
  1. 唯一性:`GlobalKey` 必须在 widget 树中唯一使用。多个 widget 共享同一个 `GlobalKey` 会导致异常。
  2. 性能开销:由于 `GlobalKey` 会阻止 widget 的优化重建,过多使用可能影响性能。应在必要时使用。
  3. 避免滥用:虽然 `GlobalKey` 很强大,但应尽量在局部范围内使用,避免在整个应用中广泛使用。

通过正确使用 `GlobalKey`,你可以在 Flutter 应用中实现更复杂的状态管理和交互逻辑。它为在 widget 树结构中实现灵活的组件通信提供了强有力的支持。

flutter报错

Can't define the 'const' constructor because the field '_key' is initialized with a non-constant value.

这个错误信息是因为在 Flutter 中,你试图在一个 `const` 构造函数中初始化一个字段,而这个字段被赋予了一个非常量的值。

问题原因

在 Flutter 中,`const` 构造函数要求所有字段都用常量值初始化。`GlobalKey` 和其他类似的对象在 Dart 中不是常量,因为它们在运行时创建并分配内存。因此,它们不能在 `const` 构造函数中被用作初始值。

解决方案

要解决这个问题,你需要确保在构造函数中,不要将 `GlobalKey` 或其他运行时生成的对象用于 `const` 构造函数。通常的做法是将这些对象作为普通的变量,而不是在 `const` 构造函数中初始化。

示例修正

假设有以下代码导致了这个错误:

Dart 复制代码
class MyWidget extends StatelessWidget {
  final GlobalKey _key = GlobalKey(); // 这里不能使用 const

  const MyWidget({Key? key}) : super(key: key); // 错误的用法
}

可以通过以下方式修正:

Dart 复制代码
class MyWidget extends StatelessWidget {
  final GlobalKey _key = GlobalKey(); // 不使用 const

  MyWidget({Key? key}) : super(key: key); // 修改为非 const 构造函数
}

或者,如果你确实需要 `MyWidget` 是一个常量构造函数,你需要移除 `GlobalKey` 的初始化:

Dart 复制代码
class MyWidget extends StatelessWidget {
  final GlobalKey _key;

  const MyWidget({Key? key, required GlobalKey keyParam})
      : _key = keyParam, // 通过参数传递而不是初始化
        super(key: key);
}

然后在使用 `MyWidget` 时,传递一个 `GlobalKey` 实例:

Dart 复制代码
final myKey = GlobalKey();

MyWidget(key: myKey, keyParam: myKey);
总结
  • `const` 构造函数中的字段必须是常量:确保所有字段初始化都是常量表达式。
  • `GlobalKey` 不能是常量:因为它们在运行时创建,所以不能在 `const` 构造函数中直接初始化。
  • 通过参数传递可变对象:如果需要使用 `const` 构造函数,可以通过参数将 `GlobalKey` 传递进去,而不是在类内部直接初始化。

常量构造函数

在 Flutter 中,构造函数用于初始化类的实例。`const MyWidget({Key? key}) : super(key: key);` 是一个常量构造函数的示例,用于初始化 `MyWidget` 类的实例。让我们逐步解析这个构造函数的含义:

`const` 关键字
  1. 常量构造函数:使用 `const` 关键字定义的构造函数称为常量构造函数。它允许在编译时创建不可变的常量实例。
  2. 优化:在使用常量构造函数初始化的对象时,如果这些对象的所有字段都是常量,Flutter 可以在编译时对这些对象进行优化,以减少内存使用和提高性能。
`MyWidget({Key? key})`
  1. 命名构造函数:`MyWidget` 是类的构造函数。括号中的 `{Key? key}` 是一个可选的命名参数。
  2. `Key` 参数:`Key` 是 Flutter 中用来标识 widget 的唯一标识符,用于在 widget 树更新时保持 widget 的状态。`Key?` 表示这个参数是可选的,并且可以为 null。
`: super(key: key)`
  1. 初始化列表:冒号 `:` 后面的是初始化列表,用于在构造函数体执行之前初始化父类的字段。
  2. `super(key: key)`:调用父类(`StatelessWidget` 或 `StatefulWidget`)的构造函数,并传递 `key` 参数。这是因为 `StatelessWidget` 和 `StatefulWidget` 的父类 `Widget` 定义了一个 `key` 参数,用于管理 widget 的标识。

SingleTickerProviderStateMixin

`SingleTickerProviderStateMixin` 是 Flutter 中提供的一种混合(mixin),用于创建动画时管理 `Ticker` 的生命周期。它通常与 `StatefulWidget` 一起使用,以有效地处理动画帧。

什么是 `Ticker`?

在 Flutter 中,`Ticker` 是一个触发动画的计时器,每帧都会调用一次回调函数。它类似于一个心跳信号,让动画在每个屏幕刷新周期内前进一小步。

`SingleTickerProviderStateMixin` 的作用
  • 管理 `Ticker`:`SingleTickerProviderStateMixin` 使 `State` 类成为一个 `TickerProvider`,这意味着它能够提供 `Ticker` 对象。这个 `Ticker` 用于驱动动画。
  • 高效地管理资源:通过提供一个 `Ticker`,`SingleTickerProviderStateMixin` 帮助确保动画在不需要时被正确地停止和释放资源。
  • 简单便捷:使用这个 mixin,可以轻松创建一个 `AnimationController`,而不需要手动管理 `Ticker` 的生命周期。
使用场景

通常在需要创建动画时,你会在 `StatefulWidget` 的 `State` 类中使用它。以下是一个基本的使用示例:

Dart 复制代码
import 'package:flutter/material.dart';

class MyAnimateWidgetPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("MyBubbleDemoPage"),
        ),
        body: Container(
          width: MediaQuery.sizeOf(context).width,
          height: MediaQuery.sizeOf(context).height,
          margin: const EdgeInsets.all(15),
          child: const MyAnimateWidget(),
        ));
  }
}

class MyAnimateWidget extends StatefulWidget {
  const MyAnimateWidget({super.key});

  @override
  _MyAnimateWidgetState createState() => _MyAnimateWidgetState();
}

class _MyAnimateWidgetState extends State<MyAnimateWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: FadeTransition(
          opacity: _controller,
          child: const FlutterLogo(size: 100.0),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    //vsync: this => 需要一个 TickerProvider,这里就是 SingleTickerProviderStateMixin 提供的
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    _controller.repeat(reverse: true);
  }

  @override
  void dispose() {
    // 记得在 dispose 方法中释放 AnimationController
    _controller.dispose();
    super.dispose();
  }
}
代码解析
  • `with SingleTickerProviderStateMixin`:将 `SingleTickerProviderStateMixin` 添加到 `State` 类中,使其成为一个 `TickerProvider`。
  • `vsync: this`:在创建 `AnimationController` 时,`vsync` 参数需要一个 `TickerProvider`。这里通过 `this` 传递当前 `State` 实例。
  • `_controller.repeat(reverse: true)`:启动动画控制器,使动画在两秒钟内从开始到结束,然后反向重复。
  • `dispose` 方法:在组件销毁时,调用 `_controller.dispose()` 释放资源,防止内存泄漏。

`SingleTickerProviderStateMixin` 是一个便捷的工具,用于在 Flutter 中实现动画。它让 `State` 类能够提供 `Ticker`,从而驱动动画控制器。通过这种方式,你可以轻松地创建高效且可管理的动画效果。对于需要多个 `Ticker` 的复杂场景,可以考虑使用 `TickerProviderStateMixin`。

MediaQuery.sizeOf(context)

`MediaQuery.sizeOf(context)` 是在 Flutter 中用于获取当前屏幕或父 widget 可用空间大小的一个方法。这是 Flutter 3.7 引入的一个静态方法,用于简化对 `MediaQuery` 的访问。

用途

  • 获取屏幕尺寸:它返回一个 `Size` 对象,包含当前设备屏幕的宽度和高度。
  • 适配布局:在构建响应式布局时,可以使用这个方法来获取屏幕尺寸并调整 widget 的大小和位置。

工作原理

  • `BuildContext`:它是 `MediaQuery` 用于查找 widget 树中与之关联的 `MediaQueryData` 对象的上下文。
  • `MediaQuery.sizeOf(context)`:该方法通过 `BuildContext` 获取 `MediaQueryData`,然后从中提取屏幕尺寸信息。
使用示例
Dart 复制代码
import 'package:flutter/material.dart';

class MyResponsiveWidgetPage extends StatelessWidget {
  const MyResponsiveWidgetPage({super.key});

  @override
  Widget build(BuildContext context) {
    // 使用 MediaQuery.sizeOf(context) 获取屏幕尺寸
    Size screenSize = MediaQuery.sizeOf(context);
    return Scaffold(
      appBar: AppBar(title: Text('Responsive Example')),
      body: Center(
        child: Container(
          width: screenSize.width * 0.8, // 宽度是屏幕宽度的 80%
          height: screenSize.height * 0.5, // 高度是屏幕高度的 50%
          color: Colors.blue,
          child: Center(
            child: Text(
              'Responsive Container',
              style: TextStyle(color: Colors.white, fontSize: 20),
            ),
          ),
        ),
      ),
    );
  }
}
注意事项
  1. BuildContext 的位置:`MediaQuery.sizeOf(context)` 所用的 `context` 必须是 widget 树中包含 `MediaQuery` 的上下文。通常这意味着它是在 `MaterialApp` 或 `CupertinoApp` 的子级中。
  2. 响应式设计:虽然可以简单地使用这个方法来适配屏幕大小,但在复杂的布局中,考虑使用其他响应式设计工具,如 `LayoutBuilder` 或 `FractionallySizedBox`,以更好地适配各种屏幕尺寸。
  3. 更新:请确保你的 Flutter SDK 版本支持 `MediaQuery.sizeOf(context)`,因为这是一种相对较新的方法。

通过使用 `MediaQuery.sizeOf(context)`,你可以轻松地访问设备的屏幕尺寸信息,从而为用户提供更好的响应式界面布局。

Dart 复制代码
import 'package:flutter/material.dart';

class MyResponsiveWidgetPage extends StatelessWidget {
  const MyResponsiveWidgetPage({super.key});

  @override
  Widget build(BuildContext context) {
    // 使用 MediaQuery.sizeOf(context) 获取屏幕尺寸
    Size screenSize = MediaQuery.sizeOf(context);
    return Scaffold(
      appBar: AppBar(title: Text('Responsive Example')),
      body: Center(
        child: Container(
          width: screenSize.width * 0.8, // 宽度是屏幕宽度的 80%
          height: screenSize.height * 0.5, // 高度是屏幕高度的 50%
          color: Colors.blue,
          child: Center(
            child: Text(
              'Responsive Container',
              style: TextStyle(color: Colors.white, fontSize: 20),
            ),
          ),
        ),
      ),
    );
  }
}

GestureDetector

`GestureDetector` 是 Flutter 中一个非常重要的组件,用于检测用户在设备屏幕上的手势。它提供了一种简单的方法来监听并响应用户的触摸、拖动、点击等交互事件。

主要功能

`GestureDetector` 提供了一系列回调函数,允许你处理不同类型的手势。以下是一些常见的手势和对应的回调:

点击手势:
  • `onTap`: 用户轻触屏幕时触发。
  • `onDoubleTap`: 用户快速连续点击两次时触发。
  • `onLongPress`: 用户长按屏幕时触发。
拖动手势:
  • `onPanStart`: 用户开始拖动时触发。
  • `onPanUpdate`: 用户拖动时持续触发。
  • `onPanEnd`: 用户拖动结束时触发。
缩放手势:
  • `onScaleStart`: 缩放手势开始时触发。
  • `onScaleUpdate`: 缩放手势更新时持续触发。
  • `onScaleEnd`: 缩放手势结束时触发。
其他手势:
  • `onVerticalDragStart`, `onVerticalDragUpdate`, `onVerticalDragEnd`: 垂直拖动相关的手势。
  • `onHorizontalDragStart`, `onHorizontalDragUpdate`, `onHorizontalDragEnd`: 水平拖动相关的手势。
使用示例
Dart 复制代码
import 'package:flutter/material.dart';

class GestureExamplePage extends StatelessWidget {
  const GestureExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GestureDetector Example')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            print('Container tapped!');
          },
          onPanUpdate: (details) {
            print('Dragging: ${details.localPosition}');
          },
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(
              child: Text('Tap or drag me!',
                  style: TextStyle(color: Colors.white, fontSize: 16)),
            ),
          ),
        ),
      ),
    );
  }
}

细节与注意事项

  1. 透明度:`GestureDetector` 默认只会在非透明的地方响应手势。如果你需要在透明区域也检测手势,可以设置 `behavior: HitTestBehavior.translucent`。
  2. 优先级:如果多个手势检测器重叠,Flutter 会根据其内部的手势识别器机制来确定哪个手势优先处理。
  3. 组合手势:`GestureDetector` 可以同时检测多个手势,例如你可以同时监听 `onTap` 和 `onDoubleTap`,但需要注意可能的冲突。
  4. 性能:在复杂的布局中,需要注意手势检测的性能开销。尽量在需要的地方使用 `GestureDetector`,避免过多的嵌套。
  5. 默认行为:`GestureDetector` 不会改变子组件的外观或行为,它仅提供手势识别能力,你需要在回调函数中定义具体行为。

通过 `GestureDetector`,Flutter 开发者可以轻松实现与用户的交互,处理各种复杂的手势需求,从而增强应用的用户体验。

onPanUpdate

`onPanUpdate` 是 `GestureDetector` 组件中的一个回调,用于处理用户的拖动(或平移)手势。当用户在屏幕上拖动时,`onPanUpdate` 会持续触发,并提供有关拖动事件的信息。

主要特性
  • 持续触发:当用户在屏幕上拖动时,每次移动都会触发 `onPanUpdate`,这使得你可以跟踪拖动路径的每一个点。
  • 事件细节:回调函数接收一个 `DragUpdateDetails` 对象,它包含有关拖动的详细信息。
`DragUpdateDetails` 的重要属性
  • `globalPosition`:用户触摸点相对于整个屏幕的坐标。
  • `localPosition`:用户触摸点相对于容器的坐标(即触摸点在检测手势的组件内的坐标)。
  • `delta`:用户自上次更新以来移动的距离偏移量。可以用来计算拖动的速度或方向。
  • `primaryDelta`:如果拖动是单向的(水平或垂直),这将返回沿主要轴的偏移量。
使用示例
Dart 复制代码
import 'package:flutter/material.dart';

class PanUpdateExamplePage extends StatelessWidget {
  const PanUpdateExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('onPanUpdate Example')),
      body: const Center(
        child: PanUpdateExampleWidget(),
      ),
    );
  }
}

class PanUpdateExampleWidget extends StatefulWidget {
  const PanUpdateExampleWidget({super.key});

  @override
  _PanUpdateExampleState createState() {
    return _PanUpdateExampleState();
  }
}

class _PanUpdateExampleState extends State<PanUpdateExampleWidget> {
  Offset _position = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          _position += details.delta;
        });
      },
      child: Stack(
        children: [
          Positioned(
              left: _position.dx,
              top: _position.dy,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.blue,
                child: const Center(
                  child: Text(
                    'Drag me',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ))
        ],
      ),
    );
  }
}

onPanStart

`onPanStart` 是 `GestureDetector` 组件中的一个回调,用于处理用户的拖动(或平移)手势的开始事件。当用户在屏幕上开始拖动时,`onPanStart` 会被触发。它是实现拖动交互的第一步,通常与 `onPanUpdate` 和 `onPanEnd` 一起使用来处理完整的拖动事件。

主要特性
  • 触发时机:当用户用手指触摸屏幕并开始拖动时立即触发。
  • 事件细节:回调函数接收一个 `DragStartDetails` 对象,提供有关手势开始的详细信息。
`DragStartDetails` 的重要属性
  • `globalPosition`:用户触摸点相对于整个屏幕的坐标。
  • `localPosition`:用户触摸点相对于手势检测区域的坐标(即触摸点在 `GestureDetector` 的子组件内的坐标)。
使用案例
Dart 复制代码
import 'package:flutter/material.dart';

class PanStartExamplePage extends StatelessWidget {
  const PanStartExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('onPanStart Example')),
      body: const Center(
        child: PanStartExampleWidget(),
      ),
    );
  }
}

class PanStartExampleWidget extends StatefulWidget {
  const PanStartExampleWidget({super.key});

  @override
  _PanStartExampleState createState() {
    return _PanStartExampleState();
  }
}

class _PanStartExampleState extends State<PanStartExampleWidget> {
  Offset _startPosition = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        setState(() {
          _startPosition = details.localPosition;
        });
      },
      child: Container(
        width: 200,
        height: 200,
        color: Colors.blue,
        child: Center(
          child: Text('Start Position: $_startPosition',
              style: const TextStyle(color: Colors.white)),
        ),
      ),
    );
  }
}

CustomPaint

`CustomPaint` 是 Flutter 中的一个强大组件,用于在屏幕上自定义绘制内容。通过 `CustomPaint`,你可以在 Flutter 应用中创建复杂的图形和视觉效果,超越标准的 UI 组件。

主要组件
  • `CustomPaint`:这是一个 widget,它包含一个 `painter` 和一个 `child`。`painter` 用于自定义绘制,`child` 是可选的,在绘制内容之上显示。
  • `CustomPainter`:一个抽象类,需要实现其中的 `paint` 和 `shouldRepaint` 方法。`paint` 方法定义了具体的绘制逻辑,而 `shouldRepaint` 决定了何时需要重绘。
关键方法
  • paint(Canvas canvas, Size size)`:在此方法中实现具体的绘制逻辑。通过 `Canvas` 对象在给定的 `Size` 上绘制图形。
  • `shouldRepaint(CustomPainter oldDelegate)`:返回一个布尔值,指示当 `CustomPaint` 的配置变化时是否需要重绘。通常在绘制逻辑或者输入参数变化时返回 `true`。
使用案例
Dart 复制代码
import 'package:flutter/material.dart';

class CirclePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;
    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false; // 如果绘制内容不变,返回 false 以提高性能
  }
}
Dart 复制代码
import 'package:flutter/material.dart';
import 'package:gsy_flutter_demo/widget/circle_painter.dart';

class CustomPaintExamplePage extends StatelessWidget {
  const CustomPaintExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('CustomPaint Example')),
      body: Center(
        child: CustomPaint(
          size: const Size(100, 100),
          painter: CirclePainter(),
        ),
      ),
    );
  }
}
注意事项
  1. 性能:由于自定义绘制可能涉及大量计算,因此要小心处理绘制逻辑,确保 `shouldRepaint` 返回正确的值以避免不必要的重绘。
  2. 绘制顺序:`CustomPaint` 的 `child` 会在 `painter` 绘制之后显示,这意味着绘制的内容将在 `child` 背后。
  3. 交互:`CustomPaint` 不支持手势检测。如果需要交互,可以将其包裹在 `GestureDetector` 中。

Expanded

`Expanded` 是 Flutter 中一个非常常用的 widget,通常用于在 `Row`, `Column` 或 `Flex` 组件中按比例扩展子组件的可用空间。它通过灵活地分配空间,帮助创建响应式的布局。

主要特性
  • 自动填充空间:`Expanded` 使用 `flex` 属性占据父组件中未被占用的可用空间。
  • 比例分配:通过 `flex` 属性,可以为多个 `Expanded` 组件指定占用空间的比例。
  • 简化布局:在需要将多个子组件平分或按比例分配空间时,`Expanded` 是一个非常便利的工具。
使用示例
Dart 复制代码
import 'package:flutter/material.dart';

class ExpandExampleWidget extends StatelessWidget {
  const ExpandExampleWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Expanded Example"),
      ),
      body: Column(
        children: <Widget>[
          Container(
            color: Colors.red,
            height: 100,
            child: const Center(
              child: Text("Fixed Height"),
            ),
          ),
          Expanded(
            flex: 2,
            child: Container(
              color: Colors.green,
              child: const Center(child: Text('Expanded\nFlex: 2')),
            ),
          ),
          Expanded(
            flex: 1,
            child: Container(
              color: Colors.blue,
              child: const Center(child: Text('Expanded\nFlex: 1')),
            ),
          ),
        ],
      ),
    );
  }
}
注意事项
  1. 父组件限制:`Expanded` 只能用于 `Row`, `Column` 或 `Flex` 类型的父组件中,不能在其他布局(如 `Stack` 或 `ListView`)中使用。
  2. 可用空间:`Expanded` 只能填充父组件中未被占用的空间。因此,如果父组件没有剩余空间(比如 `Row` 中所有子组件都有固定宽度),`Expanded` 将不起作用。
  3. 嵌套使用:可以嵌套使用多个 `Expanded` 组件,以便在复杂布局中根据需要分配空间。
  4. 交替使用:与 `Flexible` 一起使用时,`Expanded` 会占用所有的可用空间,而 `Flexible` 则可以根据其子组件的大小来调整。

`Expanded` 是 Flutter 布局系统中的一个重要工具,尤其在构建响应式用户界面时非常有用。通过合理使用 `Expanded`,可以轻松实现子组件的动态布局和空间分配。它简化了构建灵活且美观。

EdgeInsets.only

`EdgeInsets.only` 是 Flutter 中用于创建具有特定边距的 `EdgeInsets` 对象的构造函数。`EdgeInsets` 是 Flutter 的布局系统中用于定义控件的内边距或外边距的一个类。通过 `EdgeInsets.only`,你可以为某个控件指定具体的边距值,只作用于指定的边。

主要特性
  • 指定边距:可以为四个边(左、上、右、下)中的任意一个或多个边指定具体的边距值。
  • 灵活控制:允许开发者精确控制每一边的间距,而不需要统一设置所有边的边距。
基本用法

`EdgeInsets.only` 可以在任何支持 `EdgeInsets` 的属性中使用,例如 `Padding`, `Margin`, `Container` 的 `padding` 和 `margin` 属性等。

使用案例

Dart 复制代码
import 'package:flutter/material.dart';

class EdgeInsetsOnlyExamplePage extends StatelessWidget {
  const EdgeInsetsOnlyExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('EdgeInsets.only Example')),
      body: Center(
        child: Container(
          color: Colors.blueAccent,
          child: const Padding(
            padding: EdgeInsets.only(left: 20.0, top: 10.0),
            child: Text(
              'Hello, Flutter!',
              style: TextStyle(color: Colors.white, fontSize: 24),
            ),
          ),
        ),
      ),
    );
  }
}
其他相关构造函数
  • `EdgeInsets.all(double value)`:为四个边设置相同的边距。
  • `EdgeInsets.symmetric({double vertical, double horizontal})`:同时为水平和垂直方向设置对称的边距。
  • `EdgeInsets.fromLTRB(double left, double top, double right, double bottom)`:为四个边分别设置具体的边距。
  • `EdgeInsets.zero`:用于设置没有边距的 `EdgeInsets`。
使用注意事项
  • 布局影响:边距的设置会影响布局,尤其是在复杂的布局中,要确保边距设置与设计需求一致。
  • 嵌套使用:可以将多个 `EdgeInsets` 结合使用,以实现更复杂的布局效果。
  • 响应式设计:在需要适配不同屏幕或设备时,可以结合 `MediaQuery` 动态调整 `EdgeInsets` 的值。

通过使用 `EdgeInsets.only`,开发者可以在布局中实现更精细的控制,确保每个控件的间距符合设计规范。它是 Flutter 布局系统中不可或缺的一部分,提供了灵活而强大的布局能力。

EdgeInsets.zero

`EdgeInsets.zero` 是 Flutter 中 `EdgeInsets` 类的一个常量,它表示没有边距(即上下左右的边距都为 0)。在布局中使用 `EdgeInsets.zero` 可以明确指定某个控件不需要额外的内边距或外边距。

主要用途
  • 消除默认边距:在某些情况下,控件可能会有默认的边距或内边距,通过使用 `EdgeInsets.zero` 可以去除这些默认的边距。
  • 明确意图:即使边距默认为 0,使用 `EdgeInsets.zero` 可以让代码更加清晰,表示边距的设计是经过有意定义的。
  • 条件布局:在需要根据条件动态设置边距时,可以方便地使用 `EdgeInsets.zero` 来代表没有边距的选项。
使用案例
Dart 复制代码
import 'package:flutter/material.dart';

class EdgeInsetsZeroExample extends StatelessWidget {
  const EdgeInsetsZeroExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('EdgeInsets.zero Example')),
      body: Center(
        child: Container(
          color: Colors.blueAccent,
          padding: EdgeInsets.zero,
          child: const Text(
            'No Padding',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      ),
    );
  }
}
其他相关用法
  • 与条件语句结合:在构建动态布局时,可以通过条件语句选择使用 `EdgeInsets.zero` 或其他 `EdgeInsets` 值。
Dart 复制代码
EdgeInsets padding = condition ? EdgeInsets.all(10.0) : EdgeInsets.zero;
  • 默认值清除:某些 Flutter 组件可能有默认的边距设置,通过使用 `EdgeInsets.zero` 可以显式地清除这些默认设置。
注意事项
  • 布局影响:使用 `EdgeInsets.zero` 会使得组件内容紧贴其父容器的边界,确保这是预期的效果。
  • 代码可读性:即使某个属性的默认值为 0,使用 `EdgeInsets.zero` 可以提高代码的可读性和可维护性,明确表明设计选择。

通过使用 `EdgeInsets.zero`,开发者可以在布局中实现无边距的设计,确保控件准确地呈现设计意图。这在需要精确控制 UI 元素位置的场景中特别有用。

自定义提示弹框实现学习

Dart 复制代码
import 'package:flutter/material.dart';

import 'bubble_painter.dart';

///提示弹框
class BubbleTipWidget extends StatefulWidget {
  ///控件高度
  final double? height;

  ///控件宽度
  final double? width;

  ///控件圆角
  final double? radius;

  ///控件文本
  final String text;

  ///需要三角形指向的x坐标
  final double? x;

  ///需要三角形指向的y坐标
  final double? y;

  ///三角形的位置
  final ArrowLocation arrowLocation;

  final VoidCallback? voidCallback;

  const BubbleTipWidget(
      {super.key, this.width,
      this.height,
      this.radius,
      this.text = "",
      this.arrowLocation = ArrowLocation.BOTTOM,
      this.voidCallback,
      this.x = 0,
      this.y = 0});

  @override
  State<StatefulWidget> createState() => _BubbleTipWidgetState();
}

class _BubbleTipWidgetState extends State<BubbleTipWidget>
    with SingleTickerProviderStateMixin {
  AnimationController? progressController;

  final GlobalKey paintKey = GlobalKey();

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


  @override
  Widget build(BuildContext context) {
    double arrowHeight = 10;
    double arrowWidth = 10;

    double? x = widget.x;
    double? y = widget.y;
    Size size = MediaQuery.sizeOf(context);

    ///计算出位置的中心点
    if (widget.arrowLocation == ArrowLocation.BOTTOM ||
        widget.arrowLocation == ArrowLocation.TOP) {
      x = widget.x! - widget.width! / 2;
    } else {
      y = widget.y! - widget.height! / 2;
    }

    ///宽度是否超出
    bool widthOut = (widget.width! + x!) > size.width || x < 0;

    ///高度是否超出
    bool heightOut = (widget.height! + y!) > size.height || y < 0;

    ///不能小于0
    if (x < 0) {
      x = 0;
    } else if (widthOut) {
      x = size.width - widget.width!;
    }
    if (y < 0) {
      y = 0;
    } else if (heightOut) {
      y = size.height - widget.height!;
    }

    ///箭头在这个状态下是否需要居中
    bool arrowCenter = (widget.arrowLocation == ArrowLocation.BOTTOM ||
            widget.arrowLocation == ArrowLocation.TOP)
        ? !widthOut
        : !heightOut;

    ///调整箭头状态,因为此时箭头会可能不是局中的
    double arrowPosition = (widget.arrowLocation == ArrowLocation.BOTTOM ||
            widget.arrowLocation == ArrowLocation.TOP)
        ? (widget.x! - x - arrowWidth / 2)
        : (widget.y! - y - arrowHeight / 2);

    ///箭头的位置是按照弹出框的左边为起点计算的
    if (widget.arrowLocation == ArrowLocation.BOTTOM ||
        widget.arrowLocation == ArrowLocation.TOP) {
      if (arrowPosition < widget.radius! + 2) {
        arrowPosition = widget.radius! + 4;
      } else if (arrowPosition > widget.width! - widget.radius! - 2) {
        arrowPosition = widget.width! - widget.radius! - 4;
      }
    } else {
      if (arrowPosition < widget.radius! + 2) {
        arrowPosition = widget.radius! + 4;
      } else if (x > widget.height! - widget.radius! - 2) {
        arrowPosition = widget.height! - widget.radius! - 4;
      }
    }

    EdgeInsets margin = EdgeInsets.zero;
    if (widget.arrowLocation == ArrowLocation.TOP) {
      margin = EdgeInsets.only(top: arrowHeight, right: 5, left: 5);
    }

    var bubbleBuild = BubbleBuilder()
      ..mAngle = widget.radius
      ..mArrowHeight = arrowHeight
      ..mArrowWidth = arrowWidth
      ..mArrowPosition = arrowPosition
      ..mArrowLocation = widget.arrowLocation
      ..arrowCenter = arrowCenter;

    var alignment = Alignment.centerLeft;
    if(widget.arrowLocation == ArrowLocation.TOP || widget.arrowLocation ==ArrowLocation.BOTTOM) {
       alignment = Alignment.center;
    }


    return Scaffold(
      backgroundColor: Colors.transparent,
      body: GestureDetector(
        ///透明可以点击
        behavior: HitTestBehavior.translucent,
        onPanStart: _onPanStart,
        onPanUpdate: _onPanUpdate,
        onPanEnd: _onPanEnd,
        child: Container(
          alignment: Alignment.centerLeft,
          width: widget.width,
          height: widget.height,
          margin: EdgeInsets.only(left: x, top: y),
          child: Stack(
            children: <Widget>[
              ///绘制气泡背景
              CustomPaint(
                  key: paintKey,
                  size: Size(widget.width!, widget.height!),
                  painter: bubbleBuild.build()),

              Align(
                alignment: alignment,

                ///显示文本等
                child: Container(
                  margin: margin,
                  width: widget.width,
                  height: widget.height! - arrowHeight,
                  alignment: Alignment.centerLeft,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Container(
                        margin: const EdgeInsets.only(left: 20),
                        height: widget.height,
                        child: Icon(
                          Icons.notifications,
                          size: widget.height! - 30,
                          color: Theme.of(context).primaryColorDark,
                        ),
                      ),
                      Expanded(
                        child: Container(
                          margin: const EdgeInsets.only(left: 5, right: 5),
                          child: Text(
                            widget.text,
                            style: const TextStyle(fontSize: 14, color: Colors.black),
                          ),
                        ),
                      )
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }

  void _onPanStart(DragStartDetails details) {}

  void _onPanUpdate(DragUpdateDetails details) {}

  void _onPanEnd(DragEndDetails details) {
    widget.voidCallback?.call();
  }
}
相关推荐
且随疾风前行.16 天前
重学Android:自定义View基础(一)
android·自定义view
Forever不止如此2 个月前
【CustomPainter】绘制圆环
flutter·custompainter·圆环
sziitjin3 个月前
IOS 03 纯代码封装自定义View控件
ios·自定义view
qq_289093873 个月前
自定义 View 可以播放一段视频
android·音视频·自定义view
唐诺4 个月前
自定义波形图View,LayoutInflater动态加载控件保存为本地图片
自定义view·waveview·波形图
tracydragonlxy6 个月前
Android 实现竖排文本(垂直方向显示)
android·自定义view·textview·竖向文本·垂直文本
1024小神6 个月前
Node.js里面 Path 模块的介绍和使用
node.js·path
logan.gan7 个月前
AutoBackgroundBackButton 在ScrollView上方自动根据返回键按钮下方内容动态改变颜色。自动变色返回键
android·自定义view
learndiary8 个月前
Linux系统根分区空间满或PATH环境变量错导致无法登录图形界面
linux·环境变量·path·磁盘空间·不能登录