Flutter提示框插件flutter_callouts的使用

Flutter提示框插件flutter_callouts的使用

目录

动机

我们希望创建一个单一的包,能够在多种场景下与用户进行交流。

  • 弹出指向目标部件的提示框
  • 弹出没有目标的提示框,并可以在屏幕上定位
  • 弹出不同位置的Toast(使用对齐属性)

特性

Callout API 使你能够通过显示 callout 小部件来指出目标部件。

callout 的出现方式和与用户的交互方式高度可配置。

简单的API

  • 目标部件必须有一个 GlobalKey
  • callout 必须提供一个字符串 cId。
  • callout 需要你提供内容部件。
  • 与你的UI解耦
    • 一个 callout 或 toast 是在 Flutter Overlay 中显示的,因此不会干扰你的UI。

极度可配置

callout 的每一个展示方面都可以配置,包括样式、指针样式、拖动性、可调整大小性、动画、屏幕上的持续时间等…

callout 和指针样式

  • 可以配置 callout 的颜色、形状、装饰、边框。
  • 可以配置 callout 如何指向目标,例如带有箭头的线或气泡形状,以及它应该与目标分离的距离。

动画

  • 出现方式可以动画化,指向目标的指针也可以动画化。

用户可拖动

  • callout 可以选择是否可以拖动。你可以指定 callout 的一部分作为拖动句柄。

用户可调整大小

  • callout 可以选择被四个角和四条边的调整大小部件包围,即用户可以通过拖动角或边来调整 callout 的大小。

可点击的屏障

  • callout 可以有一个可选的可点击屏障(当不透明度大于0时)。(点击 callout 外部关闭它,否则可以配置关闭按钮)

关闭按钮

  • 关闭按钮是可选的。其回调和外观都是可配置的。
  • callout 还可以通过 API 隐藏/显示。

“知道了”按钮

  • callout 可以配置为显示一个“知道了”按钮。
  • 点击会记录在浏览器或应用程序的本地存储中(使用 callout 的 id)。

滚动感知

  • 如果将您的 ScrollController 传递给 API,则即使发生滚动或窗口重新调整大小,callout 仍可以继续指向其目标。

解耦、非侵入式API

  • 任何部件都可以是一个目标:只需给它一个 GlobalKey。
  • 不需要在您的 UI 中插入包装部件。
  • 每个 callout 在自己的 Overlay 中显示。

有用的回调

  • 提供了每个可能的回调,以便您的应用可以对 callout 的活动作出反应:
ValueNotifier<int>? movedOrResizedNotifier; // 每次 callout overlay 移动或调整大小时都会增加
Function? onGotitPressedF;
VoidCallback? CalloutBarrier.onTapped;
VoidCallback? onCloseButtonPressF;
ValueChanged<Offset>? onDragF;
VoidCallback? onDragStartedF;
ValueChanged<Offset>? onDragEndedF;
ValueChanged<Size>? onResizeF;
VoidCallback? onDismissedF;
VoidCallback? onHiddenF;
VoidCallback? onAcceptedF;

快速开始

  1. 安装或更新 flutter_callouts:
flutter pub add flutter_callouts

使用

在简单的示例演示中,一个 callout 和 toast 在 initState() 方法中创建。

import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';

void main() {
  runApp(const MyApp());
}

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'flutter_callouts demo',
      home: MyHomePage(),
    );
  }
}

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

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

/// 因为 callouts 是动画化的,所以添加混入
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  int _counter = 0;

  late GlobalKey fabGK;

  ScrollController controller = ScrollController();

  /// CalloutConfig 对象是配置 callout 及其指针的地方
  /// 所有参数都显示出来了,许多参数在这个示例 callout 中被注释掉了
  CalloutConfig basicCalloutConfig(ScrollController controller) =>
      CalloutConfig(
        cId: 'basic',
        // -- 初始位置和动画 ---------------------------------
        initialTargetAlignment: Alignment.topLeft,
        initialCalloutAlignment: Alignment.bottomRight,
        // initialCalloutPos:
        finalSeparation: 100,
        // fromDelta: 0.0,
        // toDelta : 0.0,
        // initialAnimatedPositionDurationMs:
        // -- 可选屏障(当不透明度 > 0 时) ----------------------
        // barrier: CalloutBarrier(
        //   opacity: .5,
        //   onTappedF: () {
        //     Callout.dismiss("basic");
        //   },
        // ),
        // -- callout 外观 ----------------------------------------
        // suppliedCalloutW: 280, // 如果未提供,则测量 callout 内容部件
        // suppliedCalloutH: 200, // 如果未提供,则测量 callout 内容部件
        // borderRadius: 12,
        borderThickness: 3,
        fillColor: Colors.yellow[700],
        // elevation: 10,
        // frameTarget: true,
        // -- 可选关闭按钮和“知道了”按钮 -------------------
        // showGotitButton: true,
        // showCloseButton: true,
        // closeButtonColor:
        // closeButtonPos:
        // gotitAxis:
        // -- 指针 -------------------------------------------------
        // arrowColor: Colors.green,
        // arrowType: ArrowType.THIN,
        animate: true,
        // lineLabel: Text('line label'),
        // fromDelta: -20,
        // toDelta: -20,
        // lengthDeltaPc: ,
        // contentTranslateX: ,
        // contentTranslateY:
        // targetTranslateX:
        // targetTranslateY:
        // scaleTarget:
        // -- 调整大小 -------------------------------------------------
        // resizeableH: true,
        // resizeableV: true,
        // -- 拖动 -------------------------------------------------
        // draggable: false,
        // draggableColor: Colors.green,
        // dragHandleHeight: ,
        vScrollController: controller,
        vsync: this,
      );

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  [@override](/user/override)
  void initState() {
    super.initState();

    /// 目标的 key
    fabGK = GlobalKey();

    fca.afterNextBuildDo(() {
      Callout.showOverlay(
        calloutConfig: basicCalloutConfig(controller),
        calloutContentF: (context) => const Padding(
          padding: EdgeInsets.all(8.0),
          child: Text('Tap this floating action button to increment the counter.'),
        ),
        targetGkF: () => fabGK,
      );
      fca.afterMsDelayDo(
        800,
        () => _showToast(Alignment.topCenter),
      );
    });
  }

  void _showToast(Alignment gravity,
          {int showForMs = 0, VoidCallback? onDismissedF}) =>
      Callout.showToast(
        removeAfterMs: showForMs,
        calloutConfig: CalloutConfig(
          cId: 'initstate-toast',
          gravity: gravity,
          initialCalloutW: 500,
          initialCalloutH: 90,
          fillColor: Colors.black26,
          showCloseButton: true,
          borderThickness: 5,
          borderRadius: 16,
          borderColor: Colors.yellow,
          elevation: 10,
          vScrollController: controller,
          vsync: this,
          onDismissedF: () => onDismissedF?.call(),
        ),
        calloutContentF: (_) => Center(
          child: Text(
            'gravity: ${gravity.toString()}',
            textScaler: const TextScaler.linear(2),
            style: const TextStyle(color: Colors.white),
          ),
        ),
      );

  [@override](/user/override)
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    return NotificationListener<SizeChangedLayoutNotification>(
      onNotification: (SizeChangedLayoutNotification notification) {
        // Callout.dismissAll(exceptFeatures: []);
        FlutterCallouts.instance.afterMsDelayDo(300, () {
          Callout.refreshAll();
        });
        return true;
      },
      child: SizeChangedLayoutNotifier(
        child: Scaffold(
          body: Center(
            child: SingleChildScrollView(
              controller: controller,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  SizedBox(
                    height: screenSize.height - 200,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text(
                          'You have pushed the + button this many times:',
                        ),
                        Text(
                          '$_counter',
                          style: Theme.of(context).textTheme.headlineMedium,
                        ),
                      ],
                    ),
                  ),
                  Container(
                    width: double.infinity,
                    height: 100,
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: Padding(
                        padding: const EdgeInsets.all(18.0),
                        child: FloatingActionButton(
                          key: fabGK,
                          onPressed: _incrementCounter,
                          tooltip: 'Increment',
                          child: const Icon(Icons.add),
                        ),
                      ),
                    ),
                  ),
                  Container(
                    height: 1000,
                    width: double.infinity,
                    color: Colors.blue[50],
                    child: const Padding(
                      padding: EdgeInsets.all(8.0),
                      child: Text('Scroll to see that the yellow callout is Scroll-aware.\n'
                      'Resize the window to see the pointer refreshing.'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

示例代码

import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await fca.initLocalStorage();
  runApp(const FlutterCalloutsSimpleDemo());
}

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'flutter_callouts demo',
      home: MyHomePage(),
    );
  }
}

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

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

/// 因为 callouts 是动画化的,所以添加混入
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late int _counter;

  late GlobalKey fabGK;

  NamedScrollController namedSC = NamedScrollController('main', Axis.vertical);

  /// CalloutConfig 对象是配置 callout 及其指针的地方
  /// 所有参数都显示出来了,许多参数在这个示例 callout 中被注释掉了
  CalloutConfig
  basicCalloutConfig(ScrollController controller) =>
      CalloutConfig(
        cId: 'basic',
        // -- 初始位置和动画 ---------------------------------
        initialTargetAlignment: Alignment.topLeft,
        initialCalloutAlignment: Alignment.bottomRight,
        // initialCalloutPos:
        finalSeparation: 100,
        // fromDelta: 0.0,
        // toDelta : 0.0,
        // initialAnimatedPositionDurationMs:
        // -- 可选屏障(当不透明度 > 0 时) ----------------------
        // barrier: CalloutBarrier(
        //   opacity: .5,
        //   onTappedF: () {
        //     Callout.dismiss("basic");
        //   },
        // ),
        // -- callout 外观 ----------------------------------------
        // suppliedCalloutW: 280, // 如果未提供,则测量 callout 内容部件
        // suppliedCalloutH: 200, // 如果未提供,则测量 callout 内容部件
        // borderRadius: 12,
        borderThickness: 3,
        fillColor: Colors.yellow[700],
        // elevation: 10,
        // frameTarget: true,
        // -- 可选关闭按钮和“知道了”按钮 -------------------
        // showGotitButton: true,
        // showCloseButton: true,
        // closeButtonColor:
        // closeButtonPos:
        // gotitAxis:
        // -- 指针 -------------------------------------------------
        // arrowColor: Colors.green,
        arrowType: ArrowType.POINTY,
        animate: true,
        // lineLabel: Text('line label'),
        // fromDelta: -20,
        // toDelta: -20,
        // lengthDeltaPc: ,
        // contentTranslateX: ,
        // contentTranslateY:
        // targetTranslateX:
        // targetTranslateY:
        // scaleTarget:
        // -- 调整大小 -------------------------------------------------
        // resizeableH: true,
        // resizeableV: true,
        // -- 拖动 -------------------------------------------------
        // draggable: false,
        // draggableColor: Colors.green,
        // dragHandleHeight: ,
        scrollControllerName: namedSC.name,
        followScroll: false,
      );

  void _incrementCounter() {
    setState(() {
      _counter++;
      fca.spwc?.setInt("counter", _counter);
    });
  }

  [@override](/user/override)
  void initState() {
    super.initState();

    _counter = fca.spwc?.getInt("counter") ?? 0;

    /// 目标的 key
    fabGK = GlobalKey();

    // controller.listenToOffset();

    fca.afterNextBuildDo(() {
      fca.showOverlay(
        calloutConfig: basicCalloutConfig(namedSC),
        calloutContent: const Padding(
          padding: EdgeInsets.all(8.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('Tap this floating action button to increment the counter.'),
            ],
          ),
        ),
        targetGkF: () => fabGK,
      );
      fca.afterMsDelayDo(
        800,
        () => _showToast(Alignment.topCenter),
      );
    });
  }

  [@override](/user/override)
  void dispose() {
    namedSC.dispose();
    super.dispose();
  }

  [@override](/user/override)
  void didChangeDependencies() {
    fca.initWithContext(context);
    super.didChangeDependencies();
  }

  void _showToast(Alignment gravity,
          {int showForMs = 0, VoidCallback? onDismissedF}) =>
      fca.showToast(
        removeAfterMs: showForMs,
        calloutConfig: CalloutConfig(
          cId: 'main-toast',
          gravity: gravity,
          initialCalloutW: 500,
          initialCalloutH: 90,
          fillColor: Colors.black26,
          showCloseButton: true,
          borderThickness: 5,
          borderRadius: 16,
          borderColor: Colors.yellow,
          elevation: 10,
          scrollControllerName: namedSC.name,
          onDismissedF: () => onDismissedF?.call(),
          // allowCalloutToScroll: false,
        ),
        calloutContent: Center(
          child: Text(
            'gravity: ${gravity.toString()}',
            textScaler: const TextScaler.linear(2),
            style: const TextStyle(color: Colors.white),
          ),
        ),
      );

  [@override](/user/override)
  Widget build(BuildContext context) =>
      NotificationListener<SizeChangedLayoutNotification>(
        onNotification: (SizeChangedLayoutNotification notification) {
          // Callout.dismissAll(exceptFeatures: []);
          fca.afterMsDelayDo(300, () {
            fca.refreshAll();
          });
          return true;
        },
        child: SizeChangedLayoutNotifier(
          child: Scaffold(
            body: Center(
              child: SingleChildScrollView(
                controller: namedSC,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    SizedBox(
                      height: MediaQuery.of(context).size.height - 200,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          const Text(
                            'You have pushed the + button this many times:',
                          ),
                          Text(
                            '$_counter',
                            style: Theme.of(context).textTheme.headlineMedium,
                          ),
                        ],
                      ),
                    ),
                    SizedBox(
                      width: double.infinity,
                      height: 100,
                      child: Align(
                        alignment: Alignment.centerRight,
                        child: Padding(
                          padding: const EdgeInsets.all(18.0),
                          child: FloatingActionButton(
                            key: fabGK,
                            onPressed: _incrementCounter,
                            tooltip: 'Increment',
                            child: const Icon(Icons.add),
                          ),
                        ),
                      ),
                    ),
                    Container(
                      height: 1000,
                      width: double.infinity,
                      color: Colors.blue[50],
                      child: const Padding(
                        padding: EdgeInsets.all(8.0),
                        child: Text(
                            'Scroll to see that the yellow callout is Scroll-aware.\n'
                            'Resize the window to see the pointer refreshing.'),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      );
}

更多关于Flutter提示框插件flutter_callouts的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter提示框插件flutter_callouts的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用flutter_callouts插件来显示提示框的示例代码。flutter_callouts插件允许你在应用中的特定位置显示丰富的提示框,非常适合用于引导用户或显示信息。

首先,你需要在pubspec.yaml文件中添加flutter_callouts依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_callouts: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来是一个简单的示例,展示如何使用flutter_callouts插件:

import 'package:flutter/material.dart';
import 'package:flutter_callouts/flutter_callouts.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Callouts Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CalloutController? _calloutController;

  @override
  void initState() {
    super.initState();
    // 初始化CalloutController
    _calloutController = CalloutController();

    // 显示提示框(在3秒后)
    Future.delayed(Duration(seconds: 3), () {
      _showCallout();
    });
  }

  @override
  void dispose() {
    // 释放资源
    _calloutController?.dispose();
    super.dispose();
  }

  void _showCallout() {
    _calloutController?.show(
      context,
      target: _getTargetWidgetKey(context),
      content: Container(
        padding: EdgeInsets.all(16.0),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(8.0),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.2),
              spreadRadius: 2,
              blurRadius: 5,
              offset: Offset(0, 2), // changes position of shadow
            ),
          ],
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '这是一个提示框',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 8.0),
            Text('你可以在这里显示更多信息。'),
          ],
        ),
      ),
      arrow: CalloutArrow(
        color: Colors.white,
        borderColor: Colors.grey.shade300,
        borderWidth: 2.0,
      ),
      arrowPosition: CalloutArrowPosition.top,
      margin: EdgeInsets.only(top: 8.0),
      constraints: BoxConstraints(maxWidth: 200),
      backgroundColor: Colors.transparent,
      onDismiss: () {
        print('提示框已关闭');
      },
    );
  }

  GlobalKey _getTargetWidgetKey(BuildContext context) {
    // 这里返回一个目标小部件的GlobalKey,提示框将指向这个小部件
    final targetKey = GlobalKey();
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      // 在下一帧显示提示框(确保目标小部件已渲染)
      _showCallout(); // 注意:这里仅为演示目的,实际应在initState中调用一次_showCallout
    });
    return targetKey; // 注意:在实际使用中,你应该将这个key分配给你希望指向的小部件
  }

  @override
  Widget build(BuildContext context) {
    // 注意:为了简化示例,这里并没有实际使用_getTargetWidgetKey返回的key。
    // 在实际使用中,你应该将_getTargetWidgetKey(context)分配给某个小部件的key属性。
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Callouts Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {},
              child: Text('点击我(目标小部件应在这里设置key)'),
            ),
          ],
        ),
      ),
    );
  }
}

注意

  1. 上述代码中的_getTargetWidgetKey方法仅用于演示目的,并没有实际将返回的GlobalKey分配给任何小部件。在实际使用中,你需要将GlobalKey分配给你希望提示框指向的具体小部件。
  2. Future.delayed(Duration(seconds: 3), () { _showCallout(); });用于在3秒后显示提示框,你可以根据需要调整或移除这个延迟。
  3. _showCallout方法中的target参数接收一个GlobalKey,该GlobalKey应绑定到你希望提示框指向的小部件上。

这个示例展示了如何使用flutter_callouts插件来显示一个简单的提示框,你可以根据需要进一步自定义提示框的内容和样式。

回到顶部