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` 案例

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` 构造函数中初始化。

示例修正

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

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

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

可以通过以下方式修正:

class MyWidget extends StatelessWidget {
  final GlobalKey _key = GlobalKey(); // 不使用 const

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

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

class MyWidget extends StatelessWidget {
  final GlobalKey _key;

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

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

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` 类中使用它。以下是一个基本的使用示例:

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`,然后从中提取屏幕尺寸信息。

使用示例
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)`,你可以轻松地访问设备的屏幕尺寸信息,从而为用户提供更好的响应式界面布局。

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`: 水平拖动相关的手势。

使用示例
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`:如果拖动是单向的(水平或垂直),这将返回沿主要轴的偏移量。
使用示例
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` 的子组件内的坐标)。
使用案例
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`。

使用案例
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 以提高性能
  }
}

 

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` 是一个非常便利的工具。
使用示例
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` 属性等。

使用案例

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` 来代表没有边距的选项。
使用案例
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` 值。
EdgeInsets padding = condition ? EdgeInsets.all(10.0) : EdgeInsets.zero;
  • 默认值清除:某些 Flutter 组件可能有默认的边距设置,通过使用 `EdgeInsets.zero` 可以显式地清除这些默认设置。
注意事项
  • 布局影响:使用 `EdgeInsets.zero` 会使得组件内容紧贴其父容器的边界,确保这是预期的效果。
  • 代码可读性:即使某个属性的默认值为 0,使用 `EdgeInsets.zero` 可以提高代码的可读性和可维护性,明确表明设计选择。

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

自定义提示弹框实现学习

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();
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921898.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity-添加世界坐标系辅助线

如果你想在场景中更直观地显示世界坐标系&#xff0c;可以通过编写一个简单的脚本来实现。下面是一个基本的示例脚本&#xff0c;它会在场景中绘制出世界坐标系的三个轴&#xff1a; using UnityEngine;public class WorldAxesIndicator : MonoBehaviour {public float length…

决策树分类算法【sklearn/决策树分裂指标/鸢尾花分类实战】

决策树分类算法 1. 什么是决策树&#xff1f;2. DecisionTreeClassifier的使用&#xff08;sklearn&#xff09;2.1 算例介绍2.2 构建决策树并实现可视化 3. 决策树分裂指标3.1 信息熵&#xff08;ID3&#xff09;3.2 信息增益3.3 基尼指数&#xff08;CART&#xff09; 4. 代码…

5分钟轻松搭建Immich图片管理软件并实现公网远程传输照片

文章目录 前言1.关于Immich2.安装Docker3.本地部署Immich4.Immich体验5.安装cpolar内网穿透6.创建远程链接公网地址7.使用固定公网地址远程访问 前言 本篇文章介绍如何在本地搭建lmmich图片管理软件&#xff0c;并结合cpolar内网穿透实现公网远程访问到局域网内的lmmich&#…

React和Next.js的相关内容

React–前端框架 React 是一个用于构建用户界面的 JAVASCRIPT 库。 React 主要用于构建 UI&#xff0c;很多人认为 React 是 MVC 中的 V&#xff08;视图&#xff09;。 React 起源于 Facebook 的内部项目&#xff0c;用来架设 Instagram 的网站&#xff0c;并于 2013 年 5 …

【LeetCode热题100】队列+宽搜

这篇博客是关于队列宽搜的几道题&#xff0c;主要包括N叉树的层序遍历、二叉树的锯齿形层序遍历、二叉树最大宽度、在每个数行中找最大值。 class Solution { public:vector<vector<int>> levelOrder(Node* root) {vector<vector<int>> ret;if(!root) …

丹摩征文活动|基于丹摩算力的可图(Kolors)的部署与使用

Kolors是一个以生成图像为目标的人工智能系统&#xff0c;可能采用了类似于OpenAI的DALLE、MidJourney等文本生成图像的技术。通过自然语言处理&#xff08;NLP&#xff09;和计算机视觉&#xff08;CV&#xff09;相结合&#xff0c;Kolors能够根据用户提供的文本描述生成符合…

【PTA】【数据库】【SQL命令】编程题1

数据库SQL命令测试题1 10-1 显示教工编号以02开头的教师信息 作者 冰冰 单位 广东东软学院 显示教工编号以02开头的教师信息 提示&#xff1a;请使用SELECT语句作答。 表结构: CREATE TABLE teacher ( TId CHAR(5) NOT NULL, -- 教师工号&#xff0c;主键 DId CHAR(2) …

Dockerhub镜像加速

一、背景 dockerhub由于被封锁和站点处于国外的原因&#xff0c;docker pull拉取镜像非常慢&#xff0c;有时候直接都无法拉取。严重妨碍了我们的学习进度以及日常使用。 总结了一些proxy代理的镜像站点&#xff0c;配置之后速度会有明显提升&#xff0c;大家可以参考使用。 二…

Linux: C语言解析域名

在上一篇博客 Linux: C语言发起 DNS 查询报文 中&#xff0c;自己构造 DNS 查询报文&#xff0c;发出去&#xff0c;接收响应&#xff0c;以二进制形式把响应的数据写入文件并进行分析。文章的最后留下一个悬念&#xff0c;就是写代码解析 DNS answer section 部分。本文来完成…

Tri Mode Ethernet MAC IP核详解

本文对 Vivado 的三速 MAC IP 核&#xff08;Tri Mode Ethernet MAC&#xff0c;TEMAC&#xff09;进行介绍。 在自行实现三速以太网 MAC 控制器时&#xff0c;GMII/RGMII 接口可以通过 IDDR、ODDR 原语实现&#xff0c;然而实际使用中自己实现的模块性能不是很稳定&#xff08…

CENTOS7 升级gcc版本

升级gcc版本 CentOS下升级gcc版本有两个途径&#xff0c;一个是添加其他源进行自动升级&#xff0c;一个是手动编译升级&#xff0c;这里先顺便讲下自动升级的两个办法&#xff1a; a. 添加Fedora源 在 /etc/yum.repos.d 目录中添加文件 FedoraRepo.repo &#xff0c;并输入…

VMware虚拟机(Ubuntu或centOS)共享宿主机网络资源

VMware虚拟机(Ubuntu或centOS)共享宿主机网络资源 由于需要在 Linux 环境下进行一些测试工作&#xff0c;于是决定使用 VMware 虚拟化软件来安装 Ubuntu 24.04 .1操作系统。考虑到测试过程中需要访问 Github &#xff0c;要使用Docker拉去镜像等外部网络资源&#xff0c;因此产…

学习日记_20241123_聚类方法(高斯混合模型)续

前言 提醒&#xff1a; 文章内容为方便作者自己后日复习与查阅而进行的书写与发布&#xff0c;其中引用内容都会使用链接表明出处&#xff08;如有侵权问题&#xff0c;请及时联系&#xff09;。 其中内容多为一次书写&#xff0c;缺少检查与订正&#xff0c;如有问题或其他拓展…

15.C++STL 2(string类的使用,6000字详解)

⭐本篇重点&#xff1a;string类的使用 ⭐本篇代码&#xff1a;c学习/05.string类的学习 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. C/C字符与string类 二. STL中的string类的使用 2.1 string类常见的构造与赋值 2.2 string对象的数据容量操作 …

神经网络(系统性学习一):入门篇——简介、发展历程、应用领域、基本概念、超参数调优、网络类型分类

相关文章&#xff1a; 神经网络中常用的激活函数 神经网络简介 神经网络&#xff08;Neural Networks&#xff09;是受生物神经系统启发而设计的数学模型&#xff0c;用于模拟人类大脑处理信息的方式。它由大量的节点&#xff08;或称为“神经元”&#xff09;组成&#xff0…

shell 基础知识2 ---条件测试

目录 一、条件测试的基本语法 二、文件测试表达式 三、字符串测试表达式 四、整数测试表达式 五、逻辑操作符 六、实验 为了能够正确处理 Shell 程序运行过程中遇到的各种情况&#xff0c; Linux Shell 提供了一组测试运算符。 通过这些运算符&#xff0c;Shell 程序能够…

数据指标与标签在数据分析中的关系与应用

导读&#xff1a;分享数据指标体系的文章很多&#xff0c;但讲数据标签的文章很少。实际上&#xff0c;标签和指标一样&#xff0c;是数据分析的左膀右臂&#xff0c;两者同样重要。实际上&#xff0c;很多人分析不深入&#xff0c;就是因为缺少对标签的应用。今天系统的讲解下…

Flutter-Web首次加载时添加动画

前言 现在web上线后首次加载会很慢&#xff0c;要5秒以上&#xff0c;并且在加载的过程中界面是白屏。因此想在白屏的时候放一个加载动画 实现步骤 1.找到web/index.html文件 2.添加以下<style>标签内容到<head>标签中 <style>.loading {display: flex;…

51单片机基础 06 串口通信与串口中断

目录 一、串口通信 二、串口协议 三、原理图 四、串口通信配置参数 1、常用的串行口工作方式1 2、数据发送 3、数据接收 4、波特率计算 5、轮询接收 6、中断接收 一、串口通信 串口通信是一种常见的数据传输方式&#xff0c;广泛用于计算机与外部设备或嵌入式系统之间…

【深度学习之回归预测篇】 深度极限学习机DELM多特征回归拟合预测(Matlab源代码)

深度极限学习机 (DELM) 作为一种新型的深度学习算法&#xff0c;凭借其独特的结构和训练方式&#xff0c;在诸多领域展现出优异的性能。本文将重点探讨DELM在多输入单输出 (MISO) 场景下的应用&#xff0c;深入分析其算法原理、性能特点以及未来发展前景。 1、 DELM算法原理及其…