Flutter实现气泡提示框学习

news2024/11/25 0:48:53

前置知识点学习

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【StarRocks】starrocks 3.2.12 【share-nothing】 多Be集群容器化部署

文章目录 一. 集群规划二.docker compose以及启动脚本卷映射对于网络环境变量 三. 集群测试用户新建、赋权、库表初始化断电重启扩容 BE 集群 一. 集群规划 部署文档 https://docs.starrocks.io/zh/docs/2.5/deployment/plan_cluster/ 分类描述FE节点1. 主要负责元数据管理、…

LLaMA-Factory 上手即用教程

LLaMA-Factory 是一个高效的大型语言模型微调工具&#xff0c;支持多种模型和训练方法&#xff0c;包括预训练、监督微调、强化学习等&#xff0c;同时提供量化技术和实验监控&#xff0c;旨在提高训练速度和模型性能。 官方开源地址&#xff1a;https://github.com/hiyouga/L…

Java基础面试题01-请描述Java中JDK和JRE的区别?

什么是 JDK&#xff1f; JDK 全称 Java Development Kit&#xff0c;中文叫“Java 开发工具包”。 它是给 Java 开发者用的工具箱&#xff0c;里面有一切写代码、编译代码、调试代码所需要的工具。 JDK 包括什么&#xff1f; Java 编译器&#xff08;javac&#xff09;&…

Ubuntu20.04下安装向日葵

向日葵远程控制app官方下载 - 贝锐向日葵官网 下载Ununtu版的图形版本的安装deb包SunloginClient_15.2.0.63064_amd64.deb 直接执行 sudo dpkg -i SunloginClient_15.2.0.63064_amd64.deb 的话会报错: 如果在Ubuntu20.04里直接执行sudo apt install libgconf-2-4安装libgco…

Typora+PicGo+云服务器搭建博客图床

文章目录 前言一. 为什么要搭建博客图床&#xff1f;1.1 什么是图床&#xff1f;1.2 为什么要搭建博客图床? 二. 安装软件三. 配置阿里云OSS3.1 注册,开通对象储存3.2 创建bucket3.3 找到你的地域节点3.4 accessKeyId和accessKeySecret3.5 给你的阿里云账户充值 四. 配置4.1 配…

Python的3D可视化库 - vedo (2)visual子模块 基本可视化行为

文章目录 1. visual模块的继承关系2. 基类CommonVisual的方法2.1 获取对象信息2.1.1 对象本身信息2.1.2 对象的查找表2.1.3 对象标量范围2.1.4 对象缩略图 2.2 呈现对象2.2.1 在窗口显示1.2.2 对象可见性 2.2.3 对象颜色2.2.4 对象透明度 2.3 添加标度条2.3.1 2D标度条2.3.2 3D…

常用Rust日志处理工具教程

在本文中&#xff0c;我想讨论Rust中的日志。通过一些背景信息&#xff0c;我将带您了解两个日志库&#xff1a;env_logger和log4rs。最后&#xff0c;我将分享我的建议和github的片段。 Rust log介绍 log包是Rust中日志API的事实标准&#xff0c;共有五个日志级别&#xff1…

废品买卖回收管理系统|Java|SSM|Vue| 前后端分离

【重要①】前后端源码万字文档部署文档 【重要②】正版源码有问题包售后 【包含内容】 【一】项目提供非常完整的源码注释 【二】相关技术栈文档 【三】源码讲解视频 【其它服务】 【一】可以提供远程部署安装&#xff0c;包扩环境 【…

案例研究|阿特斯的JumpServer分布式部署和多组织管理实践

苏州阿特斯阳光电力科技有限公司&#xff08;以下简称为阿特斯&#xff09;是一家集太阳能光伏组件制造和为全球客户提供太阳能应用产品研发、设计、制造、销售的专业公司。 阿特斯集团总部位于加拿大&#xff0c;中国区总部位于江苏省苏州市。通过全球战略和多元化的市场布局…

tongweb安全整改

一 禁止以root账号运行tongweb服务 1 如果是首次安装须创建普通用户安装tongweb 2 如果已经使用root账号安装了tongweb 2.1 创建普通用户 2.2 使用root账号授予tongweb安装目录宿主权限为普通用户 2.3赋权成功后&#xff0c;后续启动tongweb服务必须为普通用户 二 tongRDS隐…

快速识别模型:simple_ocr,部署教程

快速识别图片中的英文、标点符号、数学符号、Emoji, 模型会输出图片中文字行的坐标位置、最低得分、识别结果。当前服务用到的模型&#xff1a;检测模型、数字识别、英文符号识别。 一、部署流程 1.更新基础环境 apt update2.安装miniconda wget https://repo.anaconda.com/…

tcpdump抓包 wireShark

TCPdump抓包工具介绍 TCPdump&#xff0c;全称dump the traffic on anetwork&#xff0c;是一个运行在linux平台可以根据使用者需求对网络上传输的数据包进行捕获的抓包工具。 tcpdump可以支持的功能: 1、在Linux平台将网络中传输的数据包全部捕获过来进行分析 2、支持网络层…

HarmonyOS4+NEXT星河版入门与项目实战(11)------Button组件

文章目录 1、控件图解2、案例实现1、代码实现2、代码解释3、运行效果4、总结1、控件图解 这里我们用一张完整的图来汇整 Button 的用法格式、属性和事件,如下所示: 按钮默认类型就是胶囊类型。 2、案例实现 这里我们实现一个根据放大和缩小按钮来改变图片大小的功能。 功…

YOLOV5 /onnx模型转换成rknn

上两篇文章讲述了pytorch模型下best.pt转换成onnx模型&#xff0c;以及将onnx进行简化成为best-sim.onnx, 接下来这篇文章讲述如何将onnx模型转换成rknn模型&#xff0c;转换成该模型是为了在rk3568上运行 1.创建share文件夹 文件夹包含以下文件best-sim.onnx,rknn-tookit2-…

【51单片机】LCD1602液晶显示屏

学习使用的开发板&#xff1a;STC89C52RC/LE52RC 编程软件&#xff1a;Keil5 烧录软件&#xff1a;stc-isp 开发板实图&#xff1a; 文章目录 LCD1602存储结构时序结构 编码 —— 显示字符、数字 LCD1602 LCD1602&#xff08;Liquid Crystal Display&#xff09;液晶显示屏是…

如何使用AWS Lambda构建一个云端工具(超详细)

首发地址&#xff08;欢迎大家访问&#xff09;&#xff1a;如何使用AWS Lambda构建一个云端工具&#xff08;超详细&#xff09; 1 前言 1.1 无服务器架构 无服务器架构&#xff08;Serverless Computing&#xff09;是一种云计算服务模型&#xff0c;它允许开发者构建和运行…

【Isaac Sim】相关问题汇总

目录 一、安装点击Install时报错二、启动时报 Failed to create any GPU devices三、加载Isaac Sim自带模型或示例时报 Isaac Sim is not responding 一、安装点击Install时报错 报错&#xff1a; request to https://asset.launcher.omniverse.nvidia.com/… failed, reason:…

Spring-02-springmvc

2. 什么是SpringMVC 2.1. 概述 Spring MVC是Spring Framework的一部分&#xff0c;是基于Java实现MVC的轻量级Web框架。 为什么要学习SpringMVC呢? Spring MVC的特点&#xff1a; 轻量级&#xff0c;简单易学高效 , 基于请求响应的MVC框架与Spring兼容性好&#xff0c;无缝…

深度学习之目标检测的技巧汇总

1 Data Augmentation 介绍一篇发表在Big Data上的数据增强相关的文献综述。 Introduction 数据增强与过拟合 验证是否过拟合的方法&#xff1a;画出loss曲线&#xff0c;如果训练集loss持续减小但是验证集loss增大&#xff0c;就说明是过拟合了。 数据增强目的 通过数据增强…

qt添加模块

以QtNetwork模块为例 方式一 扩展-qt vs tools-qt project settings 方式二 右键选中项目-属性-qt project settings 方法三 在此界面选择select modules,即可进行相应模块添加