第三十八回:Tooltip Widget

本文介绍了Flutter中的TooltipWidget,它是一种在组件上悬停或长按时显示附加信息的弹出窗口。文章详细讲解了Tooltip的属性,如height、waitDuration、showDuration、message、textStyle、decoration和child,并提供了示例代码展示如何创建和定制Tooltip。作者还分享了关于长按时间和文字颜色选择的经验建议。


我们在上一章回中介绍了BottomSheet Widget相关的内容,本章回中将介绍 Tootip Widget.闲话休提,让我们一起Talk Flutter吧。

概念介绍

我们在这里说的Tooltip也是一种弹出窗口,当指针悬停于某个组件上或者长按某个组件时,就会在组件下方弹出一个悬浮窗口,这个悬浮窗口就是Tooltip.

它经常用来提供某种组件的附加信息,或者提供某项操作的解释说明。本章回中将介绍Tooltip组件的使用方法。

使用方法

和其它Widget类似,我们可以通过Tooltip的属性来操作它,下面是一些常用的属性:

  • height属性:用来控制弹出窗口的高度,窗口的宽度由窗口内容决定
  • waitDuration属性:用来控制鼠标悬停等待时间,时间到达后显示tooltip
  • showDuration属性:用来控制长按等待时间,时间到达后显示tooltip
  • message属性:用来控制窗口中显示的内容;
  • textStyle属性:用来控制窗口中文字的风格,比如文字颜色和字体大小;
  • decoration属性:用来控制窗口的边框风格,比如圆角,边框颜色等;
  • child属性:用来表示窗口附属的组件,当悬停或者长按该组件时弹出窗口;

示例代码

_showToolTip() {
  return Tooltip(
    //提示框的高度,宽度自适应文字长度.
    height: 100,
    //鼠标悬停等待时间,时间到达后显示tooltip
    // waitDuration: Duration(seconds: 2),
    //长按等待时间,时间到达后显示tooltip
    showDuration: const Duration(seconds: 3),
    message: "This is the message of ToolTip",
    //建议设置文字颜色,默认为白色,不容易看到
    textStyle: const TextStyle(
      color: Colors.black,
      fontSize: 20,
    ),
    decoration: BoxDecoration(
      border: Border.all(
        color: Colors.green,
        width: 2,
      ),
      borderRadius: BorderRadius.circular(30),
    ),
    child: Container(
      alignment: Alignment.center,
      width: 400,
      height: 300,
      child: Text('Show Tooltip'),
    ),
  );
}

我们在这里只列出了核心代码,完整的代码可以查看Github上ex020文件中的内容。编译并且运行上面的程序就可以在屏幕上看到一个文本组件,长按该文本组件时就会弹出一个绿色边框的悬浮窗口,窗口中显示黑色的文字,松开长按的手指后窗口会自动消失。我在这里就不演示程序运行结果了,建议大家自己动手去实践。

经验总结

最后,分享一些自己总结的经验:

  • 建议把长按时间设置为三秒左右,这样的交互效果会好一些,当然了三秒只是一个经验值;
  • 建议修改窗口中文字的颜色,默认文字颜色为白色,如果界面背景也是白色,就不容易看到文字;
  • 建议给窗口添加装饰,因为这样便于观察弹出的窗口,而且看上去更加富有立体感;

看官们,关于TooltipWidget就介绍到这里,欢迎大家在评论区交流与讨论!

import 'package:flutter/material.dart'; import 'package:newdemo/Home_Page.dart'; import 'package:newdemo/Practice_Page.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'package:provider/provider.dart'; // 登录状态管理类 class AuthProvider with ChangeNotifier { bool _isLoggedIn = false; String _username = ''; bool get isLoggedIn => _isLoggedIn; String get username => _username; Future<void> login(String username) async { _isLoggedIn = true; _username = username; notifyListeners(); // 保存登录状态到本地存储 final prefs = await SharedPreferences.getInstance(); await prefs.setBool('isLoggedIn', true); await prefs.setString('username', username); } Future<void> loadLoginStatus() async { final prefs = await SharedPreferences.getInstance(); _isLoggedIn = prefs.getBool('isLoggedIn') ?? false; _username = prefs.getString('username') ?? ''; notifyListeners(); } Future<void> logout() async { _isLoggedIn = false; _username = ''; notifyListeners(); // 清除本地存储的登录状态 final prefs = await SharedPreferences.getInstance(); await prefs.remove('isLoggedIn'); await prefs.remove('username'); } } class ThirdPage extends StatefulWidget { const ThirdPage({Key? key}) : super(key: key); @override State<ThirdPage> createState() => _ThirdPageState(); } class _ThirdPageState extends State<ThirdPage> { @override Widget build(BuildContext context) { return MaterialApp( home: ProjectListPage(), theme: ThemeData( primarySwatch: Colors.grey, visualDensity: VisualDensity.adaptivePlatformDensity, ), ); } } class TableRowData { final String fixedContent; String editableContent; final String presetContent; TableRowData({ required this.fixedContent, this.editableContent = "", required this.presetContent, }); Map<String, dynamic> toJson() => { 'fixedContent': fixedContent, 'editableContent': editableContent, 'presetContent': presetContent, }; factory TableRowData.fromJson(Map<String, dynamic> json) => TableRowData( fixedContent: json['fixedContent'], editableContent: json['editableContent'], presetContent: json['presetContent'], ); } class Project { String name; List<TableRowData> rows; Project({required this.name, required this.rows}); Map<String, dynamic> toJson() => { 'name': name, 'rows': rows.map((row) => row.toJson()).toList(), }; factory Project.fromJson(Map<String, dynamic> json) => Project( name: json['name'], rows: (json['rows'] as List) .map((row) => TableRowData.fromJson(row)) .toList(), ); } class ProjectListPage extends StatefulWidget { @override _ProjectListPageState createState() => _ProjectListPageState(); } class _ProjectListPageState extends State<ProjectListPage> { List<Project> projects = []; @override void initState() { super.initState(); _loadProjects(); } // 加载保存的项目 Future<void> _loadProjects() async { final prefs = await SharedPreferences.getInstance(); final String? projectsJson = prefs.getString('projects'); if (projectsJson != null) { final List<dynamic> jsonList = json.decode(projectsJson); setState(() { projects = jsonList.map((json) => Project.fromJson(json)).toList(); }); } } // 保存项目列表 Future<void> _saveProjects() async { final prefs = await SharedPreferences.getInstance(); final List<Map<String, dynamic>> projectsJson = projects.map((project) => project.toJson()).toList(); await prefs.setString('projects', json.encode(projectsJson)); } // 获取第一列固定内容 String _getFixedContent(int rowIndex) { final contents = [ "固定内容行 1", "固定内容行 2", "固定内容行 3", "固定内容行 4", "固定内容行 5", "固定内容行 6", "固定内容行 7", "固定内容行 8", "固定内容行 9" ]; return contents[rowIndex]; } // 获取预设弹窗内容 String _getPresetContent(int rowIndex) { final contents = [ "安全警告:参数超出安全范围\n建议值:25-50", "操作确认:此操作不可撤", "系统通知:数据已成功保存\n位", "提醒:请完成所有必填字段\n缺失字段I", "错误:输入格式不正确\n请输入数", "性能提示:优化算法复杂度", "警告:资源使用超过阈值\n当", "确认:是否提交当前修改?\n", "成功:操作执行完成\n" ]; return contents[rowIndex]; } // 添加新项目 void _addNewProject(BuildContext context) { final authProvider = Provider.of<AuthProvider>(context, listen: false); // 检查登录状态 if (!authProvider.isLoggedIn) { showLoginRequiredDialog(context); return; } List<TableRowData> rows = List.generate( 9, (index) => TableRowData( fixedContent: _getFixedContent(index), presetContent: _getPresetContent(index), )); setState(() { projects.add(Project( name: '${authProvider.username}的项目 ${projects.length + 1}', rows: rows, )); _saveProjects(); // 显示创建成功提示 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('项目创建成功!'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); }); } // 添加新项目 /* void _addNewProject() { List<TableRowData> rows = List.generate( 9, (index) => TableRowData( fixedContent: _getFixedContent(index), presetContent: _getPresetContent(index), )); setState(() { projects.add(Project( name: '', rows: rows, )); _saveProjects(); }); }*/ // 删除项目 void _deleteProject(int index) { setState(() { projects.removeAt(index); _saveProjects(); }); } // 导航到表格详情页 void _navigateToTableDetail(BuildContext context, Project project) { Navigator.push( context, MaterialPageRoute( builder: (context) => TableDetailPage(project: project), ), ); } // 登录提醒弹窗 void showLoginRequiredDialog(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('登录提示'), content: Text('您需要登录才能创建新项目'), actions: <Widget>[ TextButton( child: Text('取消'), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text('去登录', style: TextStyle(color: Colors.blue)), onPressed: () { Navigator.of(context).pop(); // 关闭弹窗 Navigator.push( context, MaterialPageRoute(builder: (context) => HomePage()), ); }, ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => HomePage()), // 目标页面 ); }, ), title: Text("CBT", textDirection: TextDirection.ltr, textAlign: TextAlign.center, style: TextStyle( color: Colors.black, fontFamily: 'Comic Sans MS', fontSize: 20.0, fontWeight: FontWeight.w500, )), centerTitle: true, backgroundColor: Colors.transparent, elevation: 0, surfaceTintColor: Colors.transparent, flexibleSpace: Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/background.jpg'), // 图片路径 fit: BoxFit.cover, // 覆盖整个区域 ), ), ), ), body: Container( width: 430, height: 900, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/background.jpg'), // 图片路径 fit: BoxFit.cover, // 覆盖整个区域 ), ), child: projects.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.table_chart, size: 64, color: Colors.grey), SizedBox(height: 16), Text('暂无表格项目', style: TextStyle(fontSize: 18)), SizedBox(height: 8), Text('点击右下角按钮创建新项目'), ], ), ) : ListView.builder( padding: EdgeInsets.all(8), itemCount: projects.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.only(bottom: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Colors.grey.withOpacity(0.15), ), child: ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), minVerticalPadding: 24, // 设置最小垂直间距 title: TextField( controller: TextEditingController(text: projects[index].name), decoration: InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.zero, hintText: '点击此处可编辑名称', ), style: TextStyle(fontSize: 18), onChanged: (value) { setState(() { projects[index].name = value; _saveProjects(); }); }, ), trailing: Row( mainAxisSize: MainAxisSize.min, // 让Row尽量小 children: [ IconButton( icon: Icon(Icons.edit, color: Colors.black45), // 编辑图标,蓝色 onPressed: () => _navigateToTableDetail( context, projects[index]), // 编辑项目的函数,需要用户自己实现 ), IconButton( icon: Icon(Icons.delete, color: Colors.black45), // 删除图标 onPressed: () => _showDeleteConfirmation(context, index), //onPressed: () => _deleteProject(index), ), ], ), onTap: () => _navigateToTableDetail(context, projects[index]), ), ); }, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add, color: Colors.white), backgroundColor: Colors.grey, //onPressed: _addNewProject, onPressed: () => _addNewProject(context), tooltip: '添加新项目', ), ); } // 删除确认弹窗方法 void _showDeleteConfirmation(BuildContext context, int index) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('确认删除'), content: const Text('确定要永久删除此项目吗?此操作不可撤销。'), actions: [ // 取消按钮 TextButton( onPressed: () => Navigator.pop(context), child: const Text( '取消', style: TextStyle(color: Colors.black), ), ), // 确认删除按钮 ElevatedButton( onPressed: () { Navigator.pop(context); // 关闭弹窗 _deleteProject(index); // 执行删除操作 }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, // 红色警示色 ), child: const Text('删除', style: TextStyle(color: Colors.white)), ), ], ); }, ); } } class TableDetailPage extends StatefulWidget { final Project project; TableDetailPage({required this.project}); @override _TableDetailPageState createState() => _TableDetailPageState(); } class _TableDetailPageState extends State<TableDetailPage> { late Project _project; bool _isSaving = false; @override void initState() { super.initState(); _project = widget.project; } Future<void> _saveProject() async { setState(() => _isSaving = true); try { final prefs = await SharedPreferences.getInstance(); final String? projectsJson = prefs.getString('projects'); List<Project> projects = []; if (projectsJson != null) { List<dynamic> jsonList = json.decode(projectsJson); projects = jsonList.map((json) => Project.fromJson(json)).toList(); } // 查找当前项目在列表中的位置 int index = projects.indexWhere((p) => p.name == _project.name); if (index != -1) { projects[index] = _project; } else { projects.add(_project); } // 保存更新后的项目列表 final String updatedJson = json.encode(projects.map((p) => p.toJson()).toList()); await prefs.setString('projects', updatedJson); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存成功!'), backgroundColor: Colors.green, duration: Duration(seconds: 2), )); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存失败: $e'), backgroundColor: Colors.red, )); } finally { setState(() => _isSaving = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_project.name, style: TextStyle(color: Colors.black)), flexibleSpace: Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/background.jpg'), // 图片路径 fit: BoxFit.cover, // 覆盖整个区域 ), ), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), elevation: 0, backgroundColor: Colors.transparent, centerTitle: true, ), body: Container( width: 430, height: 900, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/background.jpg'), // 图片路径 fit: BoxFit.cover, // 覆盖整个区域 ), ), child: SingleChildScrollView( child: Container( padding: EdgeInsets.all(16), child: Table( border: TableBorder.all(color: Colors.black), columnWidths: { 0: FixedColumnWidth(80), 1: FixedColumnWidth(240), 2: FixedColumnWidth(80), }, children: [ // 表头 TableRow( decoration: BoxDecoration(color: Colors.transparent), children: [ TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('固定列', style: TextStyle(), textAlign: TextAlign.center), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('可编辑列', style: TextStyle(), textAlign: TextAlign.center), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('操作', style: TextStyle(), textAlign: TextAlign.center), )), ], ), // 表格内容(9行) ...List.generate(9, (rowIndex) { return TableRow( decoration: BoxDecoration( color: Colors.transparent, ), children: [ // 第一列:固定内容,不可修改 TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text( _project.rows[rowIndex].fixedContent, style: TextStyle(color: Colors.black), ), ), ), // 第二列:可编辑内容,点击编辑 TableCell( child: Padding( padding: EdgeInsets.all(8), child: TextField( controller: TextEditingController.fromValue( TextEditingValue( text: _project.rows[rowIndex].editableContent, selection: TextSelection.collapsed( offset: _project.rows[rowIndex] .editableContent.length, // 光标在末尾 ), )), onChanged: (value) { setState(() { _project.rows[rowIndex].editableContent = value; }); }, textDirection: TextDirection.ltr, maxLines: null, // 关键参数:允许无限多行(自动扩展) minLines: 2, // 初始最小显示行数 keyboardType: TextInputType.multiline, // 启用多行键盘 textInputAction: TextInputAction.newline, // 回车键换行 decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 8), hintText: '输入内容', border: InputBorder.none, ), ), ), ), // 第三列:按钮,弹出预设内容 TableCell( child: Padding( padding: EdgeInsets.all(8), child: IconButton( icon: Icon(Icons.lightbulb_outline, size: 20), color: Colors.black45, onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: Text('系统信息'), content: SingleChildScrollView( child: Text( _project.rows[rowIndex].presetContent, style: TextStyle(fontSize: 16), ), ), actions: [ TextButton( child: Text('关闭'), onPressed: () => Navigator.pop(context), ), ], ), ); }, ), ), ), ], ); }), TableRow( decoration: BoxDecoration(color: Colors.transparent), children: [ TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('练习', style: TextStyle(), textAlign: TextAlign.center), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('点击右下角练习按钮进行练习!', style: TextStyle(), textAlign: TextAlign.center), )), TableCell( child: Padding( padding: EdgeInsets.all(12), child: Text('', style: TextStyle(), textAlign: TextAlign.center), )), ], ), ], ), ), )), // 添加底部保存按钮 floatingActionButton: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: 30), FloatingActionButton.extended( icon: Icon(Icons.save), label: Text(_isSaving ? '保存中...' : '保存'), onPressed: _isSaving ? null : _saveProject, elevation: 0, backgroundColor: Colors.black38, foregroundColor: Colors.white, ), SizedBox(width: 120), FloatingActionButton.extended( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const PracticePage()), // 目标页面 ); }, icon: Icon(Icons.directions_run), label: Text('练习'), backgroundColor: Colors.black38, foregroundColor: Colors.white, elevation: 0, ), ])); } }错误提示:Another exception was thrown: Error: Could not find the correct Provider<AuthProvider> above this ProjectListPage Widget
最新发布
12-04
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

talk_8

真诚赞赏,手有余香

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值