Flutter - 在额外滚动条上显示行 - 列顶部(就像 Whatsapp 存档的聊天记录)
Flutter - Show Row on extra scroll - Top of column (Like Whatsapp Archived Chats)
我想在一列之上放一行,一开始是不可见的。仅当 SingleChildScrollview 的滚动偏移量为负数时才会可见。
换句话说,只有当用户滚动得比正常情况(向下运动)更远时,才会显示此行。这是 Whatsapp 中的示例。 “搜索框”小部件最初不显示,仅在向上滚动时显示,向下滚动后消失。
更新
使用@Lulupointu 的回答,我得到了这个:
顶部小部件以流畅的动画滚动显示和隐藏。
@Hooshyar 的回答也有效,但不太流畅,并且使用了不同的方法。
尝试 SliverAppBar 并将 floating
和 snap
命名参数设置为 true
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
snap: true,
floating: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('SliverAppBar'),
background: FlutterLogo(),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
);
}
}
这里使用 singleChildScrollView 是我使用 s NotificationListener() 小部件想到的,还有其他解决方案,但这个是最简单的:
有一个布尔值来确定容器可见性:
bool shouldIShowTheUpperThing = false;
用 NotificationListener() 包装您的 SingleChildScrollView() :
NotificationListener(
child: SingleChildScrollView(
child: Column(
children: [
shouldIShowTheUpperThing == false ? Row(
children: [
Container(height: 0,),
],
) : Row(
children: [
Expanded(child: Container(color: Colors.red , height: 100 , child: Text('the hidden box'),)),
],
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueGrey,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueAccent,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.amber,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.black12,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
),
],
),
),
onNotification: (t) {
if (t is ScrollNotification) {
if (t.metrics.pixels < 1.0) {
setState(() {
shouldIShowTheUpperThing = true;
});
}
}
return true;
});
}
由于这是一个不常见的滚动效果,如果你想要它看起来不错,我认为你需要使用条子。
实施
我所做的是复制粘贴SliverToBoxAdapter
并修改它。
特点:
- 首次加载小部件时 child 隐藏
- child活泼
- 当用户再次向下滚动时child隐藏
限制:
- 如果
CustomScrollView
到over-scroll的children不够,child
会一直可见,启动时会出现奇怪的滚动效果
class SliverHidedHeader extends SingleChildRenderObjectWidget {
const SliverHidedHeader({
Key? key,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverHidedHeader(context: context);
}
}
class RenderSliverHidedHeader extends RenderSliverSingleBoxAdapter {
RenderSliverHidedHeader({
required BuildContext context,
RenderBox? child,
}) : _context = context,
super(child: child);
/// Whether we need to apply a correction to the scroll
/// offset during the next layout
///
///
/// This is useful to avoid the viewport to jump when we
/// insert/remove the child.
///
/// If [showChild] is true, its an insert
/// If [showChild] is false, its a removal
bool _correctScrollOffsetNextLayout = true;
/// Whether [child] should be shown
///
///
/// This is used to hide the child when the user scrolls down
bool _showChild = true;
/// The context is used to get the [Scrollable]
BuildContext _context;
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
// Here are the few custom lines, which use [scrollOffsetCorrection]
// to remove the child size
//
// Note that this should only be called for correction linked with the
// insertion (NOT the removal)
if (_correctScrollOffsetNextLayout) {
geometry = SliverGeometry(scrollOffsetCorrection: childExtent);
_correctScrollOffsetNextLayout = false;
return;
}
// Subscribe a listener to the scroll notifier
// which will snap if needed
_manageSnapEffect(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
// Subscribe a listener to the scroll notifier
// which hide the child if needed
_manageInsertChild(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
paintOrigin: _showChild ? 0 : -paintedChildSize,
layoutExtent: _showChild ? null : 0,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow:
childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child!, constraints, geometry!);
}
/// Override to remove the listeners if needed
@override
void dispose() {
final _scrollPosition = Scrollable.of(_context)!.position;
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
super.dispose();
}
/// The listener which will snap if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedSnapScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for the snapping effect
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
_manageSnapEffect({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedSnapScrollNotifierListener = () => _snapScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier.addListener(_subscribedSnapScrollNotifierListener!);
}
/// Snaps if the user just stopped scrolling and the child is
/// partially visible
void _snapScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// Whether the user is currently idle (i.e not scrolling)
//
// We don't check _scrollPosition.activity.isScrolling or
// _scrollPosition.isScrollingNotifier.value because even if
// the user is holding still we don't want to start animating
//
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final isIdle = _scrollPosition.activity is IdleScrollActivity;
// Whether at least part of the child is visible
final isChildVisible = paintedChildSize > 0;
if (isIdle && isChildVisible) {
// If more than half is visible, snap to see everything
if (paintedChildSize >= childExtent / 2 && paintedChildSize != childExtent) {
_scrollPosition.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeOut,
);
}
// If less than half is visible, snap to hide
else if (paintedChildSize < childExtent / 2 && paintedChildSize != 0) {
_scrollPosition.animateTo(
childExtent,
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
}
/// The listener which will hide the child if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedInsertChildScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for inserting/removing the child if needed
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
void _manageInsertChild({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedInsertChildScrollNotifierListener = () => _insertChildScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier
.addListener(_subscribedInsertChildScrollNotifierListener!);
}
/// When [ScrollPosition.isScrollingNotifier] fires:
/// - If the viewport is at the top and the child is not visible,
/// ^ insert the child
/// - If the viewport is NOT at the top and the child is NOT visible,
/// ^ remove the child
void _insertChildScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
final isScrolling = _scrollPosition.isScrollingNotifier.value;
// If the user is still scrolling, do nothing
if (isScrolling) {
return;
}
final scrollOffset = _scrollPosition.pixels;
// If the viewport is at the top and the child is not visible,
// insert the child
//
// We use 0.1 as a small value in case the user is nearly scrolled
// all the way up
if (!_showChild && scrollOffset <= 0.1) {
_showChild = true;
_correctScrollOffsetNextLayout = true;
markNeedsLayout();
}
// There is sometimes an issue with [ClampingScrollPhysics] where
// the child is NOT shown but the scroll offset still includes [childExtent]
//
// There is no why to detect it but we always insert the child when all
// this conditions are united.
// This means that if a user as [ClampingScrollPhysics] and stops scrolling
// exactly at [childExtent], the child will be wrongfully inserted. However
// this seems a small price to pay to avoid the issue.
if (_scrollPosition.physics.containsScrollPhysicsOfType<ClampingScrollPhysics>()) {
if (!_showChild && scrollOffset == childExtent) {
_showChild = true;
markNeedsLayout();
}
}
// If the viewport is NOT at the top and the child is NOT visible,
// remove the child
if (_showChild && scrollOffset > childExtent) {
_showChild = false;
markNeedsLayout();
// We don't have to correct the scroll offset here, no idea why
}
}
}
/// An extension on [ScrollPhysics] to check if it or its
/// parent are the given [ScrollPhysics]
extension _ScrollPhysicsExtension on ScrollPhysics {
/// Check the type of this [ScrollPhysics] and its parents and return
/// true if any is of type [T]
bool containsScrollPhysicsOfType<T extends ScrollPhysics>() {
return this is T || (parent?.containsScrollPhysicsOfType<T>() ?? false);
}
}
如何使用
在你的条子列表的顶部使用它:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MaterialApp(home: MyStatefulWidget()));
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverHidedHeader(
child: Container(
child: Center(child: Text('SliverAppBar')),
height: 100,
color: Colors.redAccent,
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
),
);
}
}
其他资源
如果您想用不同的方法解决这个问题,请查看 pull_to_refresh 包。请注意,他们的代码非常复杂,因为他们还实现了 auto-hide 功能,但如果您有时间,值得一看。
解决限制
我不确定使用这种方法可以解决限制。问题是,出于性能原因,sliver 对它下面的那个一无所知,这意味着它甚至很难知道我们何时处于有问题的情况下,更不用说处理它了。
我想在一列之上放一行,一开始是不可见的。仅当 SingleChildScrollview 的滚动偏移量为负数时才会可见。
换句话说,只有当用户滚动得比正常情况(向下运动)更远时,才会显示此行。这是 Whatsapp 中的示例。 “搜索框”小部件最初不显示,仅在向上滚动时显示,向下滚动后消失。
更新
使用@Lulupointu 的回答,我得到了这个:
顶部小部件以流畅的动画滚动显示和隐藏。 @Hooshyar 的回答也有效,但不太流畅,并且使用了不同的方法。
尝试 SliverAppBar 并将 floating
和 snap
命名参数设置为 true
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
snap: true,
floating: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('SliverAppBar'),
background: FlutterLogo(),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
);
}
}
这里使用 singleChildScrollView 是我使用 s NotificationListener() 小部件想到的,还有其他解决方案,但这个是最简单的:
有一个布尔值来确定容器可见性:
bool shouldIShowTheUpperThing = false;
用 NotificationListener() 包装您的 SingleChildScrollView() :
NotificationListener(
child: SingleChildScrollView(
child: Column(
children: [
shouldIShowTheUpperThing == false ? Row(
children: [
Container(height: 0,),
],
) : Row(
children: [
Expanded(child: Container(color: Colors.red , height: 100 , child: Text('the hidden box'),)),
],
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueGrey,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueAccent,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.amber,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.black12,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
),
],
),
),
onNotification: (t) {
if (t is ScrollNotification) {
if (t.metrics.pixels < 1.0) {
setState(() {
shouldIShowTheUpperThing = true;
});
}
}
return true;
});
}
由于这是一个不常见的滚动效果,如果你想要它看起来不错,我认为你需要使用条子。
实施
我所做的是复制粘贴SliverToBoxAdapter
并修改它。
特点:
- 首次加载小部件时 child 隐藏
- child活泼
- 当用户再次向下滚动时child隐藏
限制:
- 如果
CustomScrollView
到over-scroll的children不够,child
会一直可见,启动时会出现奇怪的滚动效果
class SliverHidedHeader extends SingleChildRenderObjectWidget {
const SliverHidedHeader({
Key? key,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverHidedHeader(context: context);
}
}
class RenderSliverHidedHeader extends RenderSliverSingleBoxAdapter {
RenderSliverHidedHeader({
required BuildContext context,
RenderBox? child,
}) : _context = context,
super(child: child);
/// Whether we need to apply a correction to the scroll
/// offset during the next layout
///
///
/// This is useful to avoid the viewport to jump when we
/// insert/remove the child.
///
/// If [showChild] is true, its an insert
/// If [showChild] is false, its a removal
bool _correctScrollOffsetNextLayout = true;
/// Whether [child] should be shown
///
///
/// This is used to hide the child when the user scrolls down
bool _showChild = true;
/// The context is used to get the [Scrollable]
BuildContext _context;
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
// Here are the few custom lines, which use [scrollOffsetCorrection]
// to remove the child size
//
// Note that this should only be called for correction linked with the
// insertion (NOT the removal)
if (_correctScrollOffsetNextLayout) {
geometry = SliverGeometry(scrollOffsetCorrection: childExtent);
_correctScrollOffsetNextLayout = false;
return;
}
// Subscribe a listener to the scroll notifier
// which will snap if needed
_manageSnapEffect(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
// Subscribe a listener to the scroll notifier
// which hide the child if needed
_manageInsertChild(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
paintOrigin: _showChild ? 0 : -paintedChildSize,
layoutExtent: _showChild ? null : 0,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow:
childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child!, constraints, geometry!);
}
/// Override to remove the listeners if needed
@override
void dispose() {
final _scrollPosition = Scrollable.of(_context)!.position;
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
super.dispose();
}
/// The listener which will snap if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedSnapScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for the snapping effect
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
_manageSnapEffect({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedSnapScrollNotifierListener = () => _snapScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier.addListener(_subscribedSnapScrollNotifierListener!);
}
/// Snaps if the user just stopped scrolling and the child is
/// partially visible
void _snapScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// Whether the user is currently idle (i.e not scrolling)
//
// We don't check _scrollPosition.activity.isScrolling or
// _scrollPosition.isScrollingNotifier.value because even if
// the user is holding still we don't want to start animating
//
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final isIdle = _scrollPosition.activity is IdleScrollActivity;
// Whether at least part of the child is visible
final isChildVisible = paintedChildSize > 0;
if (isIdle && isChildVisible) {
// If more than half is visible, snap to see everything
if (paintedChildSize >= childExtent / 2 && paintedChildSize != childExtent) {
_scrollPosition.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeOut,
);
}
// If less than half is visible, snap to hide
else if (paintedChildSize < childExtent / 2 && paintedChildSize != 0) {
_scrollPosition.animateTo(
childExtent,
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
}
/// The listener which will hide the child if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedInsertChildScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for inserting/removing the child if needed
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
void _manageInsertChild({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedInsertChildScrollNotifierListener = () => _insertChildScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier
.addListener(_subscribedInsertChildScrollNotifierListener!);
}
/// When [ScrollPosition.isScrollingNotifier] fires:
/// - If the viewport is at the top and the child is not visible,
/// ^ insert the child
/// - If the viewport is NOT at the top and the child is NOT visible,
/// ^ remove the child
void _insertChildScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
final isScrolling = _scrollPosition.isScrollingNotifier.value;
// If the user is still scrolling, do nothing
if (isScrolling) {
return;
}
final scrollOffset = _scrollPosition.pixels;
// If the viewport is at the top and the child is not visible,
// insert the child
//
// We use 0.1 as a small value in case the user is nearly scrolled
// all the way up
if (!_showChild && scrollOffset <= 0.1) {
_showChild = true;
_correctScrollOffsetNextLayout = true;
markNeedsLayout();
}
// There is sometimes an issue with [ClampingScrollPhysics] where
// the child is NOT shown but the scroll offset still includes [childExtent]
//
// There is no why to detect it but we always insert the child when all
// this conditions are united.
// This means that if a user as [ClampingScrollPhysics] and stops scrolling
// exactly at [childExtent], the child will be wrongfully inserted. However
// this seems a small price to pay to avoid the issue.
if (_scrollPosition.physics.containsScrollPhysicsOfType<ClampingScrollPhysics>()) {
if (!_showChild && scrollOffset == childExtent) {
_showChild = true;
markNeedsLayout();
}
}
// If the viewport is NOT at the top and the child is NOT visible,
// remove the child
if (_showChild && scrollOffset > childExtent) {
_showChild = false;
markNeedsLayout();
// We don't have to correct the scroll offset here, no idea why
}
}
}
/// An extension on [ScrollPhysics] to check if it or its
/// parent are the given [ScrollPhysics]
extension _ScrollPhysicsExtension on ScrollPhysics {
/// Check the type of this [ScrollPhysics] and its parents and return
/// true if any is of type [T]
bool containsScrollPhysicsOfType<T extends ScrollPhysics>() {
return this is T || (parent?.containsScrollPhysicsOfType<T>() ?? false);
}
}
如何使用
在你的条子列表的顶部使用它:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MaterialApp(home: MyStatefulWidget()));
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverHidedHeader(
child: Container(
child: Center(child: Text('SliverAppBar')),
height: 100,
color: Colors.redAccent,
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
),
);
}
}
其他资源
如果您想用不同的方法解决这个问题,请查看 pull_to_refresh 包。请注意,他们的代码非常复杂,因为他们还实现了 auto-hide 功能,但如果您有时间,值得一看。
解决限制
我不确定使用这种方法可以解决限制。问题是,出于性能原因,sliver 对它下面的那个一无所知,这意味着它甚至很难知道我们何时处于有问题的情况下,更不用说处理它了。