准备写一些比较潮流的UI,于是看了B站博主的视频,感觉不错,开始写
项目运行:点击单独的main文件,右击 执行,后期等写多了,会做统一的页面跳转,暂时先这样处理; 在写的过程中,可能在文件命名上有自己的很多风格,多多包涵;
assets 静态文件目录
rm -rf android
flutter create -a java .
flutter create -a kotlin .
rm -rf ios
flutter create -i swift .
flutter create -i objc .
使用到的库 glassmorphism: ^1.0.4 sleek_circular_slider: ^1.2.0+web
生成二维码demo
BarcodeWidget(
data: controller.text,
barcode: Barcode.qrCode(),
color: Colors.white,
width: 200,
height: 200,
),
查看二维码:
Future<void> scanQRCode() async {
try {
final qrCode = await FlutterBarcodeScanner.scanBarcode(
'#ff6666',
'取消',
true,
ScanMode.QR,
);
if (!mounted) return; // 未加载成功,直接返回
setState(() {
this.qrCode = qrCode;
});
} on PlatformException {
qrCode = '获取信息失败';
}
}
iso需要做配置:具体参考 https://share.weiyun.com/qKsTTMmM
GestureDetector 有监听上下滑动和左右滑动的功能 这里主要演示左右滑动的交互
GestureDetector(
onHorizontalDragEnd: swiperFunction,...)
void swiperFunction(DragEndDetails details) {
final selectedIndex = ExerciseType.values.indexOf(selectedType);
final hasNext = selectedIndex < ExerciseType.values.length - 1;
final hasPrevious = selectedIndex > 0;
setState(() {
if (details.primaryVelocity < 0 && hasNext) {
final nextType = ExerciseType.values[selectedIndex + 1];
selectedType = nextType;
}
if (details.primaryVelocity > 0 && hasPrevious) {
final previousType = ExerciseType.values[selectedIndex - 1];
selectedType = previousType;
}
});
}
定义枚举 并使用 用于做菜单比较合适
enum ExerciseType { low, mid, hard }
String getExerciseName(ExerciseType type) {
switch (type) {
case ExerciseType.hard:
return "剧烈";
break;
case ExerciseType.mid:
return "适中";
break;
case ExerciseType.low:
return "热身";
break;
default:
return "全部";
break;
}
}
获取的原理,通过globalKey绑定在一个weidget上,在渲染完成之后获取绘制的信息;在从回执信息的获取宽高信息和位置信息
// 定义key 绑定key
final keyText = GlobalKey();
Size size;
Offset position;
// 获取方法
// 该方法保证了在(build函数渲染完之后)页面渲染完成之后执行
void calculateSizeAndPosition() =>
WidgetsBinding.instance.addPostFrameCallback((_) {
final RenderBox box =
keyText.currentContext.findRenderObject(); // 包含widget的绘制信息
setState(() {
position = box.localToGlobal(Offset.zero); // 获取绝对信息 从左上角
size = box.size;
});
});
// 调用
calculateSizeAndPosition();
也可以在init方法中调用
void initState() {
super.initState();
calculateSizeAndPosition();
}
// 展示
Text('宽度:${size.width.toInt()}'),
Text('高度:${size.height.toInt()}'),
Text('x坐标:${position.dx.toInt()}'),
Text('y坐标:${position.dy.toInt()}'),
插件地址:https://pub.flutter-io.cn/packages/local_auth
添加android 权限
<user-permission android:name="android.permission.USE_FINGERPRINT"></user-permission>
修改kotlin下的MainActivity.kt
package com.example.flutter_faster_study
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.engine.flutterEngine
import androidx.annotation.MonNull;
class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine){
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
ios 需要在info.plist 中配置相应的内容
<key>NSFaceIDUsageDescription</key>
<string>Why is my app authenticating using face id?</string>
判断是否支持生物识别
判断指纹识别是否正确
判断支持生物识别的那些类型
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
class LocalAuthApi {
static final _auth = LocalAuthentication();
// 获取生物识别功能
static Future<bool> authenticate() async {
// 判断设备是否支持生物识别功能
final isAvailable = await hasBiometrics();
if (!isAvailable) return false;
try {
return await _auth.authenticateWithBiometrics(
localizedReason: "请进行指纹识别",
useErrorDialogs: true,
stickyAuth: true,
);
} on PlatformException catch (e) {
return false;
}
}
// 调用这个方法 判断设备是否支持生物识别功能
static Future<bool> hasBiometrics() async {
try {
return await _auth.canCheckBiometrics;
} on PlatformException catch (e) {
return false;
}
}
// 获取设备支持生物识别列表
static Future<List<BiometricType>> getBiometrics() async {
try {
return await _auth.getAvailableBiometrics();
} on PlatformException catch (e) {
return <BiometricType>[];
}
}
}
插件 https://pub.flutter-io.cn/packages/showcaseview
第一步: 在需要的widget中进行包裹
ShowCaseWidget(
builder: Builder(builder: (context) => HomeScreen()),
autoPlay: false,
autoPlayDelay: Duration(seconds: 3),
autoPlayLockEnable: false,
),
第二步 在需要的页面 添加globalKey
GlobalKey _one = GlobalKey();
GlobalKey _two = GlobalKey();
GlobalKey _three = GlobalKey();
GlobalKey _four = GlobalKey();
第三步 封装
import 'package:flutter/material.dart';
import 'package:showcaseview/showcaseview.dart';
class CustomShowcaseWidget extends StatelessWidget {
const CustomShowcaseWidget(
{Key key,
@required this.child,
@required this.desctiption,
@required this.globalKey})
: super(key: key);
final Widget child;
final String desctiption;
final GlobalKey globalKey;
@override
Widget build(BuildContext context) => Showcase.withWidget(
overlayColor: Colors.black,
overlayOpacity: 0.3,
height: 80,
width: 140,
key: globalKey, // 之前在这里错过
shapeBorder: CircleBorder(),
description: desctiption,
container: Column(
children: [
Container(
decoration: BoxDecoration(
color: Color(0xfff2abc3),
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.all(10),
child: Text(
"$desctiption",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
child: child,
);
}
第四步 使用
CustomShowcaseWidget(
desctiption: '个人中心',
globalKey: _three,
child: Icon(
Icons.person,
size: 28,
),
),
第五步 触发 在init触发
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => ShowCaseWidget.of(context).startShowCase([_one, _two, _three]));
}
通过事件触发
someEvent(){
ShowCaseWidget.of(context).startShowCase([_one, _two, _three]);
}
floatingActionButtnoLocation:FloatingAcetionButtonLocatin.centerDocked, floationgActonButton:xxx
用 OrientationBuilder 包裹子widget
body: Container(
padding: EdgeInsets.all(32),
// orientation 包含两个方向一个水平,一个垂直
child: OrientationBuilder(
builder: (context, orientation) =>
orientation == Orientation.portrait
? buildPortrait()
: buildLandscape(),
),
),
// 方法出发
floatingActionButton: FloatingActionButton(
child: Icon(Icons.rotate_left),
onPressed: () {
final isPortrait =
MediaQuery.of(context).orientation == Orientation.portrait;
isPortrait ? setLandscape() : setPortrait();
},
),
// 设置竖屏
Future setPortrait() async => await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
// 设置横屏
Future setLandscape() async => await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 清除设置
Future setPortraintAndLandscape() =>
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
参考video_player 插件介绍 里面介绍的相当的细 https://pub.flutter-io.cn/packages/video_player 实现逻辑: 1.页面使用pageview构建 对应主 pageController 2.每个视频对应一个controller playVideoController 3.在视频页面加载时 为每个视频添加一个controller,并在页面加载完成后,初始化,并播放,页面销毁后,销毁controller 4.在页面切换时,做两间事情,通过pageController改变展示的page,并将当前对应的视频,传递给video页面;从而实现视频的切换 5.通过当前播放视频的controller控制视频的状态
今天爱分享给大家带来Error: Cannot fit requested classes in a single dex file【解决方法】,希望能够帮助到大家。
Error: Cannot fit requested classes in a single dex file (# methods: 72725 > 65536)
1.在app的gradle下defaultConfig配置添加:
multiDexEnabled true
2.在app的gradle下的dependencies配置添加:
implementation 'com.android.support:multidex:1.0.3'
插件 flutter_native_splash
配置:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_native_splash: ^1.2.0
flutter_native_splash:
color: "#42a5f5"
image: assets/splash/icon.png
background_image: assets/splash/background.png
color_dark: "#042a49"
image_dark: assets/splash/icon.png
background_image_dark: "assets/splash/dark-background.png"
android: true
ios: true
web: true
fullscreen: true
android_gravity: fill
ios_content_mode: scaleAspectFill
命令:
flutter clean && flutter pub get && flutter pub run flutter_native_splash:create
https://book.flutterchina.club/chapter9/stagger_animation.html 参考 给动画添加顺序
看文档会比较清楚一点;可根据查看文档来了解
AnimationController _animationController;
List<Animation<double>> _animation;
int _selectedIndex = 1;
@override
void initState() {
super.initState();
_animationController = new AnimationController(
vsync: this,
duration: Duration(milliseconds: 700),
);
final _intervalGap = 1 / widget.items.length;
_animation = List.generate(
widget.items.length,
(index) => Tween(
begin: 0.0,
end: 1.6,
).animate(
// 交织动画
CurvedAnimation(
parent: _animationController,
curve: Interval(
index * _intervalGap,// 该值在0-1.0 之间
index * _intervalGap + _intervalGap,
),
),
),
);
// 一开始 隐藏菜单栏
_animationController.forward(from: 1.0);
}
// 展示部分
for (int i = 0; i < widget.items.length; i++)
Positioned(
left: 0,
top: itemSize * i,
width: width,
height: itemSize,
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(_animationController.status ==
AnimationStatus.reverse
? -_animation[widget.items.length - 1 - i].value
: -_animation[i].value),
alignment: Alignment.bottomLeft,
child: Material(
color: i == _selectedIndex
? widget.selectedColor
: widget.unselectedColor,
child: InkWell(
onTap: () {
widget.onItemSelected(i);
if (i != 0) {
setState(() {
_selectedIndex = i;
});
}
_animationController.forward(from: 0.0);
},
child: widget.items[i],
),
),
),
),
AnimatedSwitcher 文章参考http://laomengit.com/flutter/widgets/AnimatedSwitcher.html
https://book.flutterchina.club/chapter9/animated_switcher.html#_9-6-1-animatedswitcher
const AnimatedSwitcher({
Key key,
this.child,
@required this.duration, // 新child显示动画时长
this.reverseDuration,// 旧child隐藏的动画时长
this.switchInCurve = Curves.linear, // 新child显示的动画曲线
this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
文章参考 https://blog.csdn.net/Mr_Tony/article/details/112261101
主要经历以下几个步骤: 1.with WidgetsBindingObserver 2.初始化 添加监听者 WidgetsBinding.instance.addObserver(this); 3.页面销毁 移除监听者 WidgetsBinding.instance.removeObserver(this); // 销毁监听 4.复写监听者的生命周期函数
// 以下是WidgetsBindObserver的方法复写
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
// 应用程序处于闲置状态并且没有收到用户的输入事件。
//注意这个状态,在切换到后台时候会触发,所以流程应该是先冻结窗口,然后停止UI
print('YM----->AppLifecycleState.inactive');
break;
case AppLifecycleState.paused:
// 应用程序处于不可见状态 相当于进入了后台
TextPreferences.setText(controller.text);
print('YM----->AppLifecycleState.paused');
break;
case AppLifecycleState.resumed:
// 进入应用时候不会触发该状态
// 应用程序处于可见状态,并且可以响应用户的输入事件。它相当于 Android 中Activity的onResume。
print('YM----->AppLifecycleState.resumed');
break;
case AppLifecycleState.detached:
//当前页面即将退出
print('YM----->AppLifecycleState.detached');
break;
}
}
///当前系统改变了一些访问性活动的回调
@override
void didChangeAccessibilityFeatures() {
super.didChangeAccessibilityFeatures();
print("YM-----@@@@@@@@@ didChangeAccessibilityFeatures");
}
///低内存回调
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
print("YM-----@@@@@@@@@ didHaveMemoryPressure");
}
///用户本地设置变化时调用,如系统语言改变
@override
void didChangeLocales(List<Locale> locale) {
super.didChangeLocales(locale);
print("YM-----@@@@@@@@@ didChangeLocales");
}
///应用尺寸改变时回调,例如旋转
@override
void didChangeMetrics() {
super.didChangeMetrics();
Size size = WidgetsBinding.instance.window.physicalSize;
print(
"YM-----@@@@@@@@@ didChangeMetrics :宽:${size.width} 高:${size.height}");
}
@override
Future<bool> didPopRoute() {
print('YM--------didPopRoute'); //页面弹出
return Future.value(false); //true为拦截,false不拦截
}
@override
Future<bool> didPushRoute(String route) {
print('YM--------PushRoute');
return Future.value(true);
}
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
print('YM--------didPushRouteInformation');
return Future.value(true);
}
//文字大小改变时候的监听
@override
void didChangeTextScaleFactor() {
print(
"YM--------@@@@@@@@@ didChangeTextScaleFactor :${WidgetsBinding.instance.window.textScaleFactor}");
}
@override
void didChangePlatformBrightness() {
final window = WidgetsBinding.instance.window;
final brightness = window.platformBrightness;
// Brightness.light 亮色
// Brightness.dark 暗色
print('YM----平台主题改变----didChangePlatformBrightness$brightness');
// window.onPlatformBrightnessChanged = () {
// // This callback gets invoked every time brightness changes
// final brightness = window.platformBrightness;
// print('YM----平台亮度改变----didChangePlatformBrightness$brightness');
// };
}
flutter doctor 检查flutter SDK是否正确
flutter doctor -v 查看SKD配置出现的详细问题
flutter upgrade 下载并更新flutter sdk
flutter packages get 下载第三方flutter包
flutter create my_app 创建一个flutter项目
flutter create--org=com.appxxxx my_app 创建包含报名的flutter项目
flutter create --org=com.appxxx --project-name=appxxx my_app 添加项目名称(在项目名称和app名称不止一致,可以添加一个project-name )
flutter create -org=com.appxxxx --android-language=java my_app 创建使用特定语言的项目
flutter create -org=com.appxxxx --ios-language=objc my_app 创建使用特定语言的项目
flutter create -org=com.appxxxx --android-language=java --ios-language=objc my_app 也可以使用该命令改已有的项目
flutter create --no-voerwrite -org=com.appxxxx my_app 如果我们删除了一些文件,想创建,但不覆盖原始的代码,使用该命令 创建flutter项目缺少的新文件
创建多平台 需要把channel改为master
flutter create --platforms=windows .
flutter create --platforms=macos .
flutter create --platforms=linux .
flutter create --platforms=web .
flutter channel master
flutter devices 列出连接到电脑中的所有设备
flutter run -d xxxx 在指定的设备上运行app
需要了解出错的原因,在后面添加一个 -v flutter run -d xxxx -v 即可
flutter run -d xxxx -t lib/main.dart 指定运行的文件
flutter run -d windows -t lib/main_win.dart 指定运行的文件 运行到window平台
flutter channel 使用哪个版本
flutter channel <channer-name>
flutter logs-d <divice-id> 在控制台中,看到指定运行设备的日志
flutter clean 删除临时文件 比如build文件夹中的文件
flutter build <target-platform-name> --relese 构建发布版本的app
flutter build <target-platform-name> --debug 构建发布版本的app
flutter install -d <platform-name> 卸载老版本,安装新版本
flutter install --uninstall-only -d <platform-name> 卸载老版本
flutter attach -d <platform-name> 连接设备热加载
flutter config --enable-<platform-name>-desktop 支持左面端
flutter config --enable-web 支持web
flutter config --android-sdk <path-to-android-skd> 指定android sdk的环境路径
flutter config --clear-features 恢复默认配置
flutter format . 格式化后的代码
AnimatedList提供了一种简单的方式使列表数据发生变化时加入过渡动画; 文章参考:http://laomengit.com/flutter/widgets/AnimatedList.html
相对于来说比较全,里面说到了两种获取AnimationListState的方式,通过AnimationListState可以控制动画的展示和隐藏,列表的添加和移除,都对应着动画,同时列表的移除,就需要在移除原始数据的基础上,还需要将AnimationListState对应的动画移除,在次渲染数据,才会产生动画,而添加动画,则是添加数据,添加动画,不需要从新渲染列表;
class _HomeScreenState extends State<HomeScreen> {
final items = List.from(Data.shoppingList);
final key = GlobalKey<AnimatedListState>();
List<int> _list = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画列表"),
),
body: Column(
children: [
Expanded(
child: AnimatedList(
key: key,
initialItemCount: items.length,
itemBuilder:
(BuildContext context, int index, Animation animation) {
return buildItem(items[index], index, animation);
},
),
),
Container(
child: buildInsetButton(),
),
],
),
);
}
Widget buildInsetButton() => ElevatedButton(
child: Text("添加"),
onPressed: () => insertItem(3, Data.shoppingList.first),
);
Widget buildItem(
ShopItemModel item, int index, Animation<double> animation) =>
ShopItemWidget(
item: item,
animation: animation,
onClicked: () => removeItem(index),
);
void removeItem(int index) {
final item = items.removeAt(index);
key.currentState.removeItem(
index, (context, animation) => buildItem(item, index, animation));
}
void insertItem(int index, ShopItemModel item) {
items.insert(index, item);
key.currentState.insertItem(index);
}
}
class ShopItemWidget extends StatelessWidget {
final ShopItemModel item;
final Animation animation;
final VoidCallback onClicked;
const ShopItemWidget(
{@required this.item,
@required this.animation,
@required this.onClicked,
Key key})
: super(key: key);
@override
Widget build(BuildContext context) => ScaleTransition(
scale: animation,
child: Container(
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
leading: CircleAvatar(
radius: 32,
backgroundImage: AssetImage(item.urlImage),
),
title: Text(
item.title,
style: TextStyle(
fontSize: 20,
),
),
trailing: IconButton(
icon: Icon(Icons.check_circle, color: Colors.green, size: 32),
onPressed: onClicked,
),
),
),
);
}
在父组件的角上显示一个对角线的消息的控件,比如debug模式下,显示在App右上角的DEBUG就是此组件实现的。 组件参考:http://laomengit.com/flutter/widgets/Banner.html#banner 自定义banner 的原理;使用一个帧布局,然后给需要的banner一个旋转的动画属性;在需要的banner上层使用position定位
import 'package:flutter/material.dart';
import 'card_widget.dart';
class BannerCustomPage extends StatelessWidget {
final topLeft = AlwaysStoppedAnimation(-45 / 360);
final topRight = AlwaysStoppedAnimation(45 / 360);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text("自定义banner"),
),
body: ListView(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(16),
children: [
buildCardTopLeft(),
SizedBox(height: 16),
buildCardTopRight(),
SizedBox(height: 16),
]));
buildCardTopLeft() => Stack(children: [
CardWidget(),
Positioned(
left: -32,
top: 20,
child: buildBadge(turns: topLeft),
),
]);
buildCardTopRight() => Stack(children: [
CardWidget(),
Positioned(
right: -32,
top: 20,
child: buildBadge(turns: topRight),
),
]);
buildBadge({Animation<double> turns}) => RotationTransition(
turns: turns,
child: Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 36),
color: Colors.teal,
child: Text(
"热门产品",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 20,
),
),
),
);
}
需要用到 依赖包
注意,需要使用OverlaySupport.global 包裹materialApp;具体看插件说明
# 网络监测
connectivity: ^3.0.6
# appbar 上的提示框
overlay_support: ^1.2.1
整体来时不难,只需要通过异步获取网络的状态,然后判断其状态,处理自己需要的逻辑!在实时监听板块,需要用一个流获取传过来的值,并需要在页面销毁的时候取消监听;
代码实现
import 'dart:async';
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
import '../../utils.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
StreamSubscription subscription;
@override
void initState() {
super.initState();
subscription =
Connectivity().onConnectivityChanged.listen(showConnectivityStackBar);
}
@override
void dispose() {
super.dispose();
subscription.cancel();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text("是否连接到网络?"),
),
body: Center(
child: ElevatedButton(
child: Text(
"检查连接",
style: TextStyle(fontSize: 20),
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(12),
),
onPressed: () async {
final result = await Connectivity().checkConnectivity();
showConnectivityStackBar(result);
},
),
),
);
void showConnectivityStackBar(ConnectivityResult result) {
final hasInternet = result != ConnectivityResult.none;
final message = hasInternet ? '连接结果${result.toString()}' : '没有连接到Internet';
final color = hasInternet ? Colors.green : Colors.red;
Utils.showTopSnackBar(message, color);
}
}
如果使用组件,需要配置即可,如果使用自定义的 徽标 | 小红点 | Badge;实现逻辑 在底部buttonNavibarItem里面的icon 添加帧布局,在里面天机图标和 徽标 | 小红点 | Badge ;实现逻辑不难,具体参考
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int index = 0;
int countFavourites = 98;
int countMessages = 9;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("徽章"),
),
bottomNavigationBar: buildBottomBar(),
body: Container(
padding: EdgeInsets.all(32),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
toAnimate: true,
badgeColor: Colors.teal,
padding: EdgeInsets.all(8),
badgeContent: Text(
'$countFavourites',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
child: buildButton(
text: "添加收藏",
onClicked: () => setState(() => countMessages += 1),
),
),
const SizedBox(height: 32),
Badge(
position: BadgePosition.topStart(),
toAnimate: true,
badgeColor: Colors.teal,
padding: EdgeInsets.all(8),
badgeContent: Text(
'$countFavourites',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
child: buildButton(
text: "添加消息",
onClicked: () => setState(() => countFavourites += 1),
),
),
const SizedBox(height: 32),
],
),
),
),
);
}
Widget buildBottomBar() {
final style = TextStyle(color: Colors.black);
return BottomNavigationBar(
backgroundColor: Theme.of(context).primaryColor,
selectedItemColor: Colors.black,
unselectedItemColor: Colors.black54,
currentIndex: index,
items: [
BottomNavigationBarItem(
icon: buildCustomBadge(
counter: countFavourites,
child: Icon(Icons.favorite_border),
),
label: '收藏'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
],
onTap: (int index) => setState(() {
this.index = index;
}),
);
}
buildButton({String text, VoidCallback onClicked}) => ElevatedButton(
child: Text(
text,
style: TextStyle(
fontSize: 20,
),
),
onPressed: onClicked,
style: ElevatedButton.styleFrom(
minimumSize: Size.fromHeight(50),
primary: Colors.pink,
),
);
Widget buildCustomBadge({@required int counter, @required Widget child}) {
final text = counter.toString();
final deltaFontSize = (text.length - 1) * 3.0;
return Stack(
clipBehavior: Clip.hardEdge,
children: [
child,
Positioned(
top: -6,
right: -20,
child: CircleAvatar(
backgroundColor: Colors.white,
child: Text(
text,
style: TextStyle(
fontSize: 16 - deltaFontSize,
fontWeight: FontWeight.bold,
),
),
),
)
],
);
}
}
页面过度动画效果,使用的是animations的OpenContainer() 组件; 重新里面的两个方法,达到容器的切换 完整代码参考
final transitionType = ContainerTransitionType.fade;
OpenContainer(
transitionType: transitionType,
transitionDuration: Duration(milliseconds: 500),
openBuilder: (context, _) => DetailScreen(card: items[index]),
closedBuilder: (context, VoidCallback openContainer) => CardWidget(
card: items[index],
onClicked: openContainer,
),
),
插件 flutter_staggered_grid_view
StaggeredGridView.countBuilder(
crossAxisCount: 4,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
itemCount: items.length,
itemBuilder: (BuildContext context, int index) => OpenContainer(
transitionType: transitionType,
transitionDuration: Duration(milliseconds: 500),
openBuilder: (context, _) => DetailScreen(card: items[index]),
closedBuilder: (context, VoidCallback openContainer) => CardWidget(
card: items[index],
onClicked: openContainer,
),
),
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(2, index.isEven ? 3 : 2),
),
主要有两个方法,一个是获取电量,一个是获取状态,充电的状态; 使用到插件 :battery: ^2.0.3
final battery = Battery();
int batteryLevel = 100;
Timer timer;
BatteryState batteryState = BatteryState.full;
StreamSubscription<BatteryState> subscription;
@override
void initState() {
super.initState();
listenBatteryLevel();
listenBatteryState();
}
void listenBatteryState() {
subscription = battery.onBatteryStateChanged.listen((BatteryState state) {
setState(() {
batteryState = state;
});
});
}
void listenBatteryLevel() {
updateBatterylevel();
timer = Timer.periodic(
Duration(seconds: 10),
(_) async => updateBatterylevel(),
);
}
Future updateBatterylevel() async {
final batteryLevel = await battery.batteryLevel;
setState(() {
this.batteryLevel = batteryLevel;
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
timer.cancel();
subscription.cancel();
}
具体案列看
BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurImage * 100,
sigmaY: blurImage * 100,
),
child: Container(
color: Colors.black.withOpacity(0.2),
),
),
数据animations包下面 代码参考
PageTransitionSwitcher(
duration: Duration(milliseconds: 1200),
reverse: isFirst, // 更好的体验优化
transitionBuilder: (child, animation, secondaryAnimation) =>
SharedAxisTransition(
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType:
SharedAxisTransitionType.horizontal, // 动画类型,水平方向过度下一个页面
),
child: isFirst ? FirstScreen() : SecondScreen(),
),
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。