手撕一个app,提词器,模仿FeelWorld app的核心功能实现,最终效果如下:
配合蓝牙手柄使用的效果如下:
功能如上图所示
播放速度设置
字体大小设置
字体颜色及背景颜色设置
文本镜像设置
是否循环播放设置
强制横屏显示设置
。。。
所有代码如下:
import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test_demos/custom_drawer.dart';
class TiciqiPage extends StatefulWidget {
@override
_TiciqiPageState createState() => _TiciqiPageState();
}
class _TiciqiPageState extends State {
ScrollController _scrollController = ScrollController();
double _screenHeight = 0.0;
int _counter = 0;
Timer _timer = null;
bool _isPaused = false;
double _speedValue = 5;
bool _mirror = false;
bool _loop = false;
bool _vertical = false;
int _textSelectedIndex = 0;
int _fontSize = 20;
@override
void initState() {
// TODO: implement initState
super.initState();
_startTimer();
}
_startTimer() {
if (_timer == null) {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
_scrollController.animateTo((_counter * _speedValue * 3).toDouble(),
duration: Duration(milliseconds: 250), curve: Curves.easeIn);
_counter++;
print("counter= $_counter");
});
}
}
_pauseTimer() {
if (_timer != null) {
_timer.cancel();
_timer = null;
}
}
_stopTimer() {
if (_timer != null) {
_counter = 0;
_timer.cancel();
_timer = null;
}
}
Color _getScaffoldBackgroundColor() {
Color desColor = Colors.black;
if (_textSelectedIndex == 1) {
desColor = Colors.white;
} else if (_textSelectedIndex == 2) {
desColor = Colors.green;
} else if (_textSelectedIndex == 3) {
desColor = Colors.pink;
} else if (_textSelectedIndex == 4) {
desColor = Colors.blue;
}
return desColor;
}
Color _getTextColor() {
Color desColor = Colors.white;
if (_textSelectedIndex == 1) {
desColor = Colors.black;
} else if (_textSelectedIndex == 2) {
desColor = Colors.white;
} else if (_textSelectedIndex == 3) {
desColor = Colors.white;
} else if (_textSelectedIndex == 4) {
desColor = Colors.white;
}
return desColor;
}
@override
Widget build(BuildContext context) {
_screenHeight = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: _getScaffoldBackgroundColor(),
drawer: _drawer(),
body: NotificationListener(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
if (progress >= 1) {
_stopTimer();
if (_loop) {
Future.delayed(Duration(seconds: 1), () {
_startTimer();
});
}
}
print("BottomEdge: ${notification.metrics.extentAfter == 0}");
return false;
//return true; //放开此行注释后,进度条将失效
},
child: Stack(
alignment: Alignment.center,
children: [
Transform(
// Transform widget
transform: Matrix4.rotationX(_mirror ? pi : 0),
alignment: FractionalOffset.center,
child: Container(
height: _screenHeight,
child: ListView(
controller: _scrollController,
children: [_itemRender()],
),
), // <<< set your widget here
),
Container(
color: Colors.red,
height: 1,
alignment: Alignment.center,
),
],
),
),
floatingActionButton: Builder(
builder: (BuildContext context) {
return FloatingActionButton(
child: IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
print("refresh");
//重新开始(循环)
// if(!_isPaused){//暂停与播放
// _isPaused = true;
// _pauseTimer();
// }else{
// _isPaused = false;
// _startTimer();
// }
//镜像
Scaffold.of(context).openDrawer();
// _handlerDrawerButton(context);
},
),
);
},
),
);
}
Widget _drawerHeader() {
return Builder(
builder: (BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
width: double.maxFinite,
// color: Colors.red,
height: 100,
child: TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.black)),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
child: Row(
children: [Icon(Icons.arrow_back_ios), Text("设置")],
),
));
},
);
}
Widget _drawerFooter() {
return InkWell(
onTap: () {
print("_drawerFooter");
},
child: Container(
alignment: Alignment.center,
width: double.maxFinite,
// color: Colors.red,
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Text(
"恢复默认设置",
style: TextStyle(color: Colors.black),
)
],
)),
);
}
Widget _drawerMiddle() {
return Expanded(
child: ListView(
children: [
_createSliderItem(title: "播放速度", leftString: "慢", rightString: "快"),
_createFontSizeSliderItem(
title: "字体大小", leftString: "小", rightString: "大"),
// _createSliderItem(
// title: "文本显示位置", leftString: "0", rightString: "10"),
_createSFontItem(),
_createTextSwitchItem(title: "文本镜像"),
_createLoopSwitchItem(title: "循环播放"),
_createVerticalSwitchItem(title: "横屏显示"),
],
),
);
}
Widget _createSliderItem(
{String title, String leftString, String rightString}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Text(title),
Container(
// color: Colors.red,
child: Row(
children: [
Text(leftString),
SliderTheme(
data: SliderThemeData(
tickMarkShape: RoundSliderTickMarkShape(tickMarkRadius: 2),
trackHeight: 5,
// activeTrackColor: Colors.red,
// inactiveTrackColor: Colors.grey,
// disabledActiveTrackColor: Colors.yellow,
// disabledInactiveTrackColor: Colors.cyan,
// activeTickMarkColor: Colors.black,
// inactiveTickMarkColor: Colors.red,
// overlayColor: Colors.yellow,
// overlappingShapeStrokeColor: Colors.black,
// overlayShape: RoundSliderOverlayShape(),
// valueIndicatorColor: Colors.red,
// showValueIndicator: ShowValueIndicator.onlyForDiscrete,
// minThumbSeparation: 100,
//
// rangeTrackShape: RoundedRectRangeSliderTrackShape(),
),
child: Slider(
divisions: 20,
label: "${_speedValue.toInt()}",
min: 5,
max: 20,
value: _speedValue,
onChanged: (value) {
print("value = $value");
_speedValue = value;
setState(() {});
}),
),
Text(rightString),
],
),
)
],
);
}
Widget _createFontSizeSliderItem(
{String title, String leftString, String rightString}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Text(title),
Container(
// color: Colors.red,
child: Row(
children: [
Text(leftString),
SliderTheme(
data: SliderThemeData(
tickMarkShape: RoundSliderTickMarkShape(tickMarkRadius: 2),
trackHeight: 5,
// activeTrackColor: Colors.red,
// inactiveTrackColor: Colors.grey,
// disabledActiveTrackColor: Colors.yellow,
// disabledInactiveTrackColor: Colors.cyan,
// activeTickMarkColor: Colors.black,
// inactiveTickMarkColor: Colors.red,
// overlayColor: Colors.yellow,
// overlappingShapeStrokeColor: Colors.black,
// overlayShape: RoundSliderOverlayShape(),
// valueIndicatorColor: Colors.red,
// showValueIndicator: ShowValueIndicator.onlyForDiscrete,
// minThumbSeparation: 100,
//
// rangeTrackShape: RoundedRectRangeSliderTrackShape(),
),
child: Slider(
divisions: 20,
label: "$_fontSize",
min: 20,
max: 50,
value: _fontSize.toDouble(),
onChanged: (value) {
print("value = $value");
_fontSize = value.toInt();
setState(() {});
}),
),
Text(rightString),
],
),
)
],
);
}
Widget _createSpeedSliderItem(
{String title, String leftString, String rightString}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Text(title),
Container(
// color: Colors.red,
child: Row(
children: [
Text(leftString),
SliderTheme(
data: SliderThemeData(
tickMarkShape: RoundSliderTickMarkShape(tickMarkRadius: 2),
trackHeight: 5,
// activeTrackColor: Colors.red,
// inactiveTrackColor: Colors.grey,
// disabledActiveTrackColor: Colors.yellow,
// disabledInactiveTrackColor: Colors.cyan,
// activeTickMarkColor: Colors.black,
// inactiveTickMarkColor: Colors.red,
// overlayColor: Colors.yellow,
// overlappingShapeStrokeColor: Colors.black,
// overlayShape: RoundSliderOverlayShape(),
// valueIndicatorColor: Colors.red,
// showValueIndicator: ShowValueIndicator.onlyForDiscrete,
// minThumbSeparation: 100,
//
// rangeTrackShape: RoundedRectRangeSliderTrackShape(),
),
child: Slider(
divisions: 15,
label: "$_speedValue",
min: 5,
max: 20,
value: _speedValue.toDouble(),
onChanged: (value) {
print("value = $value");
_speedValue = value;
setState(() {});
}),
),
Text(rightString),
],
),
)
],
);
}
Widget _createSFontItem() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Text("字体及背景颜色"),
SizedBox(
height: 15,
),
Container(
// color: Colors.red,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
_textSelectedIndex = 0;
setState(() {});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.red),
borderRadius: BorderRadius.circular(30)),
width: 30,
height: 30,
child: CircleAvatar(
child: Text(
"黑",
style: TextStyle(color: Colors.white, fontSize: 10),
),
backgroundColor: Colors.black,
)),
),
InkWell(
onTap: () {
_textSelectedIndex = 1;
setState(() {});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.red),
borderRadius: BorderRadius.circular(30)),
width: 30,
height: 30,
child: CircleAvatar(
child: Text(
"白",
style: TextStyle(color: Colors.black, fontSize: 10),
),
backgroundColor: Colors.white,
)),
),
InkWell(
onTap: () {
_textSelectedIndex = 2;
setState(() {});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.red),
borderRadius: BorderRadius.circular(30)),
width: 30,
height: 30,
child: CircleAvatar(
child: Text(
"绿",
style: TextStyle(color: Colors.green, fontSize: 10),
),
backgroundColor: Colors.blueGrey,
)),
),
InkWell(
onTap: () {
_textSelectedIndex = 3;
setState(() {});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.red),
borderRadius: BorderRadius.circular(30)),
width: 30,
height: 30,
child: CircleAvatar(
child: Text(
"粉",
style: TextStyle(color: Colors.pink, fontSize: 10),
),
backgroundColor: Colors.pinkAccent,
)),
),
InkWell(
onTap: () {
_textSelectedIndex = 4;
setState(() {});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.red),
borderRadius: BorderRadius.circular(30)),
width: 30,
height: 30,
child: CircleAvatar(
child: Text(
"蓝",
style: TextStyle(color: Colors.blue, fontSize: 10),
),
backgroundColor: Colors.lightBlueAccent,
)),
),
SizedBox(
width: 30,
)
],
),
),
SizedBox(
height: 20,
),
],
);
}
Widget _createTextSwitchItem({String title}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Container(
// color: Colors.red,
child: Row(
children: [
CupertinoSwitch(
value: _mirror,
onChanged: (value) {
print("Switch $value");
_mirror = value;
setState(() {});
}),
Text(title),
],
),
),
SizedBox(
height: 10,
),
],
);
}
Widget _createLoopSwitchItem({String title}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Container(
// color: Colors.red,
child: Row(
children: [
CupertinoSwitch(
value: _loop,
onChanged: (value) {
print("Switch $value");
_loop = value;
setState(() {});
}),
Text(title),
],
),
),
SizedBox(
height: 10,
),
],
);
}
Widget _createVerticalSwitchItem({String title}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createDivider(),
SizedBox(
height: 10,
),
Container(
// color: Colors.red,
child: Row(
children: [
CupertinoSwitch(
value: _vertical,
onChanged: (value) {
print("Switch $value");
_vertical = value;
if (_vertical) {
// 强制横屏
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight
]);
} else {
// 强制竖屏
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown
]);
}
setState(() {});
}),
Text(title),
],
),
),
SizedBox(
height: 10,
),
],
);
}
Widget _drawer() {
return CustomDrawer(
child: Padding(
padding: EdgeInsets.only(left: 20),
child: Column(
children: [_drawerHeader(), _drawerMiddle(), _drawerFooter()],
),
),
widthPercent: 0.7,
);
}
void _handlerDrawerButton(BuildContext context) {
Scaffold.of(context).openDrawer();
}
Widget _createDivider() {
return Divider(
height: 1,
thickness: 0.5,
color: Color.fromARGB(100, 200, 200, 200),
);
}
Widget _itemRender() {
return Column(
children: [
Container(
child: Text("start"),
height: _screenHeight * 0.5,
),
Container(
child: ListTile(
title: Text(
'''对王羲之这位书圣的生平,史书记载十分有限,但他召集的那次兰亭集会,千百年来却广为传颂,因为王羲之即兴创作、记录这次盛会的《兰亭集序》不仅是文学史上的不朽之作,也是中国书法史上无人逾越的高峰。
东晋永和九年(353年)农历三月三日,天朗气清,惠风和畅,王羲之和朋友们举行了一次修禊活动,地点选择在会稽境内的兰亭。所谓“修禊”,是古代一种消除污秽的祭祀活动,一般多在农历三月三日春光明媚之时,选择水边,一方面祭神,同时大家也洗洗手脚,据说可免除邪恶不祥。这项仪式始于西周,但逐渐从最初的祭神发展到了游春赏景、饮酒赋诗的踏青活动。
兰亭雅集的参加者,据唐何延之《兰亭记》的记载,有谢安、孙绰、支遁……还有王羲之的三个儿子共四十一人,众人饮酒赋诗作文,最后由已有几分酒意的王羲之执笔为之作序。王羲之轻拈鼠须笔、铺开蚕茧纸,用他最擅长的中锋行楷,洋洋洒洒28行、324字一挥而就。在这篇短文中,王羲之既描写了兰亭优美的自然环境,又抒写了与朋友相聚的欢欣,同时也抒发了人生苦短、及时行乐、快然自足的情怀。文章理趣深远,沁人心脾,而书法更是遒媚劲健变化无穷,二十多个“之”字无一雷同,如有神助。事后,王羲之将《兰亭集序》重写了几十幅,均自叹不如原本。
''',
style: TextStyle(
wordSpacing: 3,
fontSize: _fontSize.toDouble(),
color: _getTextColor()),
),
)),
Container(
alignment: Alignment.bottomRight,
height: _screenHeight,
child: Text("end"),
),
],
);
}
}
上面的代码,有用到一个自定义的Drawer类 CustomDrawer
,代码如下:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class CustomDrawer extends StatefulWidget {
const CustomDrawer({
this.backgroundColor,
this.elevation = 16.0,
@required this.child,
this.widthPercent,
this.callback,
}) : assert(widthPercent < 1.0 && widthPercent > 0.0);
final Color backgroundColor;
final double elevation;
final Widget child;
final double widthPercent;
final DrawerCallback callback;
@override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State {
@override
void initState() {
if (widget.callback != null) {
widget.callback(true);
}
super.initState();
}
@override
void dispose() {
if (widget.callback != null) {
widget.callback(false);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final double _width =
MediaQuery.of(context).size.width * widget.widthPercent;
return ConstrainedBox(
constraints: BoxConstraints.expand(width: _width),
child: Material(
color: widget.backgroundColor,
elevation: widget.elevation,
child: widget.child,
),
);
}
}
结尾
今天的干货分享到此,如果小伴们,觉得有点用的话,或者已经看到这里面来的请点个赞吧,后面如果有必要完善更多细节,比如:做缓存显示等功能,会持续更新此app的相关核心代码。期待一下吧。好运~