Flutter实现Android滚动悬浮效果过程

手机APP/开发
318
0
0
2023-07-27
目录
  • 1、计算每个区块的高度
  • 2、实现分析-tabBar透明度渐变
  • 3、实现分析-app上下滚动触发tabBar
  • 4、实现分析-tabBar切换触发app滚动
  • 5、源码

有以下几种效果

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:,
  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:,
              color: colorList[index],
            );
          },
          childCount: keyList.length,
        ),
      ),
    ],
  );
}

在滚动动态计算组件高度

备注:这里计算可以用防抖优化,另外这个是计算已绘制的组件高度,因此一定要在滚动的时候动态计算。

// 监听ScrollView滚动
void _onScrollChanged() {
  initHeightList();
}
// 初始化heightList
initHeightList() {
  for (int i =; 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[].currentContext.size.height - TAB_HEIGHT;
  } catch (e) {
    showTabBarOffset = heightList[] - TAB_HEIGHT;
  }
  if (scrollController.offset >= showTabBarOffset) {
    setState(() {
      opacity =;
    });
  } else {
    setState(() {
      opacity = scrollController.offset / showTabBarOffset;
      if (opacity <) {
        opacity =;
      }
    });
  }
}

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(xfffdd108),
    labelColor: Color(xff343a40),
    unselectedLabelColor: Color(xff8E9AA6),
    unselectedLabelStyle: TextStyle(
        fontSize:, fontWeight: FontWeight.normal),
    isScrollable: true,
    labelStyle:
    TextStyle(fontSize:, 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 =; i < keyList.length; i++) {
      if (scrollController.offset >= totalOffset &&
          scrollController.offset < totalOffset + heightList[i]) {
        tabController.animateTo(
          i,
          duration: Duration(milliseconds:),
        );
        return;
      }
      totalOffset += heightList[i];
    }
  }
}

4、实现分析-tabBar切换触发app滚动

首先获取tab的改变事件,在改变时获取当前的targetKey,用于记录需要滚动到什么高度

@override
Widget build(BuildContext context) {
  return TabBar(
    controller: tabController,
    indicatorColor: Color(xfffdd108),
    labelColor: Color(xff343a40),
    unselectedLabelColor: Color(xff8E9AA6),
    unselectedLabelStyle: TextStyle(
        fontSize:, fontWeight: FontWeight.normal),
    isScrollable: true,
    labelStyle:
    TextStyle(fontSize:, 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,
  );
}

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 =;
  // 标题
  List<String> listTitle = [
    "Red",
    "Orange",
    "Yellow",
    "Green",
    "Indigo",
    "Blue",
    "Purple"
  ];
  // 颜色
  List<Color> colorList = [
    Color(xffFF0000),
    Color(xffFF7F00),
    Color(xffFFFF00),
    Color(xff00FF00),
    Color(xff00FFFF),
    Color(xff0000FF),
    Color(xff8B00FF),
  ];
  // tabBar过度透明度
  double opacity =.0;
  @override
  void initState() {
    heightList = List.filled(keyList.length,);
    targetKey = keyList[];
    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[].currentContext.size.height - TAB_HEIGHT;
    } catch (e) {
      showTabBarOffset = heightList[] - TAB_HEIGHT;
    }
    if (scrollController.offset >= showTabBarOffset) {
      setState(() {
        opacity =;
      });
    } else {
      setState(() {
        opacity = scrollController.offset / showTabBarOffset;
        if (opacity <) {
          opacity =;
        }
      });
    }
    // 滑动页面触发tabBar水平滚动
    if (scrollController.position.userScrollDirection ==
            ScrollDirection.reverse ||
        scrollController.position.userScrollDirection ==
            ScrollDirection.forward) {
      double totalOffset = -TAB_HEIGHT;
      for (int i =; i < keyList.length; i++) {
        if (scrollController.offset >= totalOffset &&
            scrollController.offset < totalOffset + heightList[i]) {
          tabController.animateTo(
            i,
            duration: Duration(milliseconds:),
          );
          return;
        }
        totalOffset += heightList[i];
      }
    }
  }
  // 初始化heightList
  initHeightList() {
    for (int i =; 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,
      )
          .then((value) {
        // 在此基础上再偏移一个TAB_HEIGHT的高度
        if (scrollController.offset - TAB_HEIGHT >) {
          scrollController.jumpTo(scrollController.offset - TAB_HEIGHT);
        }
      });
      return;
    }
    // 以下代码处理获取不到key.currentContext情况,没问题也可以去掉
    int nearestRenderedIndex =;
    bool foundIndex = false;
    for (int i = keyList.indexOf(key) -; 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) +; 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;
    for (int i = nearestRenderedIndex;
        i >= && i < keyList.length;
        i += increasedOffset) {
      if (keyList[i].currentContext == null) {
        Future.delayed(new Duration(microseconds:), () {
          _gotoAnchorPoint();
        });
        return;
      }
      if (keyList[i] != targetKey) {
        await scrollController.position.ensureVisible(
          keyList[i].currentContext.findRenderObject(),
          alignment:.0,
          curve: Curves.linear,
          alignmentPolicy: increasedOffset ==
              ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd
              : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
        );
      } else {
        await scrollController.position
            .ensureVisible(
          keyList[i].currentContext.findRenderObject(),
          alignment:.0,
        )
            .then((value) {
          Future.delayed(new Duration(microseconds:), () {
            if (scrollController.offset - TAB_HEIGHT >) {
              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 =; 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:,
                              color: colorList[index],
                            );
                          },
                          childCount: keyList.length,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
            if (showTabBar)
              Positioned(
                top:,
                width: MediaQuery.of(context).size.width,
                child: Opacity(
                  opacity: opacity,
                  child: Container(
                    color: Colors.white,
                    child: TabBar(
                      controller: tabController,
                      indicatorColor: Color(xfffdd108),
                      labelColor: Color(xff343a40),
                      unselectedLabelColor: Color(xff8E9AA6),
                      unselectedLabelStyle: TextStyle(
                          fontSize:, fontWeight: FontWeight.normal),
                      isScrollable: true,
                      labelStyle:
                          TextStyle(fontSize:, 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(),
    );
  }
}