Flutter 获取垂直列表 - 在屏幕中显示的 - 第一项索引(Index )值

1,231 阅读4分钟

首先申明,第一次写掘金技术文档,请各位大佬轻喷......

还请欢迎多多技术交流,B站用户名称 撸码小狂魔 ~ 嘿嘿.

需求来源:

临近过年,无心上班,研究技术,填补空缺.

作为Android原生开发出生,使用了Flutter开发大概有2年,深感有点回不去了.

唔~说正事吧:

平常躺尸时刷App发现某些App的垂直滑动列表中,如果出现视频items,那么屏幕上的首个视频item会自动播放.

一直在想如何使用Flutter撸一个同样效果的封装实现,今天有时间来研究,

大概思路如下:
  1. 需要一个垂直的ListVuew(有点废话哈)
  2. 须知道滚动的状态,并且要有各状态的回调,比如:开始滚动 \ 滚动中 \ 滚动结束
  3. 时刻明白自己滚了多远(这个简单,ScrollController就这一点好处了)
  4. 最重要的一点:各个列表项中Widget的高度是多少(不知道还计算个毛线)
  5. 最后......在滚动结束后的回调......要有屏幕显示的列表的....第一项.......索引(Index)值

抱着这些想法,看看我的实现效果Demo

最终效果:

录屏_选择区域_20220124144712.gif

我的Flutter环境:

截图_deepin-terminal_20220124145019.png

由于公司项目中使用了闲鱼的 flutter_boost 'v3.0-preview.18',所以一直没有升到最新的2.8的版本,一升就GG

工程结构:

特别简单,就三个文件,最核心的文件已封装在 ListViewVerTopIndexInScreenShowWidget.dart文件中,其它的两个文件只是为了做Demo展示用

截图_选择区域_20220124145435.png

这里贴上核心文件ListViewVerTopIndexInScreenShowWidget.dart的代码

import 'package:flutter/material.dart';

/**
 * create by 蒲杰
 * 994792649@qq.com
 * 2022/1/24 下午1:47
 * 说明:
 **/
// ignore_for_file: file_names

class ListViewVerTopIndexInScreenShowWidget extends StatefulWidget {
  // 列表数量
  final int itemCount;
  // 列表Widget创建回调
  final IndexedWidgetBuilder itemBuilder;
  // 列表分割Widget创建回调
  final IndexedWidgetBuilder separatorBuilder;
  // 滑动开始回调
  final Function? scrollStartCallBack;
  // 滑动中回调
  final Function? scrollUpdateCallBack;
  // 滑动结束回调
  final Function(int topIndexInScreen)? scrollEndCallBack;

  const ListViewVerTopIndexInScreenShowWidget({
    Key? key,
    required this.itemCount,
    required this.itemBuilder,
    required this.separatorBuilder,
    this.scrollStartCallBack,
    this.scrollUpdateCallBack,
    this.scrollEndCallBack,
  }) : super(key: key);

  @override
  State<ListViewVerTopIndexInScreenShowWidget> createState() =>
      _ListViewVerTopIndexInScreenShowWidgetState();
}

class _ListViewVerTopIndexInScreenShowWidgetState
    extends State<ListViewVerTopIndexInScreenShowWidget> {

  ScrollController controller = ScrollController();

  final Map<int, double> _indexChildAndSizeH = <int, double>{};
  final Map<int, double> _indexDividerAndSizeH = <int, double>{};

  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (Notification notification) {
        if (notification is ScrollStartNotification) {
          // 滚动开始
          if (widget.scrollStartCallBack != null) {
            widget.scrollStartCallBack!();
          }
        }

        if (notification is ScrollUpdateNotification) {
          // 滚动中
          if (widget.scrollUpdateCallBack != null) {
            widget.scrollUpdateCallBack!();
          }
        }

        if (notification is ScrollEndNotification) {
          // 停止滚动

          if (notification.metrics.extentBefore == 0) {
            // 滚动到头部

            if (widget.scrollEndCallBack != null) {
              widget.scrollEndCallBack!(0);
            }

            return true;
          }

          // if (notification.metrics.extentAfter == 0) {
          //   debugPrint("滚动到底部");
          // }

          double scrollOffset = controller.offset;
          // debugPrint("滚动偏移为: $scrollOffset");

          int indexKey = 0;
          double indexOffsetCount = 0.0;

          for (int i in _indexChildAndSizeH.keys.toList()) {
            indexKey = i;

            indexOffsetCount += _indexChildAndSizeH[i]!.toDouble();

            double dividerOffset = 0;

            if (indexKey - 1 >= 0) {
              dividerOffset = _indexDividerAndSizeH[indexKey - 1]!.toDouble();
            }
            indexOffsetCount += dividerOffset;

            if (indexOffsetCount -
                    scrollOffset -
                    _indexChildAndSizeH[i]!.toDouble() -
                    dividerOffset >
                0) {
              break;
            }
          }

          if (widget.scrollEndCallBack != null) {
            widget.scrollEndCallBack!(indexKey);
          }

        }

        return true;
      },
      child: ListView.separated(
        controller: controller,
        physics: const BouncingScrollPhysics(),
        padding: EdgeInsets.zero,
        itemBuilder: (context, index) {
          return _ListItem(
            framFirstCallBack: (sizeH) {
              _indexChildAndSizeH[index] = sizeH ?? 0;
            },
            child: widget.itemBuilder(context, index),
          );
        },
        separatorBuilder: (context, index) {
          return _ListItem(
            framFirstCallBack: (sizeH) {
              _indexDividerAndSizeH[index] = sizeH ?? 0;
            },
            child: widget.separatorBuilder(context, index),
          );
        },
        itemCount: widget.itemCount,
      ),
    );
  }
}

class _ListItem extends StatefulWidget {
  final Widget child;
  final Function(double? size) framFirstCallBack;

  const _ListItem({
    required this.child,
    required this.framFirstCallBack,
    Key? key,
  }) : super(key: key);

  @override
  _ListItemState createState() => _ListItemState();
}

class _ListItemState extends State<_ListItem> with WidgetsBindingObserver {
  late BuildContext _context;

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance?.addObserver(this);
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      widget.framFirstCallBack(_context.size?.height);
    });
  }

  @override
  Widget build(BuildContext context) {
    _context = context;
    return widget.child;
  }
}
代码解释1,为嘛使用ListView.separated:

因为考虑到可能会做列表项的分割UI操作,所以使用此列表的创建方式,如果您不想使用分割UI,在separatorBuilder的回调返回Widget中返回为SizedBox()即可

代码解释2,如何监听滚动的状态

最开始想在ListView的ScrollController上动文章,想着使用ScrollController的滑动监听+最近滑动时间的判断之类的骚操作,怎么尝试怎么不对.

冷静半小时后,查看ScrollController的源码发现了Notification,突然顿开...可以通过Widget的通知监听来考察滑动的状态,一百度,还真有相关的通知监听.so....完美解决

这里贴上核心代码

NotificationListener(
  onNotification: (Notification notification) {
    if (notification is ScrollStartNotification) {
      // 滚动开始
    }
    if (notification is ScrollUpdateNotification) {
      // 滚动中
    }
    if (notification is ScrollEndNotification) {
      // 停止滚动
      if (notification.metrics.extentBefore == 0) {
        // 滚动到头部
      }
      if (notification.metrics.extentAfter == 0) {
        // 滚动到底部
      }
    }
    return true;
  },
  child: ***,
)
代码解释3:_ListItem这个Widget是干什么的

如果你细心一点的话,你会发现这是一个StatefulWidget(有状态)的Widget.

并且在它的State上挂(混入)了一个WidgetsBindingObserver(生命周期监听),用来获取此Widget第一次渲染完毕后能拿到的Widget宽高等信息,并且回传至上级Widget进行管理和使用.

这里贴上核心代码

WidgetsBinding.instance?.addObserver(this);
// 第一次渲染完毕的回调
WidgetsBinding.instance?.addPostFrameCallback((_) {
  widget.framFirstCallBack(_context.size?.height);
});

到此,Flutter获取垂直列表在屏幕中显示的第一项索引(Index )值的实现封装完毕,如果您想使用,只需复制核心文件中的代码,即可进行使用.

各位大佬如有其它的实现方式,欢迎交流.