Flutter實現(xiàn)Android滾動懸浮效果過程
有以下幾種效果
1、tabBar透明度隨偏移0-1漸變過度
2、app上下滾動觸發(fā)tabBar同步滾動
3、tabBar切換觸發(fā)app上下同步滾動

1、計算每個區(qū)塊的高度
用keyList保存聲明的key,用heightList保存每個key對應的組件高度
// key列表 List<GlobalKey> keyList = [ GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey(), ]; // 計算每個key對應的高度 List<double> heightList;
把key放到需要計算的組件中(這里最后計算的發(fā)現(xiàn)就是500)
Container( key: keyList[index], height: 500, color: colorList[index], )
監(jiān)聽滾動。
備注:controller可以監(jiān)聽CustomScrollView、SingleScrollView、SmartRefresher等,不一定要用CustomScrollView,另外如果是監(jiān)聽SmartRefresher可能會出現(xiàn)負數(shù)的情況需要處理成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,
),
),
],
);
}
在滾動動態(tài)計算組件高度
備注:這里計算可以用防抖優(yōu)化,另外這個是計算已繪制的組件高度,因此一定要在滾動的時候動態(tài)計算。
// 監(jiān)聽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) {
// 這里只是計算可視部分,因此需要持續(xù)計算
print("can not get size, so do not modify heightList[i]");
}
}
}
}
2、實現(xiàn)分析-tabBar透明度漸變
小于起始點透明度:0
起始點->終點透明度:0-1
大于終點透明度:1
// 監(jiān)聽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、實現(xiàn)分析-app上下滾動觸發(fā)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
// 監(jiān)聽ScrollView滾動
void _onScrollChanged() {
initHeightList();
// 滑動頁面觸發(fā)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、實現(xiàn)分析-tabBar切換觸發(fā)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;
// 狀態(tài)欄高度
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();
}
// 監(jiān)聽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;
}
});
}
// 滑動頁面觸發(fā)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) {
// 這里只是計算可視部分,因此需要持續(xù)計算
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實現(xiàn)Android滾動懸浮效果過程的文章就介紹到這了,更多相關Flutter滾動懸浮內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
android中WebView和javascript實現(xiàn)數(shù)據(jù)交互實例
這篇文章主要介紹了android中WebView和javascript實現(xiàn)數(shù)據(jù)交互實例,需要的朋友可以參考下2014-07-07
如何通過Android Logcat插件分析firebase崩潰問題
android crash Crash(應用崩潰)是由于代碼異常而導致App非正常退出,導致應用程序無法繼續(xù)使用,所有工作都停止的現(xiàn)象,本文重點介紹如何通過Android Logcat插件分析firebase崩潰問題,感興趣的朋友一起看看吧2024-01-01
Android基于OpenCV實現(xiàn)Harris角點檢測
角點就是極值點,即在某方面屬性特別突出的點。當然,你可以自己定義角點的屬性(設置特定熵值進行角點檢測)。角點可以是兩條線的交叉處,也可以是位于相鄰的兩個主要方向不同的事物上的點。本文介紹如何基于OpenCV實現(xiàn)Harris角點檢測2021-06-06
Android實現(xiàn)Service獲取當前位置(GPS+基站)的方法
這篇文章主要介紹了Android實現(xiàn)Service獲取當前位置(GPS+基站)的方法,較為詳細的分析了Service基于GPS位置的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09
Android實現(xiàn)新增及編輯聯(lián)系人的方法
這篇文章主要介紹了Android實現(xiàn)新增及編輯聯(lián)系人的方法,是Android應用開發(fā)常見的功能,需要的朋友可以參考下2014-07-07

