flutter之敲木鱼练习跟练--续2
接着上篇文章,动画部分的完成--我们来组装一下已经写好的几个模块
首先,需要建一个新页面 muyu_home.dart 有状态组件
import 'package:flutter/material.dart';
// 主页面
class Muyu_Home extends StatefulWidget {
const Muyu_Home({super.key});
@override
State<Muyu_Home> createState() => _Muyu_HomeState();
}
class _Muyu_HomeState extends State<Muyu_Home> {
@override
Widget build(BuildContext context) {
return Container();
}
}
flutter 的语法是样式和函数是组合在一起的,不同于前端中vue的样式和函数拆分开来,一开始会感觉这种糅合在一起的有点不太舒服,但写的多了就会有所适应, 它们各有千秋! 那么回到小案例,组装写好的各个模块,代码如下
import 'package:flutter/material.dart';
import 'package:flutter_app_seven/muyu/animate_text.dart';
import 'package:flutter_app_seven/muyu/countpanel.dart';
import 'package:flutter_app_seven/muyu/muyuImage.dart';
// 主页面
class Muyu_Home extends StatefulWidget {
const Muyu_Home({super.key});
@override
State<Muyu_Home> createState() => _Muyu_HomeState();
}
class _Muyu_HomeState extends State<Muyu_Home> {
@override
Widget build(BuildContext context) {
var activeImage;
var _cruRecord;
var _counter;
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
titleTextStyle: const TextStyle(
color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold),
iconTheme: const IconThemeData(color: Colors.black),
title: const Text('电子木鱼'),
actions: [
IconButton(onPressed: () {}, icon: const Icon(Icons.history))
]),
body: Column(
children: [
Expanded(
//功德数+右侧切木鱼样式和音乐按钮组件,传递三个参数给组件 1.功德数总值 切换音乐 切换图片
child: CountPanel(
count: _counter,
onTapSwitchAudio: () {},
onTapSwitchImage: () {})),
//下半部分组件--木鱼图片
Expanded(
child: Stack(
alignment: Alignment.topCenter,
children: [
MuyuAssetsImage(image: activeImage, onTap: () {}),
if (_cruRecord != null)
AnimateText(
record: _cruRecord!,
)
],
))
],
),
);
}
}
此时运行一下程序,会看到主页面只有功德数和右侧按钮,木鱼图片可以先放一个本地图片。
组装完成主界面如下:
页面样式搞定了,需要考虑数据交互的问题!
再次回顾整体功能点:
- 1.敲击木鱼,加功德数,并伴有文字动画由深变浅、由大到小的动画文字效果;
- 2.界面功德数实时变化,随着敲击的次数增加;
- 3.点击右侧音乐按钮,底部弹出音效列表面板,可切换敲木鱼音效;
- 4.点击右侧图片按钮,底部弹出木鱼面板,可切换,不同木鱼所代表功德数值不同;
- 5.点击导航栏右侧历史icon,弹出抽屉层,记录功德数历史列表,列表信息有id、敲击时间、增加的功德数,当前木鱼的图片
先来列举出数据:
1.功能点1里面,功德值总数、增加的功德值,
2.音效列表,存放木鱼音效
3.图片列表,存放图片地址,图片所代表的功德值
4.历史列表,存放当前敲击木鱼时间、木鱼id、木鱼样式、木鱼增加功德值
5.当前图片index,当前音效index,默认index
为了后续不在重复定义数据,直接封装数据类 woodenfishenity.dart
代码如下:
// 木鱼记录信息 封装成一个类 一个数据模型
class MeritRecord {
final String id;
final int timestamp;
final int value;
final String image;
final String audio;
// 这个类指向它的实例
MeritRecord(this.id, this.audio, this.image, this.timestamp, this.value);
Map<String, dynamic> toJson() => {
"id": id, //记录的唯一标识
"timestamp": timestamp, //记录的时间戳
"image": image, //图片资源
"audio": audio, //音效名称
"value": value, //功德数
};
}
// 敲木鱼音效和样式的选择
class AudioOption {
final String name;
final String src;
const AudioOption(this.name, this.src);
}
// 维护不同样式敲木鱼的数据
class ImageOption {
final String name;//图片名称
final String src;//图片地址
///资源路径
final int min; //每次点击功德最小值
final int max; //每次点击积功德最大值
const ImageOption(this.max, this.min, this.name, this.src);
}
关于这里的id,我们使用flutter 的一个uuid 插件,它可以生成不同的id, 链接:pub.dev 使用方法:
//即可生成一個id
String id = uuid.v4();
当前时间获取:
DateTime.now().millisecondsSinceEpoch,
在木鱼图片组件中封装了一个onKnock方法,敲击实现很简单,定义两个变量,一个为实时增加的值,一个为总数,敲一次总数进行累加,传值给功德数组件,实时增加数传值给动画组件;但别忘了历史面板,敲击时也需要将信息存入到历史列表中;_onkOCK方法如下:
在木鱼图片组件接收一个onTap方法,将存入的敲击木鱼信息传递给AnimateText[动画组件],
动画组件接收父组件传递的值
由于子组件
是有状态的Widget
,所以不能直接使用
动画效果有了,来看更为简单的功德总数
功德面板代码如下:
import 'package:flutter/material.dart';
// 子widget 封装了功德数总值和 音效切换 图片切换
class CountPanel extends StatelessWidget {
final int count;
final VoidCallback onTapSwitchAudio;
final VoidCallback onTapSwitchImage;
const CountPanel({
super.key,
required this.count,
required this.onTapSwitchAudio,
required this.onTapSwitchImage,
});
@override
Widget build(BuildContext context) {
final ButtonStyle style = ElevatedButton.styleFrom(
minimumSize: const Size(50, 50),
padding: EdgeInsets.zero,
backgroundColor: Colors.green,
elevation: 0,
);
return Stack(
children: [
Center(
child: Text(
'功德数: $count',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
Positioned(
right: 10,
top: 10,
child: Wrap(
spacing: 8,
direction: Axis.vertical,
children: [
ElevatedButton(
style: style,
onPressed: onTapSwitchAudio,
child: const Icon(
Icons.music_note_outlined,
color: Colors.white,
),
),
ElevatedButton(
style: style,
onPressed: onTapSwitchImage,
child: const Icon(
Icons.image,
color: Colors.white,
),
)
],
)),
],
);
}
}
小贴士:有状态组件与无状态组件调用父组件的值的方式不同!
接下来,重难点来了!!!
木鱼图片样式切换功能
和木鱼音效切换功能
思考:它两实现思路是一致的,只要会其一,其二变通
First
:在敲木鱼的主页面中定义好木鱼图片列表、木鱼音效列表,以及默认音效的index,默认木鱼图片的index
这样数据流向是从上到下
Second
:定义数据源后,对于音效需要进行初始化一下
补充:音效播放插件-》
木鱼图片面板组合木鱼音效面板也需要进行封装成单独模块
Third:木鱼图片面板模块
界面分析:上下布局,内容左右布局-column+row+expand
这里主要分析一下实现切换图片的一个思路:
在主界面定义图片数据源--初始化时默认使用第一张图片,图片的index值作为切换图片的关键参数,
点击右侧图片按钮,从底部弹出面板->这里用到一个弹框类似以vue里面的showModal——flutter中的 showCupertinoModalPopup,
基本语法如下:
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) {
//此处ImageOptionPanel 是封装好的木鱼选择面板组件
return ImageOptionPanel(
imageOptions: imageOptions,
activeIndex: _activeIndex,
onSelect: _onSelectImage,
);
},
);
在这个弹框里面定义一个select切换方法,当前图片的index、及图片列表数据。
此时index值已传入子组件中,将index赋值给当前组件中激活的index,得到一个当前被选中的木鱼-
-在点击关闭或者切换时,做一个监听回调,将当前的index值回调给父组件-由父组件来判断回调之后的
index是否与原来一致,不一致则用新的index传入木鱼图片组件之中,一致则返回。
木鱼切换面板代码如下:
import 'package:flutter/material.dart';
import 'package:flutter_app_seven/muyu/fishenity.dart';
import 'package:flutter_app_seven/muyu/imageoption.dart';
//切换图片面板
class ImageOptionPanel extends StatelessWidget {
final List<ImageOption> imageOptions;
// 在子widget写value changed 把数据回调给父widget的使用处
final ValueChanged<int> onSelect;
final int activeIndex;
const ImageOptionPanel(
{super.key,
required this.imageOptions,
required this.onSelect,
required this.activeIndex});
Widget _buildByIndex(int index) {
// 传入一个参数index
bool active = index == activeIndex;
//监听回调手势
return GestureDetector(
onTap: () => onSelect(index),
// 单条ImageItem option
child: ImageOptionItem(
option: imageOptions[index],
active: active,
),
);
}
@override
Widget build(BuildContext context) {
// 自定义标签样式
const TextStyle labelStyle =
TextStyle(fontSize: 16, fontWeight: FontWeight.bold);
const EdgeInsets padding =
EdgeInsets.symmetric(horizontal: 8.0, vertical: 16);
return Material(
child: SizedBox(
height: 300,
child: Column(
children: [
Container(
height: 46,
alignment: Alignment.center,
child: const Text("选择木鱼", style: labelStyle)),
Expanded(
child: Padding(
padding: padding,
child: Row(
children: [
Expanded(child: _buildByIndex(0)),
const SizedBox(width: 10),
Expanded(child: _buildByIndex(1)),
],
),
))
],
),
),
);
}
}
切换木鱼音效的思路也是一致的,不做重复叙述
历史列表功能ending
点击右侧历史icon跳转到历史列表中,先定义个toHistory()方法
flutter Navigator + MaterialPageRoute 实现页面跳转、跳转并传值、返回、返回并传值、返回到指定页面
Widget使用ListView、ListTile、CircleAvatar等等,这些Widget可以学习一下。 历史列表代码如下:
// 敲木鱼历史记录
import 'package:flutter/material.dart';
import 'package:flutter_app_seven/muyu/fishenity.dart';
import 'package:intl/intl.dart';
// 分析一波:历史记录界面 listview 构成
class RecordHistoryList extends StatelessWidget {
final List<MeritRecord> records;
const RecordHistoryList({super.key, required this.records});
@override
Widget build(BuildContext context) {
// 时间格式处理
DateFormat format = DateFormat('yyyy年MM月dd日 HH:mm:ss');
Widget? _buildItem(BuildContext context, int index) {
MeritRecord merit = records[index]; //下标
String date =
format.format(DateTime.fromMillisecondsSinceEpoch(merit.timestamp));
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
backgroundImage: AssetImage(merit.image),
),
title: Text('功德+${merit.value}'),
subtitle: Text(merit.audio),
trailing: Text(
date,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
);
}
return Scaffold(
appBar: _buildAppBar(),
body: ListView.builder(
itemBuilder: _buildItem,
itemCount: records.length,
),
);
}
}
// buildAppBar PreferredSizeWidget 作用是用修改appBard的尺寸
PreferredSizeWidget _buildAppBar() => AppBar(
automaticallyImplyLeading: true,
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true, //标题居中
title: const Text('功德记录',
style: TextStyle(color: Colors.black, fontSize: 16)),
elevation: 0,
backgroundColor: Colors.white,
);
每天输出一点,收获就多一点; 路漫漫其修远兮,吾将上下而求索
转载自:https://juejin.cn/post/7349052422059622436