• [技术交流] 开发者技术支持-Flutter跟鸿蒙的交互pigeon
    ​ 1.问题说明:Flutter 跟鸿蒙原生的双向交互的需要2.原因分析:目前Flutter使用最多、最流行的交互SDK是pigeon,可以生成Flutter侧的.g.dart和鸿蒙侧是.g.ets文件进行双向交互文件3.解决思路:在Flutter项目中的pubspec.yaml文件中,导入 pigeon,使用终端命令生成双向交互文件4.解决方案:一、导入pigeondev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 pigeon: git: url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" path: "packages/pigeon"二、创建交互抽象文件(抽象类)share_plugin_api.dartimport 'package:pigeon/pigeon.dart';enum PlatformType { friend, // 朋友圈 wechat, // 微信好友 xlwb, // 新浪微博 xhs, // 小红书 qq, // QQ album, // 保存到相册}class ShareDataModel { PlatformType? platform; // 分享的平台类型 String? title; String? content; String? description; String? url; // 文件或网页H5等链接 String? filePath; // 文件本地路径 Uint8List? fileData; // 文件二级制数据 double? width; // 图片估计需要宽高 double? height; ShareDataModel({ this.platform, this.title, this.content, this.description, this.url, this.filePath, this.fileData, this.width, this.height, });}class ShareResultModel { PlatformType? platform; // 分享的平台类型 bool? isInstalled; // 是否安装了某平台的APP String? message; ShareResultModel({ this.platform, this.isInstalled, this.message, });}@HostApi()abstract class ShareHostApi { // Flutter调用平台侧同步方法 // 分享调原生 void shareSync(ShareDataModel shareModel);}@FlutterApi()abstract class ShareFlutterApi { // 平台侧调用Flutter同步方法 // 分享结果回调,原生调用Flutter void shareResultSync(ShareResultModel resultModel);}三、配置将要生成文件路径的main文件,生成文件,使用命令:dart run lib/common/plugins/tool/generate.dartgenerate.dartimport 'package:pigeon/pigeon.dart';/** *生成文件,使用命令:dart run lib/common/plugins/tool/generate.dart *必须在当前工程目录下 * */void main() async { String inputDir = 'lib/common/plugins/plugin_apis'; String dartGenDir = 'lib/common/plugins/plugin_api_gs'; String arkTSGenDir = 'ohos/entry/src/main/ets/plugins/plugin_api_gs'; // 定义Pigeon任务列表,每个任务对应一个API文件的代码生成任务 // 包含输入文件路径、Dart输出文件路径和ArkTS输出文件路径 final List<PigeonTask> tasks = [ PigeonTask( input: '$inputDir/share_plugin_api.dart', dartOutName: '$dartGenDir/share_plugin_api', arkTSOutName: '$arkTSGenDir/SharePluginApi', ) ]; // 遍历所有任务并执行代码生成 for (final task in tasks) { // 构造Dart输出文件的完整路径,添加.g.dart后缀表示生成的文件 final dartOut = '${task.dartOutName}.g.dart'; // 构造ArkTS输出文件的完整路径,添加.g.ets后缀表示生成的鸿蒙TS文件 final arkTSOut = '${task.arkTSOutName}.g.ets'; // 使用Pigeon工具执行代码生成任务 await Pigeon.runWithOptions(PigeonOptions( input: task.input, // 输入的API定义文件 dartOut: dartOut, // Dart代码输出文件路径 arkTSOut: arkTSOut, // ArkTS代码输出文件路径 )); }}// Pigeon任务数据类,用于封装每个代码生成任务的配置信息class PigeonTask { final String input; // 输入文件路径 final String dartOutName; // Dart输出文件名称(不含后缀) final String arkTSOutName; // ArkTS输出文件名称(不含后缀) PigeonTask({ required this.input, required this.dartOutName, required this.arkTSOutName, });}四、使用命令生成Flutter侧的.g.dart和鸿蒙侧的.g.ets文件share_plugin_api.g.dartSharePluginApi.g.ets五、Flutter侧,实现share_plugin_impl.dart 接收鸿蒙侧的调用import '../plugin_api_gs/share_plugin_api.g.dart';class SharePluginImpl implements ShareFlutterApi { // 分享结果回调,原生调用Flutter @override void shareResultSync(ShareResultModel resultModel) { if (resultModel.isInstalled == false) { // 先判断要分享的APP是否安装 if (resultModel.platform == PlatformType.friend) { // 微信朋友圈 } else if (resultModel.platform == PlatformType.wechat) { // 微信好友 } else if (resultModel.platform == PlatformType.xlwb) { // 新浪微博 } else if (resultModel.platform == PlatformType.xhs) { // 小红书 } else if (resultModel.platform == PlatformType.qq) { // QQ } return; } }}六、Flutter侧文件夹截图  Flutter 的main.dart中,设置Api的实现void main() { WidgetsFlutterBinding.ensureInitialized(); ShareFlutterApi.setup(SharePluginImpl()); runApp(const MyApp());}七、鸿蒙侧创建引擎SharePlugin.etsimport { FlutterPluginBinding } from '@ohos/flutter_ohos';import { FlutterPlugin } from '@ohos/flutter_ohos';import { ShareUtils } from '../../utils/ShareUtils';import { PlatformType, ShareDataModel, ShareFlutterApi, ShareHostApi } from '../plugin_api_gs/SharePluginApi.g';class SharePluginImpl extends ShareHostApi { // 分享调原生 shareSync(shareModel: ShareDataModel): void { if (shareModel.getPlatform() == PlatformType.FRIEND) { // 微信朋友圈 } else if (shareModel.getPlatform() == PlatformType.WECHAT) { // 微信好友 } else if (shareModel.getPlatform() == PlatformType.XLWB) { // 新浪微博 } else if (shareModel.getPlatform() == PlatformType.XHS) { // 小红书 ShareUtils.shareFileToXHS(shareModel); } else if (shareModel.getPlatform() == PlatformType.QQ) { // QQ } }}export default class SharePlugin implements FlutterPlugin { constructor() { } getUniqueClassName(): string { return 'SharePlugin'; } onAttachedToEngine(binding: FlutterPluginBinding) { // 创建设置SharePluginImpl实现Flutter侧的方法 ShareHostApi.setup(binding.getBinaryMessenger(), new SharePluginImpl()); // 创建原生分享Api,用于原生侧调Flutter侧的方法 ShareUtils.shareApi = new ShareFlutterApi(binding.getBinaryMessenger()); } onDetachedFromEngine(binding: FlutterPluginBinding) { // 释放 ShareHostApi.setup(binding.getBinaryMessenger(), null); ShareUtils.shareApi = null; }}八、在鸿蒙EntryAbility.ets入口文件中加入引擎:this.addPlugin(new SharePlugin())import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';import SharePlugin from '../plugins/plugin_impls/SharePlugin';import AbilityConstant from '@ohos.app.ability.AbilityConstant';import Want from '@ohos.app.ability.Want';import { ShareUtils } from '../utils/ShareUtils';export default class EntryAbility extends FlutterAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { super.onCreate(want, launchParam) // 集成注册分享等SDK ShareUtils.shareRegister(this.context, want); } onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void { super.onNewWant(want, launchParams) // 处理分享完毕回调 ShareUtils.handleShareCall(want) } configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) this.addPlugin(new SharePlugin()) }}九、鸿蒙侧创建交互执行具体业务的工具类ShareUtils.ets,import { common, Want } from "@kit.AbilityKit";import { XhsShareSdk } from "@xiaohongshu/open_sdk";import { PlatformType, ShareDataModel, ShareFlutterApi, ShareResultModel} from "../plugins/plugin_api_gs/SharePluginApi.g";export class ShareUtils { // 分享结果用于调Flutter static shareApi: ShareFlutterApi | null = null; // 分享全局的context static context: common.UIAbilityContext // 分享的注册 static shareRegister(context: common.UIAbilityContext, want: Want) { ShareUtils.context = context // 小红书初始化SDK XhsShareSdk.registerApp(context, '小红书的appkey') } // 处理分享完毕回调 static handleShareCall(want: Want) { } // 分享到小红书 static shareFileToXHS(shareModel: ShareDataModel) { // 若未安装小红书,鸿蒙调Flutter侧代码给用用户提示 let resultModel: ShareResultModel = new ShareResultModel(PlatformType.XHS, false, '',); ShareUtils.shareApi?.shareResultSync(resultModel, { reply: () => { // 原生侧调Flutter侧方法完成后的回调 }, }) }}十、鸿蒙侧文件夹截图  十一、Flutter侧调用鸿蒙原生ShareDataModel shareModel = ShareDataModel();if (type == ShareType.friend) { shareModel.platform = PlatformType.friend;} else if (type == ShareType.wechat) { shareModel.platform = PlatformType.wechat;} else if (type == ShareType.xlwb) { shareModel.platform = PlatformType.xlwb;} else if (type == ShareType.xhs) { shareModel.platform = PlatformType.xhs;} else if (type == ShareType.qq) { shareModel.platform = PlatformType.qq;}shareModel.filePath = imageModel.imagePath;shareModel.fileData = imageModel.imageData;shareModel.width = imageModel.width;shareModel.height = imageModel.height;ShareHostApi shareApi = ShareHostApi();shareApi.shareSync(shareModel);十二、作为一个Flutter初学者,恳请大佬们多多指教,多给宝贵意见,万分感谢​
  • [技术交流] 开发者技术支持-Flutter的列表下拉刷新、上拉加载更多组件封装
    1.问题说明:Flutter 原生列表实现下拉、上拉回调加载很费劲,想封装一个基础刷新组件全局使用2.原因分析:目前Flutter使用最多、最流行的网络请求框架是pull_to_refresh: ^2.0.03.解决思路:在Flutter项目中的pubspec.yaml文件中,导入 pull_to_refresh: ^2.0.0,封装刷新组件4.解决方案:一、导入pull_to_refresh: ^2.0.0dependencies: flutter: sdk: flutter pull_to_refresh: ^2.0.0二、封装刷新组件RefreshWidgetimport 'package:flutter/material.dart';import 'package:pull_to_refresh/pull_to_refresh.dart';class RefreshWidget extends StatelessWidget { final Widget child; final Future<void> Function()? onRefresh; final Future<void> Function()? onLoadMore; final bool enablePullDown; final bool enablePullUp; final RefreshController? controller; const RefreshWidget({ Key? key, required this.child, this.onRefresh, this.onLoadMore, this.enablePullDown = true, this.enablePullUp = false, this.controller, }) : super(key: key); @override Widget build(BuildContext context) { return SmartRefresher( enablePullDown: enablePullDown, enablePullUp: enablePullUp, header: const ClassicHeader( idleText: '下拉可以刷新', releaseText: '松开立即刷新', refreshingText: '正在刷新...', completeText: '刷新完成', failedText: '刷新失败', canTwoLevelText: '释放进入二楼', textStyle: TextStyle(color: Colors.grey, fontSize: 14), refreshingIcon: SizedBox( width: 25.0, height: 25.0, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation<Color>(Colors.grey), strokeWidth: 2.0), ), ), footer: const ClassicFooter( idleText: '上拉加载更多', loadingText: '正在加载...', noDataText: '没有更多数据了', canLoadingText: '松开加载更多', failedText: '加载失败', textStyle: TextStyle(color: Colors.grey, fontSize: 14), ), controller: controller ?? RefreshController(), onRefresh: onRefresh, onLoading: onLoadMore, child: child, ); }}三、使用样例,此为部分代码1.RefreshWidget组件的使用RefreshWidget( controller: _viewModel.refreshController, onRefresh: () => _viewModel.onRefresh(), onLoadMore: () => _viewModel.onLoadMore(), enablePullDown: true, enablePullUp: true, child: CustomScrollView( physics: AlwaysScrollableScrollPhysics(), shrinkWrap: true, slivers: _viewModel.groups.asMap().entries.map((e) { return SliverMainAxisGroup( slivers: [ SliverPersistentHeader( delegate: HelpCategoryHeaderDelegate(_viewModel, e.value, e.key), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return HelpCategoryItem(_viewModel, e.value, index); }, childCount: e.value.helpContentList?.length, ), ), ], ); }).toList(), ),)2.ViewModel的部门代码import 'package:get/get.dart';import 'package:pull_to_refresh/pull_to_refresh.dart';import '../models/help_category_model.dart';class HelpCategoryViewModel extends GetxController { late RefreshController refreshController = RefreshController(); int pageNum = 1; int pageSize = 10; RxList<HelpRecordModel> groups = <HelpRecordModel>[].obs; // 下拉刷新 onRefresh() { pageNum = 1; loadData(); } // 上拉加载更多 onLoadMore() { pageNum += 1; loadData(); } // 加载网络数据 Future<void> loadData() async { }}四、作为一个Flutter初学者,恳请大佬们多多指教,多给宝贵意见,万分感谢
  • [技术交流] 开发者技术支持-Flutter的网络请求封装Dio
    1.问题说明:Flutter 原生网络请求很费劲,想封装一个基础网络请求单例全局使用2.原因分析:目前Flutter使用最多、最流行的网络请求框架是dio: ^5.9.03.解决思路:在Flutter项目中的pubspec.yaml文件中,导入 dio: ^5.9.0,创建网络请求单例使用dio,封装请求方法4.解决方案:一、导入dio: ^5.9.0,在Flutter项目中的pubspec.yaml文件中dependencies: flutter: sdk: flutter dio: ^5.9.0二、创建网络请求单例,BaseRequestimport 'dart:convert';import 'package:dio/dio.dart';import 'package:flutter/cupertino.dart';import 'base_response.dart';void LogDebug(String message) { debugPrint("[DEBUG] $message");}class BaseRequest { static final BaseRequest _instance = BaseRequest._internal(); factory BaseRequest() => _instance; BaseRequest._internal() { _initDio(); } late Dio _dio; static const String baseUrl = "https://www.baidu.com/"; void _initDio() { _dio = Dio(); _dio.options.baseUrl = baseUrl; _dio.options.connectTimeout = const Duration(seconds: 10); _dio.options.receiveTimeout = const Duration(seconds: 10); _dio.options.headers["Content-Type"] = "application/json"; _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { // 用户登录接口返回的token final token = ''; if (token != null) { options.headers["Authorization"] = "Bearer $token"; } // 判断是否是上传文件接口,修改请求头 final isUpload = options.headers.containsKey("isUpload") ? options.headers["isUpload"] as bool : false; if (isUpload) { options.headers["Content-Type"] = 'multipart/form-data'; } return handler.next(options); }, onResponse: (response, handler) { LogDebug("requestUrl: ${response.requestOptions.uri}"); LogDebug( "requestHeaders: ${json.encode(response.requestOptions.headers)}"); LogDebug("requestBody: ${json.encode(response.requestOptions.data)}"); LogDebug( "requestParams: ${json.encode(response.requestOptions.queryParameters)}"); LogDebug("responseData: ${json.encode(response.data)}"); // 相应数据转全局基础BaseResponse对象(Model) final baseResponse = BaseResponse.fromJson(response.data as Map<String, dynamic>); response.data = baseResponse; return handler.next(response); }, onError: (DioException e, handler) { LogDebug("requestError: ${e.message}"); return handler.next(e); }, )); } // PUT 请求 Future<BaseResponse?> putRequest( String path, { dynamic data, Map<String, dynamic>? queryParams, }) async { try { final response = await _dio.put( path, data: data, queryParameters: queryParams, ); return response.data as BaseResponse; } catch (e) { LogDebug("PUT 失败: $e"); return null; } } // GET 请求 Future<BaseResponse?> getRequest( String path, { dynamic data, Map<String, dynamic>? queryParams, }) async { try { final response = await _dio.get( path, data: data, queryParameters: queryParams, ); return response.data as BaseResponse; } catch (e) { LogDebug("GET 失败: $e"); return null; } } // POST 请求 Future<BaseResponse?> postRequest( String path, { dynamic data, Map<String, dynamic>? queryParams, }) async { try { final response = await _dio.post( path, data: data, queryParameters: queryParams, ); return response.data as BaseResponse; } catch (e) { LogDebug("POST 失败: $e"); return null; } } // 文件上传 Future<BaseResponse?> postFileUpload( String path, { required FormData formData, }) async { try { final response = await _dio.post( path, data: formData, options: Options(headers: {"isUpload": true}), ); return response.data as BaseResponse; } catch (e) { LogDebug("POST Upload 失败: $e"); return null; } }}三、全局基础相应Model,BaseResponseclass BaseResponse<T> { int? timestamp; int? code; String? msg; T? data; Map<String, dynamic>? errorData; bool? success; BaseResponse({ this.timestamp, this.code, this.msg, this.data, this.errorData, this.success, }); factory BaseResponse.fromJson(Map<String, dynamic> json) { return BaseResponse( timestamp: json['timestamp'] as int?, code: json['code'] as int?, msg: json['msg'] as String?, data: json['data'] as T?, errorData: json['errorData'] as Map<String, dynamic>?, success: json['success'] as bool?, ); } Map<String, dynamic> toJson() { final data = <String, dynamic>{}; data['timestamp'] = timestamp; data['code'] = code; data['msg'] = msg; data['data'] = this.data; data['errorData'] = errorData; data['success'] = success; return data; }}四,请求类Api:RequestApiimport 'dart:io';import 'package:dio/dio.dart';import '../base/base_request.dart';import '../base/base_response.dart';class RequestApi { static final BaseRequest _baseRequest = BaseRequest(); // put请求传requestBody static Future<BaseResponse?> putBody( Map<String, dynamic> body, ) async { return await _baseRequest.putRequest( "", // 接口相对路径 data: body, ); } // put请求传queryParams static Future<BaseResponse?> putParams( Map<String, dynamic> queryParams, ) async { return await _baseRequest.putRequest( "", // 接口相对路径 queryParams: queryParams, ); } // get请求传requestBody static Future<BaseResponse?> getBody( Map<String, dynamic> body, ) async { return await _baseRequest.getRequest( "", // 接口相对路径 data: body, ); } // get请求传queryParams static Future<BaseResponse?> getParams( Map<String, dynamic> queryParams, ) async { return await _baseRequest.getRequest( "", // 接口相对路径 queryParams: queryParams, ); } // post请求传requestBody static Future<BaseResponse?> postBody( Map<String, dynamic> body, ) async { return await _baseRequest.postRequest( "", // 接口相对路径 data: body, ); } // post请求传queryParams static Future<BaseResponse?> postParams( Map<String, dynamic> queryParams, ) async { return await _baseRequest.postRequest( "", // 接口相对路径 queryParams: queryParams, ); } // post上传文件传formData static Future<BaseResponse?> postFileUpload( String filePath, ) async { File fileClass = File(filePath); String fileName = fileClass.path.split('/').last; MultipartFile multipartFile = await MultipartFile.fromFile( filePath, filename: fileName, contentType: DioMediaType.parse("application/octet-stream"), ) as MultipartFile; // 具体的文件类型,请根据自己公司要求来 FormData formData = FormData.fromMap({ 'file': multipartFile, 'type': '0', }); return await _baseRequest.postFileUpload( "", // 接口相对路径 formData: formData, ); }}五、作为一个Flutter初学者,恳请大佬们多多指教,多给宝贵意见,万分感谢
  • [技术交流] 开发者技术支持-Flutter的CustomScrollView列表组头悬浮
    1.问题说明:Flutter为实现列表组头悬浮2.原因分析:ListView组件是没有组头的,只能找其他组件代替,例如:CustomScrollView3.解决思路:CustomScrollView组件slivers是可以添加组SliverMainAxisGroup组件,在SliverMainAxisGroup中包裹组头SliverPersistentHeader和列表SliverList组件,分别在它们的代理组件上实现组头和列表4.解决方案:一、CustomScrollView组件代码实现CustomScrollView( slivers: _viewModel.bloodShareGroups.map((group) { return SliverMainAxisGroup(slivers: [ SliverPersistentHeader( delegate: BloodShareHeaderDelegate(_viewModel, group), pinned: true, ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return BloodShareFriendItem( _viewModel, group, index); }, childCount: group.children.length, ), ), ]); }).toList(),),二、组头SliverPersistentHeader组件的代理组件实现,列表滑动组头悬浮就在于pinned: true,import 'package:flutter/material.dart';import '../../../../common/theme/app_theme.dart';import '../models/blood_share_group_model.dart';import '../viewmodels/blood_share_viewmodel.dart';class BloodShareHeaderDelegate extends SliverPersistentHeaderDelegate { final BloodShareViewModel _viewModel; final BloodShareGroupModel groupModel; const BloodShareHeaderDelegate(this._viewModel, this.groupModel); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return _noLastBuild(); } Widget _noLastBuild() { return Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( color: AppTheme.bodyBackgroundColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(width: 14), Text( groupModel.title, textAlign: TextAlign.start, style: TextStyle( color: Color(0xFF999999), fontSize: 14, fontWeight: FontWeight.normal, ), ), ], ), ); } @override double get maxExtent { return 36; } @override double get minExtent { return 36; } @override bool shouldRebuild(covariant BloodShareHeaderDelegate oldDelegate) { return this.groupModel.title != oldDelegate.groupModel.title; }}三、组列表SliverList组件的代理组件实现直接在原生代理SliverChildBuilderDelegate组件的子组件回调中创建列表的自定义Item常规组件BloodShareFriendItemSliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return BloodShareFriendItem( _viewModel, group, index); }, childCount: group.children.length, ),)四、作为一个Flutter初学者,恳请大佬们多多指教,多给宝贵意见,万分感谢
  • [技术交流] 开发者技术支持-Flutter的ListView实现刻度尺组件
    1.问题说明:Flutter为实现刻度尺组件,可左右滑动、且滑动组件到刻度时在屏幕中间ListView的偏移量的监听是个难点2.原因分析:ListView没有滑动结束的方法回调,无法定位偏移量,只能通过NotificationListener监听组件,去监听ListView是否滑动结束3.解决思路:通过使用NotificationListener监听组件,监听ListView滑动结束,获取ListView的偏移量,计算当前偏移量距离哪个刻度比较近,使用ListView的控制器ScrollController的jumpTo()方法,滑动到对应刻度4.解决方案: 一、刻度尺Dialog的代码实现import 'package:flutter/material.dart';import 'package:get/get.dart';import '../../../../common/theme/app_theme.dart';import '../models/blood_friend_scale_model.dart';class BloodFriendScaleDialog extends StatefulWidget { final FriendScaleDialogModel dialogModel; const BloodFriendScaleDialog( this.dialogModel, ); @override State<BloodFriendScaleDialog> createState() => _BloodFriendScaleDialogState();}class _BloodFriendScaleDialogState extends State<BloodFriendScaleDialog> { late final FriendScaleDialogViewModel _viewModel; late final FriendScaleDialogModel _dialogModel; late final ScrollController _controller; // 滑动控制器用于监听 bool _scrollEnd = true; double _lastOffset = 0; late final double lineWidth = 2; late final double lineSpace = 100; @override void initState() { super.initState(); _viewModel = FriendScaleDialogViewModel(); _dialogModel = widget.dialogModel; _viewModel.dealData(_dialogModel); // 初始化偏移量 double offsetSpace = lineWidth + lineSpace; double jumpOffset = (_viewModel.currentIndex ?? 0) * offsetSpace; _controller = ScrollController( initialScrollOffset: jumpOffset, ); addListener(); } @override void dispose() { _controller.dispose(); super.dispose(); } // 添加偏移量监听 addListener() { _controller.addListener(() { double offsetSpace = lineWidth + lineSpace; int integer = (_controller.offset / offsetSpace).floor(); double residue = _controller.offset % offsetSpace; if (residue > offsetSpace) { integer += 1; } _viewModel.getSureScale(integer); }); } // 更新偏移量 _updateOffset(double pixels) { if (_lastOffset == pixels) { return; } else { _lastOffset = pixels; } if (_scrollEnd) { _scrollEnd = false; double offsetSpace = lineWidth + lineSpace; int integer = (pixels / offsetSpace).floor(); double residue = pixels % offsetSpace; if (residue > offsetSpace / 2) { integer += 1; } double jumpOffset = integer * offsetSpace; _controller.jumpTo(jumpOffset); } } // 点击取消事件 _clickCancelEvent() { Navigator.of(context).pop(); } _clickSureEvent() { if (_dialogModel.clickSureBlock != null) { if (_viewModel.sureScale != null) { _dialogModel.clickSureBlock(_viewModel.sureScale!.value); } } } @override Widget build(BuildContext context) { // 半屏幕间隔,用于第一个和最后一个 double hspace = (MediaQuery.of(context).size.width - lineWidth) / 2; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), ), ), height: 300, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded(child: SizedBox()), GestureDetector( onTap: () => _clickCancelEvent(), child: Text( '取消', textAlign: TextAlign.center, style: TextStyle( color: Color(0xFF999999), fontSize: 15, fontWeight: FontWeight.w600), ), ), SizedBox(width: 15), ], ), Text( _dialogModel.title, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), SizedBox(height: 10), Text( _dialogModel.subTitle, textAlign: TextAlign.center, style: TextStyle( color: Color(0xFF999999), fontSize: 12, fontWeight: FontWeight.w600, ), ), SizedBox(height: 5), Obx(() { return Text.rich( TextSpan( text: _viewModel.sureScale?.value.scale.value ?? '', style: TextStyle( color: Color(0xFFF79797), fontSize: 28, fontWeight: FontWeight.normal, ), children: [ TextSpan( text: _dialogModel.unit, style: TextStyle( color: Color(0xFFF79797), fontSize: 12, fontWeight: FontWeight.w600, ), ) ], ), ); }), SizedBox(height: 5), Container( decoration: BoxDecoration( color: AppTheme.bodyBackgroundColor, ), height: 80, child: Stack( alignment: Alignment.topCenter, children: [ NotificationListener<ScrollNotification>( onNotification: (notification) { if (notification is ScrollEndNotification) { _scrollEnd = true; _updateOffset(notification.metrics.pixels); return true; } _scrollEnd = false; return false; }, child: ListView.builder( controller: _controller, scrollDirection: Axis.horizontal, itemCount: _dialogModel.scales.length, itemBuilder: (BuildContext context, int index) { return Container( decoration: BoxDecoration( color: Colors.transparent, ), width: _getItemWidth(hspace, index), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (index == 0 && _dialogModel.scales.length > 1) SizedBox(width: hspace), Container( alignment: Alignment.topCenter, decoration: BoxDecoration( color: Colors.transparent, ), width: 2, child: Container( decoration: BoxDecoration( color: Color(0xFF999999), ), width: index % 2 == 0 ? 2 : 1, height: index % 2 == 0 ? 40 : 20, ), ), ], ), ); }), ), Positioned( top: -10, child: Image.asset( 'assets/images/personalcenter_scale.png', width: 30, height: 50, fit: BoxFit.fill, ), ), ], ), ), _sureBuilder(), ], ), ); } double _getItemWidth(double hspace, int index) { if (index == 0) { if (_dialogModel.scales.length == 1) { return hspace + lineWidth; } else { return hspace + lineWidth + lineSpace; } } else if (index == _dialogModel.scales.length - 1) { return lineWidth + hspace; } else { return lineWidth + lineSpace; } } // 确定UI Widget _sureBuilder() { return GestureDetector( onTap: () => _clickSureEvent(), child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: Color(0xFFF79797), borderRadius: BorderRadius.horizontal( left: Radius.circular(22), right: Radius.circular(22)), ), height: 44, width: double.infinity, margin: EdgeInsets.only(top: 10, left: 15, right: 15), child: Text( '确定', textAlign: TextAlign.start, style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.normal, ), ), ), ); }}class FriendScaleDialogViewModel extends GetxController { late final FriendScaleDialogModel dialogModel; Rx<BloodFriendScaleModel>? sureScale; int? currentIndex; // 处理数据 dealData(FriendScaleDialogModel model) { dialogModel = model; sureScale = BloodFriendScaleModel(scale: ''.obs).obs; for (int i = 0; i < dialogModel.scales.length; i++) { BloodFriendScaleModel scaleModel = dialogModel.scales[i]; if (dialogModel.currentScale != null) { if (scaleModel.scale == dialogModel.currentScale?.value.scale.value) { currentIndex = i; sureScale?.value.scale.value = scaleModel.scale.value; } } else { if (i == 0) { currentIndex = 0; sureScale?.value.scale.value = scaleModel.scale.value; } } } } // 当前选中的刻度Model赋值 getSureScale(int index) { if (dialogModel.scales.length > index) { currentIndex = index; BloodFriendScaleModel scaleModel = dialogModel.scales[index]; sureScale?.value.scale.value = scaleModel.scale.value; } }}class FriendScaleDialogModel { String title; String subTitle; String unit; List<BloodFriendScaleModel> scales; Rx<BloodFriendScaleModel>? currentScale; Function(BloodFriendScaleModel) clickSureBlock; FriendScaleDialogModel({ required this.title, required this.subTitle, required this.unit, required this.scales, this.currentScale, required this.clickSureBlock, });}二、ViemModel中的代码实现import 'package:flutter/material.dart';import 'package:get/get.dart';import '../dialogs/blood_friend_scale_dialog.dart';import '../models/blood_friend_scale_model.dart';import '../models/blood_friend_warn_model.dart';class BloodFriendAddWarnViewModel extends GetxController { Rx<BloodFriendWarnModel>? warnModel; // 新增或编辑提醒Model // 重复刻度数集合 List<String> scales = <String>[ '0', '1', '3', '5', '10', ]; // 重复刻度Model集合 List<BloodFriendScaleModel> repeats = <BloodFriendScaleModel>[]; // 重复刻度Model集合 getRepeats() { scales.forEach((name) { BloodFriendScaleModel scaleModel = BloodFriendScaleModel( scale: name.obs, ); repeats.add(scaleModel); }); } // 刻度选择器 showScaleSelectPicker(BuildContext context) { FriendScaleDialogModel dialogModel = FriendScaleDialogModel( title: '重复提醒次数', subTitle: '每次响铃重复次数', unit: '次', scales: repeats, currentScale: warnModel?.value.repeat, clickSureBlock: (BloodFriendScaleModel scaleModel) { Navigator.of(context).pop(); warnModel?.value.repeat?.value.scale.value = scaleModel.scale.value; }, ); showModalBottomSheet( enableDrag: false, context: context, builder: (BuildContext sheetContext) { return BloodFriendScaleDialog(dialogModel); }, ); }}三、Model中的代码实现class BloodFriendScaleModel { RxString scale = ''.obs; // 刻度数值 BloodFriendScaleModel({ required this.scale, });}四、个人感悟1.Flutter的列表ListView不像iOS或安卓有滑动方法回调ListView只能通过监听NotificationListener包裹ListView,监听其滑动结束的回调  监听回调更新偏移量  2.ScrollController的addListener只能监听偏移量的变化,在其监听中实现只要滑动偏移量大于等于某个刻度时,当前顶部刻度显示就是某个刻度  3.实现效果如下   4.作为一个Flutter初学者,恳请大佬们多多指教,多给宝贵意见,万分感谢​
  • [技术交流] 开发者技术支持-Flutter的MVVM的设计模式
    ​1.问题说明:Flutter通用的MVVM的设计模式,状态管理2.原因分析:iOS、安卓、鸿蒙,三端APP,页面到子组件状态管理要双向联动 3.解决思路:Flutter三方框架:# getx 框架get: ^4.7.24.解决方案:一、Flutter的三方框架配置在pubspec.yaml文件中version: 1.0.0+1environment: sdk: '>=3.4.0 <4.0.0'# Dependencies specify other packages that your package needs in order to work.# To automatically upgrade your package dependencies to the latest versions# consider running `flutter pub upgrade --major-versions`. Alternatively,# dependencies can be manually updated by changing the version numbers below to# the latest version available on pub.dev. To see which dependencies have newer# versions available, run `flutter pub outdated`.dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 # getx 框架 get: ^4.7.2二、UI搭建:Page页面import 'package:flutter/material.dart';import 'package:get/get.dart';import 'package:gjdg_flutter/pages/personal_center/help_centre/views/search_help_head.dart';import '../../../../common/theme/app_theme.dart';import '../viewmodels/help_centre_viewmodel.dart';import '../views/search_common_problem.dart';import '../views/search_problem_sort.dart';class HelpCentrePage extends StatefulWidget { const HelpCentrePage({super.key}); @override State<HelpCentrePage> createState() => _HelpCentrePageState();}class _HelpCentrePageState extends State<HelpCentrePage> { late final HelpCentreViewModel _viewModel; @override void initState() { super.initState(); _viewModel = Get.put(HelpCentreViewModel()); } // 点击联系在线客服事件 _clickPhoneEvent() { print('点击联系在线客服事件:_clickPhoneEvent'); } // 点击提交意见反馈事件 _clickFeedBackEvent() { print('点击提交意见反馈事件:_clickFeedBackEvent'); } @override Widget build(BuildContext context) { final double screenWidth = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: const Text( "帮助中心", style: TextStyle(fontSize: 18), ), centerTitle: true, leading: IconButton( icon: Icon( Icons.arrow_back_ios, color: Colors.grey.shade500, ), onPressed: () => Get.back(), ), ), backgroundColor: AppTheme.bodyBackgroundColor, body: Container( width: double.infinity, decoration: BoxDecoration( color: AppTheme.bodyBackgroundColor, ), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SearchCommonProblem(viewModel: _viewModel), ], ), ), Container( decoration: BoxDecoration( color: Colors.white, ), padding: const EdgeInsets.only(top: 12, bottom: 30), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( onTap: () => _clickPhoneEvent(), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset( 'assets/images/percenalcenter_phone.png', width: 20, height: 20, fit: BoxFit.contain, ), const SizedBox(width: 4), Text( '联系在线客服', style: const TextStyle( fontSize: 12, ), ), ], ), ), GestureDetector( onTap: () => _clickFeedBackEvent(), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset( 'assets/images/percenalcenter_submit_feedback.png', width: 20, height: 20, fit: BoxFit.contain, ), const SizedBox(width: 4), Text( '提交意见反馈', style: const TextStyle( fontSize: 12, ), ), ], ), ), ], ), ), ], ), ), ); }}三、子组件import 'package:flutter/material.dart';import 'package:get/get.dart';import '../viewmodels/help_centre_viewmodel.dart';class SearchCommonProblem extends StatefulWidget { final HelpCentreViewModel viewModel; const SearchCommonProblem({super.key, required this.viewModel}); @override State<SearchCommonProblem> createState() => _SearchCommonProblemState();}class _SearchCommonProblemState extends State<SearchCommonProblem> { late final HelpCentreViewModel _viewModel; @override void initState() { super.initState(); _viewModel = widget.viewModel; } // 点击常见问题列表事件 _clickProblemEvent(int index) { print('点击常见问题列表事件:_clickProblemEvent'); } @override Widget build(BuildContext context) { return Card( color: Colors.white, elevation: 0, shape: RoundedRectangleBorder( side: BorderSide.none, borderRadius: BorderRadius.circular(10), ), margin: EdgeInsets.symmetric(horizontal: 15), child: Container( width: double.infinity, padding: EdgeInsets.only(left: 15, right: 15, top: 15), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '常见问题', textAlign: TextAlign.start, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ), SizedBox(height: 10), Container( width: double.infinity, height: _viewModel.commonProblems.length * 40, child: Obx(() { return ListView.builder( physics: NeverScrollableScrollPhysics(), itemCount: _viewModel.commonProblems.length, itemExtent: 40, itemBuilder: (itemContext, index) { return GestureDetector( onTap: () => _clickProblemEvent(index), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '${index + 1}', style: TextStyle( color: _viewModel.commonProblems[index].color, fontSize: 16, fontWeight: FontWeight.bold, ), ), SizedBox(width: 8), Text( _viewModel.commonProblems[index].title, style: TextStyle( color: Colors.black, fontSize: 12, ), ), ], ), ), if (index != _viewModel.commonProblems.length - 1) Divider( height: 1, color: Color(0xFFF3F3F3), ) ], ), ); }, ); }), ), ], ), ), ); }}四、Model 数据import 'dart:ui';class CommonProblemModel { String title = ''; // 标题 Color color = Color(0xFFF7B500); CommonProblemModel({ required this.title, required this.color, });}五、ViewModel 状态管理,数据跟UI双向绑定import 'dart:ui';import 'package:get/get.dart';import '../models/common_problem_model.dart';class HelpCentreViewModel extends GetxController { RxList<CommonProblemModel> commonProblems = <CommonProblemModel>[ CommonProblemModel( title: 'CGM佩戴视频(完整版)', color: Color(0xFFD5020D), ), CommonProblemModel( title: '硅基动感BGM血糖监测操作使用视频', color: Color(0xFFDB200B), ), CommonProblemModel( title: 'BGM绑定连接APP操作', color: Color(0xFFE03E09), ), CommonProblemModel( title: '手表基础操作', color: Color(0xFFE65C07), ), CommonProblemModel( title: '手表基础操作', color: Color(0xFFEC7904), ), CommonProblemModel( title: '手表基础操作', color: Color(0xFFF19702), ), CommonProblemModel( title: '手表基础操作', color: Color(0xFFF7B500), ), ].obs;}六、个人理解:1.Flutter状态管理使用Get框架,ViewModel 继承GetxController2.页面Page创建ViewModellate final HelpCentreViewModel _viewModel;@overridevoid initState() { super.initState(); _viewModel = Get.put(HelpCentreViewModel());}3.子组件ViewModel传值SearchCommonProblem(viewModel: _viewModel),4.子组件ViewModel接收 ​5.Flutter UI状态监听 使用ViewModel的数据组件,使用 Obx( ( ) { return 组件} 做状态监听 七,作为一个Flutter初学者,希望大佬们多多提宝贵意见,大家一起学习进度​
  • [技术交流] 开发者技术支持-Flutter通用Web页面
    1.问题说明:Flutter通用Web页面需求2.原因分析:iOS、安卓、鸿蒙,三端APP通用Web网页的加载和使用 3.解决思路:Flutter三方框架:webview_flutter: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/webview_flutter/webview_flutter"搭建UI:WebViewWidget 组件的使用4.解决方案:一、Flutter的三方框架配置在pubspec.yaml文件中version: 1.0.0+1environment: sdk: '>=3.4.0 <4.0.0'# Dependencies specify other packages that your package needs in order to work.# To automatically upgrade your package dependencies to the latest versions# consider running `flutter pub upgrade --major-versions`. Alternatively,# dependencies can be manually updated by changing the version numbers below to# the latest version available on pub.dev. To see which dependencies have newer# versions available, run `flutter pub outdated`.dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 # getx 框架 get: ^4.7.2 webview_flutter: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/webview_flutter/webview_flutter" 二、UI搭建:Web页面import 'package:flutter/material.dart';import 'package:webview_flutter/webview_flutter.dart';import 'package:get/get.dart';import '../models/general_web_model.dart';import '../viewmodels/general_web_viewmodel.dart';class GeneralWebPage extends StatefulWidget { const GeneralWebPage({super.key}); @override State<GeneralWebPage> createState() => _GeneralWebPageState();}class _GeneralWebPageState extends State<GeneralWebPage> { late final GeneralWebViewModel _viewModel; late final WebViewController _controller; // 目标页面接收对象 final GeneralWebModel _webModel = Get.arguments as GeneralWebModel; // 基础网页域名 final String _baseUrl = 'https://www.baidu.com/'; // 具体网页地址 late final String _webUrl = _baseUrl + _webModel.url; // 需要注册的JS方法集合 late final List<String> _javaScripts = []; // 注册JS方法 addJavaScriptChannel() { for (String _javaScript in _javaScripts) { _controller.addJavaScriptChannel(_javaScript, onMessageReceived: (JavaScriptMessage message) { print('从JavaScript接收到消息: ${message.message}'); // 处理接收到的消息 }); } } // 样例调用H5的JS方法 runTestJavaScript() { // 调用页面中的JavaScript函数 _controller.runJavaScript('showMessage("Hello from Flutter!")'); } @override void initState() { super.initState(); _viewModel = Get.put(GeneralWebViewModel()); _controller = WebViewController(); _controller.setJavaScriptMode(JavaScriptMode.unrestricted); // 注册JS方法 addJavaScriptChannel(); _controller.setBackgroundColor(Colors.white); _controller.setNavigationDelegate( NavigationDelegate( onNavigationRequest: (NavigationRequest request) { if (request.url.startsWith(_baseUrl)) { // 允许跳转到指定域名的页面 return NavigationDecision.navigate; } // 阻止跳转到其他域名的页面 return NavigationDecision.prevent; }, onPageStarted: (String url) { print('------onPageStarted------'); _viewModel.isLoading.value = true; }, onPageFinished: (String url) { print('------onPageFinished------'); _viewModel.isLoading.value = false; }, onProgress: (int progress) { print('------onProgress------'); _viewModel.progress.value = progress; }, onWebResourceError: (WebResourceError error) { print('------onWebResourceError------'); }, onUrlChange: (UrlChange change) { print('------onUrlChange------'); }, onHttpAuthRequest: (HttpAuthRequest request) { print('------onHttpAuthRequest------'); }, ), ); _controller.loadRequest(Uri.parse(_webUrl)); } @override Widget build(BuildContext context) { // 获取状态栏高度 final statusBarHeight = MediaQuery.of(context).padding.top; return Scaffold( appBar: _webModel.isAppBar ? AppBar( title: Text( _webModel.title, style: TextStyle(fontSize: 18), ), centerTitle: true, leading: IconButton( icon: Icon( Icons.arrow_back_ios, color: Colors.grey.shade500, ), onPressed: () => Get.back(), ), ) : null, body: Container( decoration: BoxDecoration(color: Colors.white), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!_webModel.isAppBar) // 状态栏占位 SizedBox(height: statusBarHeight), Expanded( child: Stack( alignment: AlignmentDirectional.topCenter, children: [ WebViewWidget(controller: _controller), Obx(() { return Visibility( visible: _viewModel.isLoading.value, child: LinearProgressIndicator( value: _viewModel.progress / 100, backgroundColor: Colors.white, color: Colors.white, valueColor: AlwaysStoppedAnimation(Colors.green), minHeight: 2, )); }), ], )), ], ), ), ); }}三、通用Web页面传值的Modelclass GeneralWebModel { String url = ''; // 网页相对路径(域名之后的路径) bool isAppBar = true; // 是否展示AppBar String title = ''; // 标题 GeneralWebModel({ required this.url, this.isAppBar = true, this.title = '', });}四、通用Web页面的ViewModelimport 'package:get/get.dart';import 'package:get/get_rx/get_rx.dart';import 'package:get/get_rx/src/rx_types/rx_types.dart';class GeneralWebViewModel extends GetxController { RxInt progress = 0.obs; RxBool isLoading = true.obs;}五、全局使用GetX 状态管理框架# getx 框架get: ^4.7.2六、个人感悟:目前自己只是一个Flutter初级开发人员,后续会持续跟进Flutter技术的更新,希望大佬们多多提宝贵建议,大家一起进度
  • [分享交流] Flutter未来的技术发展方向有哪些呢?
    Flutter未来的技术发展方向有哪些呢?
  • [技术干货] Flutter 小技巧之有趣的动画技巧[转载]
    动画效果事实上 Flutter 里实现类似的动画效果很简单,甚至不需要自定义布局,只需要通过官方的内置控件就可以轻松实现。首先我们需要使用 AnimatedPositioned 和 AnimatedContainer :AnimatedPositioned 用于在 Stack 里实现位移动画效果AnimatedContainer 用于实现大小变化的动画效果接着我们定义一个 PositionItem ,将 AnimatedPositioned 和 AnimatedContainer 嵌套在一起,并且通过 PositionedItemData 用于改变它们的位置和大小。class PositionItem extends StatelessWidget {  final PositionedItemData data;  final Widget child;  const PositionItem(this.data, {required this.child});  @override  Widget build(BuildContext context) {    return new AnimatedPositioned(      duration: Duration(seconds: 1),      curve: Curves.fastOutSlowIn,      child: new AnimatedContainer(        duration: Duration(seconds: 1),        curve: Curves.fastOutSlowIn,        width: data.width,        height: data.height,        child: child,      ),      left: data.left,      top: data.top,    );  }}class PositionedItemData {  final double left;  final double top;  final double width;  final double height;  PositionedItemData({    required this.left,    required this.top,    required this.width,    required this.height,  });}之后我们只需要把 PositionItem 放到通过 Stack 下,然后通过 LayoutBuilder 获得 parent 的大小,根据 PositionedItemData 调整 PositionItem 的位置和大小,就可以轻松实现开始的动画效果。child: LayoutBuilder(  builder: (_, con) {    var f = getIndexPosition(currentIndex % 3, con.biggest);    var s = getIndexPosition((currentIndex + 1) % 3, con.biggest);    var t = getIndexPosition((currentIndex + 2) % 3, con.biggest);    return Stack(      fit: StackFit.expand,      children: [        PositionItem(f,            child: InkWell(              onTap: () {                print("red");              },              child: Container(color: Colors.redAccent),            )),        PositionItem(s,            child: InkWell(              onTap: () {                print("green");              },              child: Container(color: Colors.greenAccent),            )),        PositionItem(t,            child: InkWell(              onTap: () {                print("yello");              },              child: Container(color: Colors.yellowAccent),            )),      ],    );  },),如下图所示,只需要每次切换对应的 index ,便可以调整对应 Item 的大小和位置发生变化,从而触发 AnimatedPositioned 和 AnimatedContainer 产生动画效果,达到类似开始时动图的动画效果。计算大小    效果    完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/anim_switch_layout_demo_page.dart如果你对于实现原理没兴趣,那到这里就可以结束了,通过上面你已经知道了一个小技巧:改变 AnimatedPositioned 和 AnimatedContainer 的任意参数,就可以让它们产生动画效果,而它们的参数和 Positioned 与 Container 一模一样,所以使用起来可以无缝替换 Positioned 与 Container ,只需要简单配置额外的 duration 等参数。进阶学习那 AnimatedPositioned 和 AnimatedContainer 是如何实现动画效果 ?这里就要介绍一个抽象父类 ImplicitlyAnimatedWidget 。几乎所有 Animated 开头的控件都是继承于它,既然是用于动画 ,那么 ImplicitlyAnimatedWidget 就肯定是一个 StatefulWidget ,那么不出意外,它的实现逻辑主要在于 ImplicitlyAnimatedWidgetState ,而我们后续也会通过它来展开。首先我们回顾一下,一般在 Flutter 使用动画需要什么:AnimationController : 用于控制动画启动、暂停TickerProvider : 用于创建 AnimationController 所需的 vsync 参数,一般最常使用 SingleTickerProviderStateMixinAnimation : 用于处理动画的 value ,例如常见的 CurvedAnimation接收动画的对象:例如 FadeTransition简单来说,Flutter 里的动画是从 Ticker 开始,当我们在 State 里 with TickerProviderStateMixin 之后,就代表了具备执行动画的能力:每次 Flutter 在绘制帧的时候,Ticker 就会同步到执行 AnimationController 里的 _tick 方法,然后执行 notifyListeners ,改变 Animation 的 value,从而触发 State 的 setState 或者 RenderObject 的 markNeedsPaint 更新界面。举个例子,如下代码所示,可以看到实现一个简单动画效果所需的代码并不少,而且这部分代码重复度很高,所以针对这部分逻辑,官方提供了 ImplicitlyAnimatedWidget 模版。class _AnimatedOpacityState extends State<AnimatedOpacity>    with TickerProviderStateMixin {  late final AnimationController _controller = AnimationController(    duration: const Duration(seconds: 2),    vsync: this,  )..repeat(reverse: true);  late final Animation<double> _animation = CurvedAnimation(    parent: _controller,    curve: Curves.easeIn,  );  @override  void dispose() {    _controller.dispose();    super.dispose();  }  @override  Widget build(BuildContext context) {    return Container(      color: Colors.white,      child: FadeTransition(        opacity: _animation,        child: const Padding(padding: EdgeInsets.all(8), child: FlutterLogo()),      ),    );  }}例如上面的 Fade 动画,换成 ImplicitlyAnimatedWidgetState 只需要实现 forEachTween 方法和 didUpdateTweens 方法即可,而不再需要关心 AnimationController 和 CurvedAnimation 等相关内容。class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState<AnimatedOpacity> {  Tween<double>? _opacity;  late Animation<double> _opacityAnimation;  @override  void forEachTween(TweenVisitor<dynamic> visitor) {    _opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;  }  @override  void didUpdateTweens() {    _opacityAnimation = animation.drive(_opacity!);  }  @override  Widget build(BuildContext context) {    return FadeTransition(      opacity: _opacityAnimation,      alwaysIncludeSemantics: widget.alwaysIncludeSemantics,      child: widget.child,    );  }}那 ImplicitlyAnimatedWidgetState 是如何做到改变 opacity 就触发动画?关键还是在于实现的 forEachTween :当 opacity 被更新时,forEachTween 会被调用,这时候内部会通过 _shouldAnimateTween 判断值是否更改,如果目标值已更改,就执行基类里的 AnimationController.forward 开始动画。这里补充一个内容:FadeTransition 内部会对 _opacityAnimation 添加兼容,当 AnimationController 开始执行动画的时候,就会触发 _opacityAnimation 的监听,从而执行 markNeedsPaint ,而如下图所示, markNeedsPaint 最终会触发 RenderObject 的重绘。所以到这里,我们知道了:通过继承 ImplicitlyAnimatedWidget 和 ImplicitlyAnimatedWidgetState 我们可以更方便实现一些动画效果,Flutter 里的很多默认动画效果都是通过它实现。另外 ImplicitlyAnimatedWidget 模版里,除了 ImplicitlyAnimatedWidgetState ,官方还提供了另外一个子类 AnimatedWidgetBaseState。事实上 Flutter 里我们常用的 Animated 都是通过 ImplicitlyAnimatedWidget 模版实现,如下图所示是 Flutter 里常见的 Animated 分别继承的 State :ImplicitlyAnimatedWidgetState    AnimatedWidgetBaseState    关于这两个 State 的区别,简单来说可以理解为:ImplicitlyAnimatedWidgetState 里主要是配合各类 *Transition 控件使用,比如: AnimatedOpacity里使用了 FadeTransition 、AnimatedScale 里使用了 ScaleTransition ,因为 ImplicitlyAnimatedWidgetState 里没有使用 setState,而是通过触发 RenderObject 的 markNeedsPaint 更新界面。AnimatedWidgetBaseState 在原本 ImplicitlyAnimatedWidgetState 的基础上增加了自动 setState 的监听,所以可以做一些更灵活的动画,比如前面我们用过的 AnimatedPositioned 和 AnimatedContainer 。其实 AnimatedContainer 本身就是一个很具备代表性的实现,如果你去看它的源码,就可以看到它的实现很简单,只需要在 forEachTween 里实现参数对应的 Tween 实现即可。例如前面我们改变的 width 和 height ,其实就是改变了Container 的 BoxConstraints ,所以对应的实现也就是 BoxConstraintsTween ,而 BoxConstraintsTween 继承了 Tween ,主要是实现了 Tween 的 lerp 方法。在 Flutter 里 lerp 方法是用于实现插值:例如就是在动画过程中,在 beigin 和 end 两个 BoxConstraint 之间进行线性插值,其中 t 是动画时钟值下的变化值,例如:计算出 100x100 到 200x200 大小的过程中需要的一些中间过程的尺寸。如下代码所示,通过继承 AnimatedWidgetBaseState ,然后利用 ColorTween 的 lerp ,就可以很快实现如下文字的渐变效果。代码    效果    总结最后总结一下,本篇主要介绍了:利用 AnimatedPositioned 和 AnimatedContainer 快速实现切换动画效果介绍 ImplicitlyAnimatedWidget 和如何使用 ImplicitlyAnimatedWidgetState / AnimatedWidgetBaseState 简化实现动画的需求,并且快速实现自定义动画。那么,你还有知道什么使用 Flutter 动画的小技巧吗?原文链接:https://blog.csdn.net/ZuoYueLiang/article/details/125365460
  • [技术干货] 10天学会flutter DAY10 flutter 玩转 动画与打包[转载]
    动画​ Flutter中的动画系统基于Animation对象的,和之前的手势不同,它不是一个Widget,这是因为Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,就相当于一个定时器,它用于保存动画的插值和状态,并执行数值的变化。widget可以在build函数中读取Animation对象的当前值, 并且可以监听动画的状态改变。AnimationController​ AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。AnimationController controller = AnimationController(  duration: const Duration(milliseconds: 2000), //动画时间 lowerBound: 10.0,    //生成数字的区间  upperBound: 20.0,    //10.0 - 20.0 vsync: this  //TickerProvider 动画驱动器提供者);Ticker​ Ticker的作用是添加屏幕刷新回调,每次屏幕刷新都会调用TickerCallback。使用Ticker来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源。因为Flutter中屏幕刷新时会通知Ticker,锁屏后屏幕会停止刷新,所以Ticker就不会再触发。最简单的做法为将SingleTickerProviderStateMixin添加到State的定义中。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: "animation",      home: Scaffold(        appBar: AppBar(          title: Text('animation'),        ),        body: AnimWidget(),      ),    );  }}// 动画是有状态的class AnimWidget extends StatefulWidget {  @override  State<StatefulWidget> createState() {    return _AnimWidgetState();  }}class _AnimWidgetState extends State<AnimWidget>    with SingleTickerProviderStateMixin {  AnimationController controller;  bool forward = true;  @override  void initState() {    super.initState();    controller = AnimationController(      // 动画的时长      duration: Duration(milliseconds: 2000),      lowerBound: 10.0,      upperBound: 100.0,      // 提供 vsync 最简单的方式,就是直接混入 SingleTickerProviderStateMixin      // 如果有多个AnimationController,则使用TickerProviderStateMixin。      vsync: this,    );       //状态修改监听    controller      ..addStatusListener((AnimationStatus status) {        debugPrint("状态:$status");      })      ..addListener(() {        setState(() => {});      });    debugPrint("controller.value:${controller.value}");  }  @override  Widget build(BuildContext context) {    return Column(      children: <Widget>[        Container(          width: controller.value,          height: controller.value,          color: Colors.blue,        ),        RaisedButton(          child: Text("播放"),          onPressed: () {            if (forward) {              controller.forward();            } else {              controller.reverse();            }            forward = !forward;          },        ),        RaisedButton(          child: Text("停止"),          onPressed: () {            controller.stop();          },        )      ],    );  }}动画状态监听:在forword结束之后状态为completed。在reverse结束之后状态为dismissedTween​ 默认情况下,AnimationController对象值为:double类型,范围是0.0到1.0 。如果我们需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。要使用Tween对象,需要调用其animate()方法,然后传入一个控制器对象,同时动画过程中产生的数值由Tween的lerp方法决定。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: "animation",      home: Scaffold(        appBar: AppBar(          title: Text('animation'),        ),        body: AnimWidget(),      ),    );  }}// 动画是有状态的class AnimWidget extends StatefulWidget {  @override  State<StatefulWidget> createState() {    return _AnimWidgetState();  }}class _AnimWidgetState extends State<AnimWidget>    with SingleTickerProviderStateMixin {  AnimationController controller;  bool forward = true;  Tween<Color> tween;  @override  void initState() {    super.initState();    controller = AnimationController(      // 动画的时长      duration: Duration(milliseconds: 2000),      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin      vsync: this,    );    //使用Color    tween = ColorTween(begin: Colors.blue, end: Colors.yellow);    //添加动画值修改监听    tween.animate(controller)..addListener(() => setState(() {}));  }  @override  Widget build(BuildContext context) {    return Column(      children: <Widget>[        Container(          width: 100,          height: 100,          //获取动画当前值          color: tween.evaluate(controller),        ),        RaisedButton(          child: Text("播放"),          onPressed: () {            if (forward) {              controller.forward();            } else {              controller.reverse();            }            forward = !forward;          },        ),        RaisedButton(          child: Text("停止"),          onPressed: () {            controller.stop();          },        )      ],    );  }}Curve​ 动画过程默认是线性的(匀速),如果需要非线形的,比如:加速的或者先加速后减速等。Flutter中可以通过Curve(曲线)来描述动画过程。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: "animation",      home: Scaffold(        appBar: AppBar(          title: Text('animation'),        ),        body: AnimWidget(),      ),    );  }}// 动画是有状态的class AnimWidget extends StatefulWidget {  @override  State<StatefulWidget> createState() {    return _AnimWidgetState();  }}class _AnimWidgetState extends State<AnimWidget>    with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;  bool forward = true;  @override  void initState() {    super.initState();    controller = AnimationController(      // 动画的时长      duration: Duration(milliseconds: 2000),      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin      vsync: this,    );    //弹性    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);    //使用Color    animation = Tween(begin: 10.0, end: 100.0).animate(animation)      ..addListener(() {        setState(() => {});      });  }  @override  Widget build(BuildContext context) {    return Column(      children: <Widget>[        Container(          //不需要转换          width: animation.value,          height: animation.value,          //获取动画当前值          color: Colors.blue,        ),        RaisedButton(          child: Text("播放"),          onPressed: () {            if (forward) {              controller.forward();            } else {              controller.reverse();            }            forward = !forward;          },        ),        RaisedButton(          child: Text("停止"),          onPressed: () {            controller.stop();          },        )      ],    );  }}AnimatedWidget​ 通过上面的学习我们能够感受到Animation对象本身和UI渲染没有任何关系。而通过addListener()和setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,简单来说就是自动调用setState()。​ Flutter中已经封装了很多动画,比如对widget进行缩放,可以直接使用ScaleTransitionimport 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return MaterialApp(      title: "animation",      home: Scaffold(        appBar: AppBar(          title: Text('animation'),        ),        body: AnimWidget(),      ),    );  }}// 动画是有状态的class AnimWidget extends StatefulWidget {  @override  State<StatefulWidget> createState() {    return _AnimWidgetState();  }}class _AnimWidgetState extends State<AnimWidget>    with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;  bool forward = true;  @override  void initState() {    super.initState();    controller = AnimationController(      // 动画的时长      duration: Duration(milliseconds: 2000),      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin      vsync: this,    );    //弹性    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);    //使用Color    animation = Tween(begin: 10.0, end: 100.0).animate(animation);  }  @override  Widget build(BuildContext context) {    return Column(      children: <Widget>[        ScaleTransition(          child:  Container(            width: 100,            height: 100,            color: Colors.blue,          ),          scale: controller,        ),        RaisedButton(          child: Text("播放"),          onPressed: () {            if (forward) {              controller.forward();            } else {              controller.reverse();            }            forward = !forward;          },        ),        RaisedButton(          child: Text("停止"),          onPressed: () {            controller.stop();          },        )      ],    );  }}Hero动画​ Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return new MaterialApp(      title: 'Flutter Demo',      home: Scaffold(          appBar: AppBar(            title: Text("主页"),          ),          body: Route1()),    );  }}// 路由Aclass Route1 extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      alignment: Alignment.topCenter,      child: InkWell(        child: Hero(          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同          child: CircleAvatar(            backgroundImage: AssetImage(              "assets/banner.jpeg",            ),          ),        ),        onTap: () {          Navigator.push(context, MaterialPageRoute(builder: (_) {            return Route2();          }));        },      ),    );  }}class Route2 extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Center(      child: Hero(          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同          child: Image.asset("assets/banner.jpeg")),    );  }}组合动画有些时候我们可能会需要执行一个动画序列执行一些复杂的动画。import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    return new MaterialApp(      title: 'Flutter Demo',      home: Route(),    );  }}class Route extends StatefulWidget {  @override  State<StatefulWidget> createState() {    return RouteState();  }}class RouteState extends State<Route> with SingleTickerProviderStateMixin {  Animation<Color> color;  Animation<double> width;  AnimationController controller;  @override  void initState() {    super.initState();    controller = AnimationController(      // 动画的时长      duration: Duration(milliseconds: 2000),      // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin      vsync: this,    );    //高度动画    width = Tween<double>(      begin: 100.0,      end: 300.0,    ).animate(      CurvedAnimation(        parent: controller,        curve: Interval(          //间隔,前60%的动画时间 1200ms执行高度变化          0.0, 0.6,        ),      ),    );    color = ColorTween(      begin: Colors.green,      end: Colors.red,    ).animate(      CurvedAnimation(        parent: controller,        curve: Interval(          0.6, 1.0, //高度变化完成后 800ms 执行颜色编码        ),      ),    );  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("主页"),      ),      body: InkWell(        ///1、不用显式的去添加帧监听器,再调用setState()        ///2、缩小动画构建的范围,如果没有builder,setState()将会在父widget上下文调用,导致父widget的build方法重新调用,现在只会导致动画widget的build重新调用        child: AnimatedBuilder(            animation: controller,            builder: (context, child) {              return Container(                color: color.value,                width: width.value,                height: 100.0,              );            }),        onTap: () {          controller.forward().whenCompleteOrCancel(() => controller.reverse());        },      ),    );  }}打包​ Flutter在打Release包时候回使用AOT,因此在对一个Flutter测试时候务必使用Release来进行测试。打包命令:flutter build apk 。当然我们需要打包时,还需要配置一些比如签名的内容。配置这些内容和普通Android工程没有区别,都是在build.gradle中进行,只是Flutter工程AS没有提供GUI。​ 在Flutter工程的android/app下面的build.gradle可以修改包名、版本等信息,这就不用多说了。获得签名文件之后,将它复制到flutter的android目录:​ 然后在app的build.gradle中配置:signingConfigs {        release {            keyAlias 'enjoy'            keyPassword '123456'            // 因为是放到父级的根目录,使用rootProject            // 如果放在这个build.gradle的同级,直接使用file            storeFile rootProject.file('enjoy.jks')            storePassword '123456'        }    }    buildTypes {        release {            // TODO: Add your own signing config for the release build.            // Signing with the debug keys for now, so `flutter run --release` works.            signingConfig signingConfigs.release        }    }饼图https://github.com/google/chartsStack布局中的fit属性与Image的fit类似,表示内容的扩充情况。默认为StackFit.loose表示Stack与内容一样大。如果设置为StackFit.passthrough则表示Stack父Widget的约束会传给Stack内部非Positioned的子Widget。效果如代码中的StackFit.dart原文链接:https://blog.csdn.net/u010755471/article/details/124691809
  • [问题求助] flutter上传文件到obs
    flutter如何将文件上传到obs里,使用哪个插件啊,调用那些类啊,求解
  • [技术干货] flutter 路由实践(下)
    其实现也比较清晰,对每一个 _RouteEntry,通过调用 didChangeNext 和 didChangePrevious 来建立联系,比如在 didChangeNext 中绑定当前 Route 的 secondaryAnimation 和下一个路由的 animation 进行动画联动,再比如在 didChangePrevious 中获取上一个路由的 title,这个可以用于 CupertinoNavigationBar 中 back 按钮展示上一页面的 title。然后调用 maybeNotifyRouteChange 发出通知,指定当前正在处于展示状态的 Route。最后,遍历 toBeDisposed 执行 _RouteEntry 的销毁,这个列表会保存上面循环处理过程中,确定需要移出的 _RouteEntry,通过调用 OverlayEntry remove 函数(它会将自己从 Overlay 中移除)和 OverlayEntry dispose 函数(它会调用 Route 的 dispose,进行资源释放,比如 TransitionRoute 中 AnimationController 销毁)。最后再看关于状态的处理,以下是所有的状态:enum _RouteLifecycle {  staging, // we will wait for transition delegate to decide what to do with this route.  //  // routes that are present:  //  add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages  adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages  // routes that are ready for transition.  push, // we'll want to run install, didPush, etc; a route added via push() and friends  pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends  pushing, // we're waiting for the future from didPush to complete  replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends  idle, // route is being harmless  //  // routes that are not present:  //  // routes that should be included in route announcement and should still listen to transition changes.  pop, // we'll want to call didPop  remove, // we'll want to run didReplace/didRemove etc  // routes should not be included in route announcement but should still listen to transition changes.  popping, // we're waiting for the route to call finalizeRoute to switch to dispose  removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose  // routes that are completely removed from the navigator and overlay.  dispose, // we will dispose the route momentarily  disposed, // we have disposed the route}本质上这些状态分为三类,add(处理初始化的时候直接添加),push(与 add 类似,但是增加了动画的处理),pop(处理页面移出),remove(移出某个页面,相对 pop 没有动画,也没有位置限制)。addadd 方式添加路由目前还只用于在应用初始化是添加初始化页面使用,对应的是在 NavigatorState 的 initState 中,void initState() {  super.initState();  for (final NavigatorObserver observer in widget.observers) {    assert(observer.navigator == null);    observer._navigator = this;  }  String initialRoute = widget.initialRoute;  if (widget.pages.isNotEmpty) {    _history.addAll(      widget.pages.map((Page<dynamic> page) => _RouteEntry(        page.createRoute(context),        initialState: _RouteLifecycle.add,      ))    );  } else {    // If there is no page provided, we will need to provide default route    // to initialize the navigator.    initialRoute = initialRoute ?? Navigator.defaultRouteName;  }  if (initialRoute != null) {    _history.addAll(      widget.onGenerateInitialRoutes(        this,        widget.initialRoute ?? Navigator.defaultRouteName      ).map((Route<dynamic> route) =>        _RouteEntry(          route,          initialState: _RouteLifecycle.add,        ),      ),    );  }  _flushHistoryUpdates();}它会将从 onGenerateInitialRoutes 得来的所有初始路由转成 _RouteEntry 加入到 _history,此时它们的状态是 _RouteLifecycle.add,然后就是调用 _flushHistoryUpdates 进行处理。void _flushHistoryUpdates({bool rearrangeOverlay = true}) {  // ...  while (index >= 0) {    switch (entry.currentState) {      case _RouteLifecycle.add:        assert(rearrangeOverlay);        entry.handleAdd(          navigator: this,        );        assert(entry.currentState == _RouteLifecycle.adding);        continue;      case _RouteLifecycle.adding:        if (canRemoveOrAdd || next == null) {          entry.didAdd(            navigator: this,            previous: previous?.route,            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,            isNewFirst: next == null          );          assert(entry.currentState == _RouteLifecycle.idle);          continue;        }        break;      case _RouteLifecycle.idle:        if (!seenTopActiveRoute && poppedRoute != null)          entry.handleDidPopNext(poppedRoute);        seenTopActiveRoute = true;        // This route is idle, so we are allowed to remove subsequent (earlier)        // routes that are waiting to be removed silently:        canRemoveOrAdd = true;        break;        // ...    }    index -= 1;    next = entry;    entry = previous;    previous = index > 0 ? _history[index - 1] : null;  }  // ...}add 路线主要会调用两个函数,handleAdd 和 didAdd,void handleAdd({ @required NavigatorState navigator}) {  assert(currentState == _RouteLifecycle.add);  assert(navigator != null);  assert(navigator._debugLocked);  assert(route._navigator == null);  route._navigator = navigator;  route.install();  assert(route.overlayEntries.isNotEmpty);  currentState = _RouteLifecycle.adding;}install 函数可以看作是 Route 的初始化函数,比如在 ModalRoute 中创建 ProxyAnimation 来管理一些动画的执行,在 TransitionRoute 中创建了用于执行切换动画的 AnimationController,在 OverlayRoute 中完成了当前 Route 的 OverlayEntry 的创建及插入。createOverlayEntries 用于创建 OverlayEntry,其实现在 ModalRoute,Iterable<OverlayEntry> createOverlayEntries() sync* {  yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);  yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);}每一个 Route 都能生成两个 OverlayEntry,一个是 _buildModalBarrier,它可以生成两个页面之间的屏障,我们可以利用它给新页面设置一个背景色,同时还支持动画过渡,另一个是 _buildModalScope,它生成的就是这个页面真正的内容,外部会有多层包装,最底层就是 WidgetBuilder 创建的 widget。大致看下两个函数的实现,Widget _buildModalBarrier(BuildContext context) {  Widget barrier;  if (barrierColor != null && !offstage) { // changedInternalState is called if these update    assert(barrierColor != _kTransparent);    final Animation<Color> color = animation.drive(      ColorTween(        begin: _kTransparent,        end: barrierColor, // changedInternalState is called if this updates      ).chain(_easeCurveTween),    );    barrier = AnimatedModalBarrier(      color: color,      dismissible: barrierDismissible, // changedInternalState is called if this updates      semanticsLabel: barrierLabel, // changedInternalState is called if this updates      barrierSemanticsDismissible: semanticsDismissible,    );  } else {    barrier = ModalBarrier(      dismissible: barrierDismissible, // changedInternalState is called if this updates      semanticsLabel: barrierLabel, // changedInternalState is called if this updates      barrierSemanticsDismissible: semanticsDismissible,    );  }  return IgnorePointer(    ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates              animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture    child: barrier,  );}ModalBarrier 是两个 Route 之间的屏障,它可以通过颜色、拦截事件来表示两个 Route 的隔离,这些都是可以配置的,这里 IgnorePointer 的作用是为了在执行切换动画的时候无法响应时间。Widget _buildModalScope(BuildContext context) {  return _modalScopeCache ??= _ModalScope<T>(    key: _scopeKey,    route: this,    // _ModalScope calls buildTransitions() and buildChild(), defined above  );} Widget build(BuildContext context) {  return _ModalScopeStatus(    route: widget.route,    isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates    canPop: widget.route.canPop, // _routeSetState is called if this updates    child: Offstage(      offstage: widget.route.offstage, // _routeSetState is called if this updates      child: PageStorage(        bucket: widget.route._storageBucket, // immutable        child: FocusScope(          node: focusScopeNode, // immutable          child: RepaintBoundary(            child: AnimatedBuilder(              animation: _listenable, // immutable              builder: (BuildContext context, Widget child) {                return widget.route.buildTransitions(                  context,                  widget.route.animation,                  widget.route.secondaryAnimation,                  IgnorePointer(                    ignoring: widget.route.animation?.status == AnimationStatus.reverse,                    child: child,                  ),                );              },              child: _page ??= RepaintBoundary(                key: widget.route._subtreeKey, // immutable                child: Builder(                  builder: (BuildContext context) {                    return widget.route.buildPage(                      context,                      widget.route.animation,                      widget.route.secondaryAnimation,                    );                  },                ),              ),            ),          ),        ),      ),    ),  );}_ModalScope 需要承载用户界面的展示,它的 build 函数可以看到在 widget.route.buildPage 出用户定义的页面之上有很多层,可以一层一层看下大致作用:_ModalScopeStatus,继承自 InheritedWidget,用于给底层结点提供数据Offstage,可以通过 offstage 变量控制是否绘制PageStorage,它提供了一种存储策略,也就是 PageStorageBucket,这个类可以给某一个 BuildContext 绑定特定的数据,支持写入和读取,可用于某一个 widget 的状态存储等FocusScope,用于焦点管理用,一般只有获取焦点的控件才能接收到按键信息等RepaintBoundary,控制重绘范围,意在减少不必要的重绘AnimatedBuilder,动画控制 Widget,会根据 animation 进行 rebuildwidget.route.buildTransitions,它在不同的 Route 中可以有不同的实现,比如 Android 的默认实现是自下向上渐入,ios 的默认实现是自右向左滑动,另外也可以通过自定义 Route 或自定义 ThemeData 实现自定义的切换动画,还有一点需要说明,Route 中的动画分为 animation 和 secondaryAnimation,其中 animation 定义了自己 push 时的动画,secondaryAnimation 定义的是新页面 push 时自己的动画,举个例子,在 ios 风格中,新页面自右向左滑动,上一个页面也会滑动,此时控制上一个页面滑动的动画就是 secondaryAnimationIgnorePointer,同样是用于页面切换动画执行中,禁止用户操作RepaintBoundary,这里的考量应该是考虑到上层有一个动画执行,所以这里包一下避免固定内容重绘Builder,Builder 的唯一作用应该是提供 BuildContext,虽然说每一个 build 函数都有 BuildContext 参数,但这个是当前 Widget 的,而不是直属上级的,这可能有点抽象,比如说下面的 buildPage 需要使用 BuildContext 作为参数,那么如果它需要使用 context 的 ancestorStateOfType 的话,实际上就是从 _ModalScopeState 开始向上查找,而不是从 Builder 开始向上查找widget.route.buildPage,这个函数内部就是使用 Route 的 WidgetBuilder 创建用户界面,当然不同的 Route 可能还会在这里再次进行包装以上就是一个页面中,从 Overlay(说是 Overlay 不是那么合理,但是在此先省略中间的 _Theatre 等) 往下的布局嵌套。新的 OverlayEntry 创建完成之后,会把它们都传递到 Overlay 中,且在这个过程中会调用 Overlay 的 setState 函数,请求重新绘制,在 Overlay 中实现新旧页面的切换。以上是 install 的整个过程,执行完了之后把 currentState 置为 adding 返回。
  • [技术干货] flutter 路由器实践 (上)
    flutter 应用的运行需要依赖 MaterialApp/CupertinoApp 这两个 Widget,他们分别对应着 android/ios 的设计风格,同时也为应用的运行提供了一些基本的设施,比如与路由相关的主页面、路由表等,再比如跟整体页面展示相关的 theme、locale 等。其中与路由相关的几项配置有 home、routes、initialRoute、onGenerateRoute、onUnknownRoute,它们分别对应着主页面 widget、路由表(根据路由找到对应 widget)、首次加载时的路由、路由生成器、未知路由代理(比如常见的 404 页面)。MaterialApp/CupertinoApp 的子结点都是 WidgetsApp,只不过他们给 WidgetsApp 传入了不同的参数,从而使得两种 Widget 的界面风格不一致。Navigator 就是在 WidgetsApp 中创建的,Widget build(BuildContext context) {  Widget navigator;    if (_navigator != null) {    navigator = Navigator(      key: _navigator,      // If window.defaultRouteName isn't '/', we should assume it was set      // intentionally via `setInitialRoute`, and should override whatever      // is in [widget.initialRoute].      initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName          ? WidgetsBinding.instance.window.defaultRouteName          : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,      onGenerateRoute: _onGenerateRoute,      onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null        ? Navigator.defaultGenerateInitialRoutes        : (NavigatorState navigator, String initialRouteName) {          return widget.onGenerateInitialRoutes(initialRouteName);        },      onUnknownRoute: _onUnknownRoute,      observers: widget.navigatorObservers,    );  }  ...}在 WidgetsApp 的 build 中第一个创建的就是 Navigator,主要看一下它的参数,首先,_navigator 是一个 GlobalKey,使得 WidgetsApp 可以通过 key 调用 Navigator 的函数进行路由切换,也就是在 WidgetsBinding 中处理 native 的路由切换信息的时候,最终是由 WidgetsApp 完成的。另外这里的 _navigator 应该只在 WidgetsApp 中有使用,其他地方需要使用一般是直接调用 Navigator.of 获取,这个函数会沿着 element 树向上查找到 NavigatorState,所以在应用中切换路由是需要被 Navigator 包裹的,不过由于 WidgetsApp 中都有生成 Navigator,开发中也不必考虑这些。另外,就是关于底层获取上层 NavigatorElement 实例的方式,在 Element 树中有两种方式可以从底层获取到上层的实例,一种方式是使用 InheritedWidget,另一种就是直接沿着树向上查找(ancestorXXXOfExactType 系列),两种方式的原理基本是一致的,只不过 InheritedWidget 在建立树的过程中会一层层向下传递,而后者是使用的时候才向上查找,所以从这个角度来说使用 InheritedWidget 会高效些,但是 InheritedWidget 的优势不止如此,它是能够在数据发生改变的时候通知所有依赖它的结点进行更新,这也是 ancestorXXXOfExactType 系列所没有的。observers 是路由切换的监听列表,可以由外部传入,在路由切换的时候做些操作,比如 HeroController 就是一个监听者。Navigator 是一个 StatefulWidget,在 NavigatorState 的 initState 中完成了将 initRoute 转换成 Route 的过程,并调用 push 将其入栈,生成 OverlayEntry,这个会继续传递给下层负责显示页面的 Overlay 负责展示。在 push 的过程中,route 会被转换成 OverlayEntry 列表存放,每一个 OverlayEntry 中存储一个 WidgetBuilder,从某种角度来说,OverlayEntry 可以被认为是一个页面。所有的页面的协调、展示是通过 Overlay 完成的,Overlay 是一个类似于 Stack 的结构,它可以展示多个子结点。在它的 initState 中,void initState() {  super.initState();  insertAll(widget.initialEntries);}会将 initialEntries 都存到 _entries 中。Overlay 作为一个能够根据路由确定展示页面的控件,它的实现其实比较简单:Widget build(BuildContext context) {  // These lists are filled backwards. For the offstage children that  // does not matter since they aren't rendered, but for the onstage  // children we reverse the list below before adding it to the tree.  final List<Widget> onstageChildren = <Widget>[];  final List<Widget> offstageChildren = <Widget>[];  bool onstage = true;  for (int i = _entries.length - 1; i >= 0; i -= 1) {    final OverlayEntry entry = _entries[i];    if (onstage) {      onstageChildren.add(_OverlayEntry(entry));      if (entry.opaque)        onstage = false;    } else if (entry.maintainState) {      offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));    }  }  return _Theatre(    onstage: Stack(      fit: StackFit.expand,      children: onstageChildren.reversed.toList(growable: false),    ),    offstage: offstageChildren,  );}build 函数中,将所有的 OverlayEntry 分成了可见与不可见两部分,每一个 OverlayEntry 生成一个 _OverlayEntry,这是一个 StatefulWidget,它的作用主要是负责控制当前页重绘,都被封装成 然后再用  _Theatre 展示就完了,在 _Theatre 中,可见/不可见的子结点都会转成 Element,但是在绘制的时候,_Theatre 对应的 _RenderTheatre 只会把可见的子结点绘制出来。判断某一个 OverlayEntry 是否能够完全遮挡上一个 OverlayEntry 是通过它的 opaque 变量判断的,而 opaque 又是由 Route 给出的,在页面动画执行时,这个值会被设置成 false,然后在页面切换动画执行完了之后就会把 Route 的 opaque 参数赋值给它的 OverlayEntry,一般情况下,窗口对应的 Route 为 false,页面对应的 Route 为 true。 所以说在页面切换之后,上一个页面始终都是存在于 element 树中的,只不过在 RenderObject 中没有将其绘制出来,这一点在 Flutter Outline 工具里面也能够体现。从这个角度也可以理解为,在 flutter 中页面越多,需要处理的步骤就越多,虽然不需要绘制底部的页面,但是整个树的基本遍历还是会有的,这部分也算是开销。_routeNamedflutter 中进行页面管理主要的依赖路由管理系统,它的入口就是 Navigator,它所管理的东西,本质上就是承载着用户页面的 Route,但是在 Navigator 中有很多函数是 XXXName 系列的,它们传的不是 Route,而是 RouteName,据个人理解,这个主要是方便开发引入的,我们可以在 MaterialApp/CupertinoApp 中直接传入路由表,每一个名字对应一个 WidgetBuilder,然后结合 pageRouteBuilder(这个可以自定义,不过 MaterialApp/CupertinoApp 都有默认实现,能够将 WidgetBuilder 转成 Route),便可以实现从 RouteName 到 Route 的转换。Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) {  if (allowNull && widget.onGenerateRoute == null)    return null;  final RouteSettings settings = RouteSettings(    name: name,    arguments: arguments,  );  Route<T> route = widget.onGenerateRoute(settings) as Route<T>;  if (route == null && !allowNull) {    route = widget.onUnknownRoute(settings) as Route<T>;  }  return route;}这个过程分三步,生成 RouteSettings,调用 onGenerateRoute 从路由表中拿到对应的路由,如果无命中,就调用 onUnknownRoute 给一个类似于 404 页面的东西。onGenerateRoute 和 onUnknownRoute 在构建 Navigator 时传入,在 WidgetsApp 中实现,Route<dynamic> _onGenerateRoute(RouteSettings settings) {  final String name = settings.name;  final WidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null      ? (BuildContext context) => widget.home      : widget.routes[name];  if (pageContentBuilder != null) {    final Route<dynamic> route = widget.pageRouteBuilder<dynamic>(      settings,      pageContentBuilder,    );    return route;  }  if (widget.onGenerateRoute != null)    return widget.onGenerateRoute(settings);  return null;}如果是默认的路由会直接使用给定的 home 页面(如果有),否则就直接到路由表查,所以本质上这里的 home 页面更多的是一种象征,身份的象征,没有也无所谓。另外路由表主要的产出是 WidgetBuilder,它需要经过一次包装,成为 Route 才是成品,或者如果不想使用路由表这种,也可以直接实现 onGenerateRoute 函数,根据 RouteSetting 直接生成 Route,这个就不仅仅是返回 WidgetBuilder 这么简单了,需要自己包装。onUnknownRoute 主要用于兜底,提供一个类似于 404 的页面,它也是需要直接返回 Route。_flushHistoryUpdates不知道从哪一个版本开始,flutter 的路由管理引入了状态,与之前每一个 push、pop 都单独实现不同,所有的路由切换操作都是用状态表示,同时所有的 route 都被封装成 _RouteEntry,它内部有着关于 Route 操作的实现,但都被划分为比较小的单元,且都依靠状态来执行。状态是一个具有递进关系的枚举,每一个 _RouteEntry 都有一个变量存放当前的状态,在 _flushHistoryUpdates 中会遍历所有的 _RouteEntry 然后根据它们当前的状态进行处理,同时处理完成之后会切换它们的状态,再进行其他处理,这样的好处很明显,所有的路由都放在一起处理之后,整个流程会变得更加清晰,且能够很大程度上进行代码复用,比如 push 和 pushReplacement 两种操作,这在之前是需要在两个方法中单独实现的,而现在他们则可以放在一起单独处理,不同的只有后者比前者会多一个 remove 的操作。关于 _flushHistoryUpdates 的处理步骤:void _flushHistoryUpdates({bool rearrangeOverlay = true}) {  assert(_debugLocked && !_debugUpdatingPage);  // Clean up the list, sending updates to the routes that changed. Notably,  // we don't send the didChangePrevious/didChangeNext updates to those that  // did not change at this point, because we're not yet sure exactly what the  // routes will be at the end of the day (some might get disposed).  int index = _history.length - 1;  _RouteEntry next;  _RouteEntry entry = _history[index];  _RouteEntry previous = index > 0 ? _history[index - 1] : null;  bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.  Route<dynamic> poppedRoute; // The route that should trigger didPopNext on the top active route.  bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.  final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];  while (index >= 0) {    switch (entry.currentState) {        // ...    }    index -= 1;    next = entry;    entry = previous;    previous = index > 0 ? _history[index - 1] : null;  }  // Now that the list is clean, send the didChangeNext/didChangePrevious  // notifications.  _flushRouteAnnouncement();  // Announces route name changes.  final _RouteEntry lastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);  final String routeName = lastEntry?.route?.settings?.name;  if (routeName != _lastAnnouncedRouteName) {    RouteNotificationMessages.maybeNotifyRouteChange(routeName, _lastAnnouncedRouteName);    _lastAnnouncedRouteName = routeName;  }  // Lastly, removes the overlay entries of all marked entries and disposes  // them.  for (final _RouteEntry entry in toBeDisposed) {    for (final OverlayEntry overlayEntry in entry.route.overlayEntries)      overlayEntry.remove();    entry.dispose();  }  if (rearrangeOverlay)    overlay?.rearrange(_allRouteOverlayEntries);}以上是除了状态处理之外,一次 _flushHistoryUpdates 的全过程,首先它会遍历整个路由列表,根据状态做不同的处理,不过一般能够处理到的也不过最上层一两个,其余的多半是直接跳过的。处理完了之后,调用 _flushRouteAnnouncement 进行路由之间的前后链接,比如进行动画的联动等,void _flushRouteAnnouncement() {  int index = _history.length - 1;  while (index >= 0) {    final _RouteEntry entry = _history[index];    if (!entry.suitableForAnnouncement) {      index -= 1;      continue;    }    final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate);    if (next?.route != entry.lastAnnouncedNextRoute) {      if (entry.shouldAnnounceChangeToNext(next?.route)) {        entry.route.didChangeNext(next?.route);      }      entry.lastAnnouncedNextRoute = next?.route;    }    final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate);    if (previous?.route != entry.lastAnnouncedPreviousRoute) {      entry.route.didChangePrevious(previous?.route);      entry.lastAnnouncedPreviousRoute = previous?.route;    }    index -= 1;  }}
  • [技术干货] Flutter深色模式适配的实现
    一、简介Flutter的深色模式以及跟随系统设置比较简单,我感觉需要注意的是开发过程中尽量使用Theme中的颜色与样式,开发过程中遇到的比较大的坑就是provider的一些问题,可能是因为我用的版本新一些,网上找了很多文章,总会遇到一些问题。本文的深色模式适配是通过修改themeMode来实现的,供诸位有缘人参考。二、环境介绍1. Flutter: 2.0.32. Dart: 2.12.03. provider: 5.0.0状态管理,用于运行时切换主题4. shared_preferences: 2.0.5数据持久化,用于保存当前选中的主题,以便下次启动时读取使用用户选择的主题environment: sdk: ">=2.12.0 <3.0.0"dependencies: flutter: sdk: flutter # 忽略了一些依赖... # shared_preferences https://pub.flutter-io.cn/packages/shared_preferences shared_preferences: ^2.0.5 # 全局状态管理 https://github.com/rrousselGit/provider/blob/master/resources/translations/zh-CN/README.md provider: ^5.0.0 三、主题1. ThemeData factory ThemeData({ Brightness brightness, // 应用主题亮度,可选(dark、light) VisualDensity visualDensity, // 视觉密度 MaterialColor primarySwatch, // 主要样式,设置primaryColor后该背景色会被覆盖 Color primaryColor, // 主要部分背景颜色(导航和tabBar等) Brightness primaryColorBrightness, // primaryColor的亮度 Color primaryColorLight, // primaryColor的浅色版 Color primaryColorDark, // primaryColor的深色版 Color accentColor, // 前景色(文本,按钮等) Brightness accentColorBrightness, // accentColor的亮度 Color canvasColor, // MaterialType.canvas 的默认颜色 Color shadowColor, // 阴影颜色 Color scaffoldBackgroundColor, // Scaffold的背景颜色。典型Material应用程序或应用程序内页面的背景颜色 Color bottomAppBarColor, // BottomAppBar的默认颜色 Color cardColor, // Card的颜色 Color dividerColor, // Divider和PopupMenuDivider的颜色,也用于ListTile之间、DataTable的行之间等。 Color focusColor, // 突出颜色 Color hoverColor, // hoverColor Color highlightColor, // 高亮颜色,选中在泼墨动画期间使用的突出显示颜色,或用于指示菜单中的项。 Color splashColor, // 墨水飞溅的颜色。InkWell InteractiveInkFeatureFactory splashFactory, // 定义由InkWell和InkResponse反应产生的墨溅的外观。 Color selectedRowColor, // 用于突出显示选定行的颜色。 Color unselectedWidgetColor, // 用于处于非活动(但已启用)状态的小部件的颜色。例如,未选中的复选框。通常与accentColor形成对比。也看到disabledColor。 Color disabledColor, // 禁用状态下部件的颜色,无论其当前状态如何。例如,一个禁用的复选框(可以选中或未选中)。 Color buttonColor, // RaisedButton按钮中使用的Material 的默认填充颜色。 ButtonThemeData buttonTheme, // 定义按钮部件的默认配置, ToggleButton
  • [交流吐槽] flutter 的镜像支持
    flutter 是一个多终端的前端框架,可以使用一套代码,在IOS,Android 上使用,目前正在支持Windows,Mac等终端,由Google开发并开源。但是由于 flutter 的包以及依赖库,都是架构在Google 自家的服务上,对于国内访问有着很大的问题。希望华为云能够架设相关的镜像,我跟 Flutter 方面联系以及沟通过,建设相关的镜像需要反代两个网址。PUB_HOSTED_URLFLUTTER_STORAGE_BASE_URL他们分别的原始地址是 https://pub.dartlang.org/https://storage.googleapis.com这是官方的介绍https://flutter.io/community/china我也手动反代过,发现需要对 https://pub.dartlang.org/页面里面的一些地址需要替换,例如https://pub.dartlang.org/packages/shared_preferences#-versions-tab-Versions 中的下载地址,需要替换成镜像后FLUTTER_STORAGE_BASE_URL的地址。希望华为云能够支持