Flutter实现Android滚动悬浮效果过程
有以下几种效果
1、tabBar透明度随偏移0-1渐变过度
2、app上下滚动触发tabBar同步滚动
3、tabBar切换触发app上下同步滚动
1、计算每个区块的高度
用keyList保存声明的key,用heightList保存每个key对应的组件高度
// key列表 List<GlobalKey> keyList = [ GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), ]; // 计算每个key对应的高度 List<double> heightList;
把key放到需要计算的组件中(这里最后计算的发现就是500)
Container( key: keyList[index], height: 500, color: colorList[index], )
监听滚动。
备注:controller可以监听CustomScrollView、SingleScrollView、SmartRefresher等,不一定要用CustomScrollView,另外如果是监听SmartRefresher可能会出现负数的情况需要处理成0下。
// 滚动控制器 ScrollController scrollController = new ScrollController(); @override void initState() { scrollController.addListener(() => _onScrollChanged()); super.initState(); } @override Widget build(BuildContext context) { return CustomScrollView( controller: scrollController, slivers: <Widget>[ SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( key: keyList[index], height: 500, color: colorList[index], ); }, childCount: keyList.length, ), ), ], ); }
在滚动动态计算组件高度
备注:这里计算可以用防抖优化,另外这个是计算已绘制的组件高度,因此一定要在滚动的时候动态计算。
// 监听ScrollView滚动 void _onScrollChanged() { initHeightList(); } // 初始化heightList initHeightList() { for (int i = 0; i < keyList.length; i++) { if (keyList[i].currentContext != null) { try { heightList[i] = keyList[i].currentContext.size.height; } catch (e) { // 这里只是计算可视部分,因此需要持续计算 print("can not get size, so do not modify heightList[i]"); } } } }
2、实现分析-tabBar透明度渐变
小于起始点透明度:0
起始点->终点透明度:0-1
大于终点透明度:1
// 监听ScrollView滚动 void _onScrollChanged() { initHeightList(){ // 是否显示tabBar double showTabBarOffset; try { showTabBarOffset = keyList[0].currentContext.size.height - TAB_HEIGHT; } catch (e) { showTabBarOffset = heightList[0] - TAB_HEIGHT; } if (scrollController.offset >= showTabBarOffset) { setState(() { opacity = 1; }); } else { setState(() { opacity = scrollController.offset / showTabBarOffset; if (opacity < 0) { opacity = 0; } }); } }
3、实现分析-app上下滚动触发tabBar
首先接入tabController控制器
// tabBar控制器 TabController tabController; @override void initState() { tabController = TabController(vsync: this, length: listTitle.length); super.initState(); } @override Widget build(BuildContext context) { return TabBar( controller: tabController, indicatorColor: Color(0xfffdd108), labelColor: Color(0xff343a40), unselectedLabelColor: Color(0xff8E9AA6), unselectedLabelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.normal), isScrollable: true, labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), tabs: _buildTabsWidget(listTitle), onTap: _onTabChanged, ); }
然后在滚动中使用tabController.animateTo滚动到tabBar
// 监听ScrollView滚动 void _onScrollChanged() { initHeightList(); // 滑动页面触发tabBar水平滚动 if (scrollController.position.userScrollDirection == ScrollDirection.reverse || scrollController.position.userScrollDirection == ScrollDirection.forward) { double totalOffset = -TAB_HEIGHT; for (int i = 0; i < keyList.length; i++) { if (scrollController.offset >= totalOffset && scrollController.offset < totalOffset + heightList[i]) { tabController.animateTo( i, duration: Duration(milliseconds: 0), ); return; } totalOffset += heightList[i]; } } }
4、实现分析-tabBar切换触发app滚动
首先获取tab的改变事件,在改变时获取当前的targetKey,用于记录需要滚动到什么高度
@override Widget build(BuildContext context) { return TabBar( controller: tabController, indicatorColor: Color(0xfffdd108), labelColor: Color(0xff343a40), unselectedLabelColor: Color(0xff8E9AA6), unselectedLabelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.normal), isScrollable: true, labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), tabs: _buildTabsWidget(listTitle), onTap: _onTabChanged, ); } void _onTabChanged(int index) { targetKey = keyList[index]; _gotoAnchorPoint(); }
然后使用 scrollController.position
.ensureVisible滚动到targetKey所在位置即可
// 点击tabBar去对应锚点 void _gotoAnchorPoint() async { scrollController.position .ensureVisible( targetKey.currentContext.findRenderObject(), alignment: 0.0, ); }
5、源码
tabbar_scroll_demo_page.dart
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class TabBarScrollDemoPage extends StatefulWidget { TabBarScrollDemoPage({ Key key, }) : super(key: key); @override _TabBarScrollDemoPageState createState() => _TabBarScrollDemoPageState(); } class _TabBarScrollDemoPageState extends State<TabBarScrollDemoPage> with SingleTickerProviderStateMixin, WidgetsBindingObserver { // 滚动控制器 ScrollController scrollController = new ScrollController(); // key列表 List<GlobalKey> keyList = [ GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), ]; // 当前锚点key GlobalKey targetKey; // 计算每个key对应的高度 List<double> heightList; // tabBar控制器 TabController tabController; // 是否显示tabBar bool showTabBar = true; // 状态栏高度 static const double TAB_HEIGHT = 48; // 标题 List<String> listTitle = [ "Red", "Orange", "Yellow", "Green", "Indigo", "Blue", "Purple" ]; // 颜色 List<Color> colorList = [ Color(0xffFF0000), Color(0xffFF7F00), Color(0xffFFFF00), Color(0xff00FF00), Color(0xff00FFFF), Color(0xff0000FF), Color(0xff8B00FF), ]; // tabBar过度透明度 double opacity = 0.0; @override void initState() { heightList = List.filled(keyList.length, 0); targetKey = keyList[0]; tabController = TabController(vsync: this, length: listTitle.length); scrollController.addListener(() => _onScrollChanged()); WidgetsBinding.instance.addObserver(this); super.initState(); } void _onTabChanged(int index) { targetKey = keyList[index]; _gotoAnchorPoint(); } // 监听ScrollView滚动 void _onScrollChanged() { initHeightList(); // 是否显示tabBar double showTabBarOffset; try { showTabBarOffset = keyList[0].currentContext.size.height - TAB_HEIGHT; } catch (e) { showTabBarOffset = heightList[0] - TAB_HEIGHT; } if (scrollController.offset >= showTabBarOffset) { setState(() { opacity = 1; }); } else { setState(() { opacity = scrollController.offset / showTabBarOffset; if (opacity < 0) { opacity = 0; } }); } // 滑动页面触发tabBar水平滚动 if (scrollController.position.userScrollDirection == ScrollDirection.reverse || scrollController.position.userScrollDirection == ScrollDirection.forward) { double totalOffset = -TAB_HEIGHT; for (int i = 0; i < keyList.length; i++) { if (scrollController.offset >= totalOffset && scrollController.offset < totalOffset + heightList[i]) { tabController.animateTo( i, duration: Duration(milliseconds: 0), ); return; } totalOffset += heightList[i]; } } } // 初始化heightList initHeightList() { for (int i = 0; i < keyList.length; i++) { if (keyList[i].currentContext != null) { try { heightList[i] = keyList[i].currentContext.size.height; } catch (e) { // 这里只是计算可视部分,因此需要持续计算 print("can not get size, so do not modify heightList[i]"); } } } } // 点击tabBar去对应锚点 void _gotoAnchorPoint() async { GlobalKey key = targetKey; if (key.currentContext != null) { scrollController.position .ensureVisible( key.currentContext.findRenderObject(), alignment: 0.0, ) .then((value) { // 在此基础上再偏移一个TAB_HEIGHT的高度 if (scrollController.offset - TAB_HEIGHT > 0) { scrollController.jumpTo(scrollController.offset - TAB_HEIGHT); } }); return; } // 以下代码处理获取不到key.currentContext情况,没问题也可以去掉 int nearestRenderedIndex = 0; bool foundIndex = false; for (int i = keyList.indexOf(key) - 1; i >= 0; i -= 1) { // find first non-null-currentContext key above target key if (keyList[i].currentContext != null) { try { // Only when size is get without any exception,this key can be used in ensureVisible function Size size = keyList[i].currentContext.size; print("size: $size"); foundIndex = true; nearestRenderedIndex = i; } catch (e) { print("size not availabel"); } break; } } if (!foundIndex) { for (int i = keyList.indexOf(key) + 1; i < keyList.length; i += 1) { // find first non-null-currentContext key below target key if (keyList[i].currentContext != null) { try { // Only when size is get without any exception,this key can be used in ensureVisible function Size size = keyList[i].currentContext.size; print("size: $size"); foundIndex = true; nearestRenderedIndex = i; } catch (e) { print("size not availabel"); } break; } } } int increasedOffset = nearestRenderedIndex < keyList.indexOf(key) ? 1 : -1; for (int i = nearestRenderedIndex; i >= 0 && i < keyList.length; i += increasedOffset) { if (keyList[i].currentContext == null) { Future.delayed(new Duration(microseconds: 10), () { _gotoAnchorPoint(); }); return; } if (keyList[i] != targetKey) { await scrollController.position.ensureVisible( keyList[i].currentContext.findRenderObject(), alignment: 0.0, curve: Curves.linear, alignmentPolicy: increasedOffset == 1 ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart, ); } else { await scrollController.position .ensureVisible( keyList[i].currentContext.findRenderObject(), alignment: 0.0, ) .then((value) { Future.delayed(new Duration(microseconds: 1000), () { if (scrollController.offset - TAB_HEIGHT > 0) { scrollController.jumpTo(scrollController.offset - TAB_HEIGHT); } else {} }); }); break; } } } // 悬浮tab的item List<Widget> _buildTabsWidget(List<String> tabList) { var list = List<Widget>(); String keyValue = DateTime.now().millisecondsSinceEpoch.toString(); for (var i = 0; i < tabList.length; i++) { var widget = Tab( text: tabList[i], key: Key("i$keyValue"), ); list.add(widget); } return list; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('滚动悬浮示例Demo'), ), body: Center( child: Stack( alignment: Alignment.topLeft, overflow: Overflow.clip, children: <Widget>[ Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Expanded( child: CustomScrollView( controller: scrollController, slivers: <Widget>[ SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( key: keyList[index], height: 500, color: colorList[index], ); }, childCount: keyList.length, ), ), ], ), ), ], ), if (showTabBar) Positioned( top: 0, width: MediaQuery.of(context).size.width, child: Opacity( opacity: opacity, child: Container( color: Colors.white, child: TabBar( controller: tabController, indicatorColor: Color(0xfffdd108), labelColor: Color(0xff343a40), unselectedLabelColor: Color(0xff8E9AA6), unselectedLabelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.normal), isScrollable: true, labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), tabs: _buildTabsWidget(listTitle), onTap: _onTabChanged, ), ), ), ), ], ), ), ); } }
直接运行上述文件即可
示例: main.dart
import 'package:flutter/material.dart'; import 'package:scroll_tabbar_sample/tabbar_scroll_demo_page.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: TabBarScrollDemoPage(), ); } }
到此这篇关于Flutter实现Android滚动悬浮效果过程的文章就介绍到这了,更多相关Flutter滚动悬浮内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解Android中ListView实现图文并列并且自定义分割线(完善仿微信APP)
本篇文章主要介绍了Android中ListView实现图文并列并且自定义分割线(完善仿微信APP),具有一定的参考价值,有兴趣的可以了解一下。2016-12-12Android使用Retrofit实现自定义Converter解析接口流程详解
Retrofit是一个RESTful的HTTP网络请求框架的封装,网络请求的工作本质上是OkHttp完成,而Retrofit仅负责网络请求接口的封装2023-03-03Android控件之CheckBox、RadioButton用法实例分析
这篇文章主要介绍了Android控件之CheckBox、RadioButton用法,以实例形式较为详细的分析了CheckBox和RadioButton实现复选按钮及单选按钮功能的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下2015-09-09图解 Kotlin SharedFlow 缓存系统及示例详解
这篇文章主要为大家介绍了图解 Kotlin SharedFlow 缓存系统及示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-10-10Android学习笔记之AndroidManifest.xml文件解析(详解)
这篇文章主要介绍了Android学习笔记之AndroidManifest.xml文件解析,需要的朋友可以参考下2015-10-10Android 解决dialog弹出时无法捕捉Activity的back事件问题
这篇文章主要介绍了Android 解决dialog弹出时无法捕捉Activity的back事件问题的相关资料,需要的朋友可以参考下2016-11-11
最新评论