flutter实现上滑出现隐藏吸顶的tab栏,滚动到相应位置切换tab栏。

2,703 阅读3分钟

前言

flutter实现上滑出现隐藏吸顶的tab栏,滚动到相应位置切换tab栏,
类似购物网站宝贝详情页上拉的效果,和大家一起学习探讨。

先上效果图:

概述

CustomScrollView是Flutter提供的可以用来自定义滚动效果的组件,
它可以像胶水一样将多个Sliver粘合在一起。吸顶部分用了SliverPersistentHeader实现的,SliverAppBar也是基于此实现的,
SliverPersistentHeader最重要的一个属性是SliverPersistentHeaderDelegate

自定义头部

SliverPersistentHeaderDelegate的实现类必须实现其4个方法。其中:

minExtent:收起状态下组件的高度; maxExtent:展开状态下组件的高度; shouldRebuild:类似于react中的shouldComponentUpdate; build:构建渲染的内容。 代码如下:

其中 shrinkOffset它代表当前头部的滚动偏移量,利用shrinkOffset > this.thresholdValue 滚动偏移量大于传入他的阈值来隐藏和控制显示本来的头部和tab栏直接切换。

class SliverCustomHeaderStatusDelegate extends SliverPersistentHeaderDelegate {
  BuildContext context;
  Widget tabs;
  Widget top;
  num thresholdValue;
  num minH;
  num maxH;

  SliverCustomHeaderStatusDelegate(
      {this.context,
        this.tabs,
        this.top,
        this.thresholdValue,
        this.maxH,
        this.minH});

  @override
  double get minExtent => this.minH;

  @override
  double get maxExtent => this.maxH;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  bool makeStickyTab(shrinkOffset) {
    //它代表当前头部的滚动偏移量
    if (shrinkOffset > this.thresholdValue) {
      return true;
    } else {
      return false;
    }
  }

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
        height: this.maxExtent,
        width: MediaQuery.of(context).size.width,
        child: this.makeStickyTab(shrinkOffset) ? tabs : top);
  }
}

测量

向下滑动页面,达到根据下拉距离自动切换tab栏的效果,需要动态算出每个tab栏对应下拉部分距离最上方的距离,测距如下:

首先申请全局key

var globalKeyOne = GlobalKey();

布局的时候用上可以

    SliverToBoxAdapter(
      child: Container(
        key: globalKeyOne,
        width: double.infinity,
        height: ScreenUtil().setHeight(400),
        color: Colors.red,
      ),
    ),

然后initstate时候测量,延时一下,需要等state layout结束之后才能获取size

      /// 延时一下,需要等state layout结束之后才能获取size
      Future.delayed(Duration(milliseconds: 100), () {
        oneY = getY(globalKeyOne.currentContext);
        twoY = getY(globalKeyTwo.currentContext);
        threeY = getY(globalKeyThree.currentContext);
        fourY = getY(globalKeyFour.currentContext);

        header = ScreenUtil().setHeight(this.headerMax - this.headerMin) +
            10; //10是误差
      });

获取widget所在位置距离顶部的距离用方法:

  double getY(BuildContext buildContext) {
    final RenderBox box = buildContext.findRenderObject();
    //final size = box.size;
    final topLeftPosition = box.localToGlobal(Offset.zero);
    return topLeftPosition.dy;
  }

滚动

滚动是根据ScrollController的jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。

点击tab栏每个tab切换到对应的部分,利用测量处理的每个部分的距离和滑动的距离动态jumpTo到指定的位置

 TabBar(
      onTap: (index) {
        if (!mounted) {
          return;
        }
        switch (index) {
          case 0:
            _controller.jumpTo(0);
            _tabController.animateTo(0);
            break;
          case 1:
            _controller.jumpTo(header);
            _tabController.animateTo(1);
            break;
          case 2:
            _controller.jumpTo(header + (twoY - oneY));
            _tabController.animateTo(2);
            break;
          case 3:
            _controller.jumpTo(header + (threeY - oneY));
            _tabController.animateTo(3);
            break;
          case 4:
            _controller.jumpTo(header + (fourY - oneY));
            _tabController.animateTo(4);
            break;
        }

同时在initState里监听滚动到什么位置,根据滚动的位置动态去切换tab激活位置。在ScrollController的监听事件里处理逻辑。

      _controller.addListener(() {
        var of = _controller.offset;
        //第二块距离顶部距离
        var distance_2 = header + (twoY - oneY);
        //第3块距离顶部距离
        var distance_3 = header + (threeY - oneY);
        //第4块距离顶部距离
        var distance_4 = header + (fourY - oneY);
        if (of > header && of < distance_2) {
          _tabController.animateTo(1);
        } else if (of > distance_2 && of < distance_3) {
          _tabController.animateTo(2);
        } else if (of > distance_3 && of < distance_4) {
          _tabController.animateTo(3);
        } else if (of > distance_4) {
          _tabController.animateTo(4);
        }
      });

采坑部分就是之前用的NestedScrollView,body部分之间用的SingleChildScrollView,将_controller放到了SingleChildScrollView上,之间就不滚动了,还是对sliver系列理解不够透彻,后来换成CustomScrollView,将_controller放到CustomScrollView上就没有问题了,

    return SafeArea(
      child: Scaffold(
        backgroundColor: Color(0xFF201E24),
        body: CustomScrollView(
          controller: _controller,
          slivers: [
            SliverPersistentHeader(
              pinned: true,
              delegate: SliverCustomHeaderStatusDelegate(
                tabs: _tab,
                top: _top,
                minH: ScreenUtil().setHeight(this.headerMin),
                maxH: ScreenUtil().setHeight(this.headerMax),
                thresholdValue:
                ScreenUtil().setHeight(this.headerMax - this.headerMin),
                context: context,
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                key: globalKeyOne,
                width: double.infinity,
                height: ScreenUtil().setHeight(400),
                color: Colors.red,
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                key: globalKeyTwo,
                width: double.infinity,
                height: ScreenUtil().setHeight(1400),
                color: Colors.green,
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                key: globalKeyThree,
                width: double.infinity,
                height: ScreenUtil().setHeight(1800),
                color: Colors.grey,
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                key: globalKeyFour,
                width: double.infinity,
                height: ScreenUtil().setHeight(400),
                color: Colors.pinkAccent,
              ),
            ),
          ],
        ),
      ),
    );

具体全部代码在项目地址


欢迎大家和我一起学习分享flutter,项目会持续更新新的学习demo

此项目的github地址:项目地址

下面是我们的公众号:flutter编程笔记(code9871)

公众号 不定期分享自己的学习想法

往期回顾:

flutter实现数字刻度尺

flutter实现简单的旋转动画

flutter实现简单的抽屉效果

flutter实现顶部吸附效果