# flutter_faster_study **Repository Path**: ggbhack/flutter_faster_study ## Basic Information - **Project Name**: flutter_faster_study - **Description**: 课程来源于 快学flutter 在博主分享的基础上,使用代码实现 - **Primary Language**: Dart - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-07-25 - **Last Updated**: 2023-03-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # flutter_faster_study 准备写一些比较潮流的UI,于是看了B站博主的视频,感觉不错,开始写 项目运行:点击单独的main文件,右击 执行,后期等写多了,会做统一的页面跳转,暂时先这样处理; 在写的过程中,可能在文件命名上有自己的很多风格,多多包涵; ### 目录结构介绍 assets 静态文件目录 * lib demo 路径 * apps 里面就是每个小应用 * [onboading Flutter快速编码实践-onboading](./lib/apps/onboading/) * [health_master 包括showcase + Flutter 体能管家APP-跟着短视频健身](./lib/apps/health_master/) * [orientation_builder Flutter方向控制](./lib/apps/orientation_builder/) * [QR 二维码生成和扫码](./lib/apps/QR/) * [ui_code_clean 用Flutter实现2021最流行的玻璃态页面-体能管家APP](./lib/apps/ui_code_clean/) * [widget_info 获取widget的位置信息和大小](./lib/apps/widget_info/) * [change_theme 使用getX的方式实现 改变主题样式](./lib/apps/change_theme/) * [auth_face_finger 指纹识别和生物认证](./lib/apps/auth_face_finger/) * [save_preferences Flutter持久化用户登录信息](./lib/apps/save_preferences/) * [animate_sidemenu 自定义菜单栏动画+翻转动画](./lib/apps/animate_sidemenu/) * [banner-demo 旋转标签样式Banner](./lib/apps/network_check/) * [animation_list_demo Flutter动画效果List](./lib/apps/animation_list_demo/) * [video_test 测试rtsp视频流](./lib/apps/video_test/) * [network_check Flutter检查网络连接](./lib/apps/network_check/) * [badge_demo Flutter 徽标 | 小红点 | Badge](./lib/apps/badge_demo/) * [animation_fade_scale Flutter渐变动画控制Widget](./lib/apps/animation_fade_scale/) * [animation_open_container Flutter页面过渡动画效果 ](./lib/apps/animation_open_container/) * [battery_demo Flutter获取设备电池电量、充电状态](./lib/apps/battery_demo/) * [fliter_demo 模糊效果](./lib/apps/fliter_demo/) * [animation_page 模糊效果](./lib/apps/animation_page/) ### Flutter 重新创建指定语言的android/ios目录 1. 移除android目录,重新创建指定语言的android目录 #### 进入工程目录,删除android目录 ``` rm -rf android ``` #### 重新创建java语言的android目录 ``` flutter create -a java . ``` #### 重新创建kotlin语言的android目录 ``` flutter create -a kotlin . ``` 2. 移除ios目录,重新创建指定语言的ios目录 #### 进入工程目录,删除ios ``` rm -rf ios ``` #### 重新创建指定swift语言的ios目录 ``` flutter create -i swift . ``` #### 重新创建指定objective-c 语言的ios目录 ``` 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 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 有监听上下滑动和左右滑动的功能 这里主要演示左右滑动的交互 ``` 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; } } ``` ### Flutter获取Widget的大小和位置信息 获取的原理,通过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 权限 ``` ``` 修改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 中配置相应的内容 ``` NSFaceIDUsageDescription Why is my app authenticating using face id? ``` 判断是否支持生物识别 判断指纹识别是否正确 判断支持生物识别的那些类型 ``` import 'package:flutter/services.dart'; import 'package:local_auth/local_auth.dart'; class LocalAuthApi { static final _auth = LocalAuthentication(); // 获取生物识别功能 static Future 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 hasBiometrics() async { try { return await _auth.canCheckBiometrics; } on PlatformException catch (e) { return false; } } // 获取设备支持生物识别列表 static Future> getBiometrics() async { try { return await _auth.getAvailableBiometrics(); } on PlatformException catch (e) { return []; } } } ``` ### 功能引导 插件 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]); } ``` ### 实现居中的tab 菜单 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控制视频的状态 ### getX 实现 主题切换 比较简单,但demo案例 ### Error: Cannot fit requested classes in a single dex file【解决方法】 今天爱分享给大家带来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; 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, //布局构建器 }) ``` ### app状态监听 文章参考 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) { 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 didPopRoute() { print('YM--------didPopRoute'); //页面弹出 return Future.value(false); //true为拦截,false不拦截 } @override Future didPushRoute(String route) { print('YM--------PushRoute'); return Future.value(true); } @override Future 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'); // }; } ``` ### 开发者必须知道的30个命令 ``` 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 flutter logs-d 在控制台中,看到指定运行设备的日志 flutter clean 删除临时文件 比如build文件夹中的文件 flutter build --relese 构建发布版本的app flutter build --debug 构建发布版本的app flutter install -d 卸载老版本,安装新版本 flutter install --uninstall-only -d 卸载老版本 flutter attach -d 连接设备热加载 flutter config --enable--desktop 支持左面端 flutter config --enable-web 支持web flutter config --android-sdk 指定android sdk的环境路径 flutter config --clear-features 恢复默认配置 flutter format . 格式化后的代码 ``` ### AnimatedList 的使用 AnimatedList提供了一种简单的方式使列表数据发生变化时加入过渡动画; 文章参考:http://laomengit.com/flutter/widgets/AnimatedList.html 相对于来说比较全,里面说到了两种获取AnimationListState的方式,通过AnimationListState可以控制动画的展示和隐藏,列表的添加和移除,都对应着动画,同时列表的移除,就需要在移除原始数据的基础上,还需要将AnimationListState对应的动画移除,在次渲染数据,才会产生动画,而添加动画,则是添加数据,添加动画,不需要从新渲染列表; ``` class _HomeScreenState extends State { final items = List.from(Data.shoppingList); final key = GlobalKey(); List _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 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, ), ), ), ); } ``` ### banner 的使用 在父组件的角上显示一个对角线的消息的控件,比如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 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 ``` 整体来时不难,只需要通过异步获取网络的状态,然后判断其状态,处理自己需要的逻辑!在实时监听板块,需要用一个流获取传过来的值,并需要在页面销毁的时候取消监听; [项目参考](./lib/apps/network_check/) 代码实现 ``` 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 { 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 如果使用组件,需要配置即可,如果使用自定义的 徽标 | 小红点 | Badge;实现逻辑 在底部buttonNavibarItem里面的icon 添加帧布局,在里面天机图标和 徽标 | 小红点 | Badge ;实现逻辑不难,[具体参考](./lib/app/../apps/badge_demo/screens/home/home_screen.dart) ``` 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 { 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, ), ), ), ) ], ); } } ``` ### Flutter页面过渡动画效果 页面过度动画效果,使用的是animations的OpenContainer() 组件; 重新里面的两个方法,达到容器的切换 [完整代码参考](./lib/apps/animation_open_container/screens/home/home_screen.dart) ``` 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 流式布局 配合页面动画 插件 flutter_staggered_grid_view [完整代码参考](./lib/apps/animation_open_container/screens/home/home_screen.dart) ``` 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), ), ``` ### Flutter获取设备电池电量、充电状态 主要有两个方法,一个是获取电量,一个是获取状态,充电的状态; 使用到插件 :battery: ^2.0.3 [完整代码示例参考](./lib/apps/battery_demo/screens/home/home_screen.dart) ``` final battery = Battery(); int batteryLevel = 100; Timer timer; BatteryState batteryState = BatteryState.full; StreamSubscription 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(); } ``` ### 模糊效果 具体案列看 [fliter_demo 模糊效果](./lib/apps/fliter_demo/) ``` BackdropFilter( filter: ImageFilter.blur( sigmaX: blurImage * 100, sigmaY: blurImage * 100, ), child: Container( color: Colors.black.withOpacity(0.2), ), ), ``` ### 动画效果切换页面 数据animations包下面 [代码参考](./libs/apps/animition_page/screens/home/home_screen.dart) ``` 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(), ), ```