preface
Extended_nested_scroll_view was the first Flutter component I uploaded to pub.dev.
It has been nearly 3 years, 43 iterations, stable functionality, and official code synchronization.
And I’ve been preparing to reinvent it. Why? I have been exposed to Flutter for 3 years, and my cognition is different. I believe THAT if I were faced with the NestedScrollView problem now, I would be able to handle it better.
Note: The SliverPinnedToBoxAdapter used later is a component in extended_sliver, you treat it as SliverPersistentHeader(Pinned to the data side is true, MinExtent = maxExtent).
What is NestedScrollView
A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.
Interlink external scrolling (Header section) with internal scrolling (Body section). It won’t roll inside. Roll outside. The outside rolls away. The inside rolls away. So how does NestedScrollView do this?
NestedScrollView is actually a CustomScrollView, pseudo code.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
Copy the code
- OuterController is
CustomScrollView
的controller
In terms of the hierarchy, it’s external - Here we use
PrimaryScrollController
, thenbody
Any scrolling components inside are not customizedcontroller
In case, will be publicinnerController
.
To see why this is, first take a look at the primary property that every scrolling component has. If controller is null and it is a vertical method, it defaults to true.
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
Then in scroll_view.dart, if primary is true, get the primary ScrollController’s controller.
final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
returnbuildViewport(context, offset, axisDirection, slivers); });Copy the code
This explains why some students set a controller for the scroll component in the body and find that the internal and external scroll no longer interconnects.
Why extend the official one
Now that I understand what a NestedScrollView is, why should I extend the official component?
The Header contains multiple Pinned Sliver time issues
Analysis of the
So if you look at a graph, what do you think the end result of scrolling up is? The code is below.
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100 height '),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100 height '),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100 height '),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('Body: The contents inside$index, altitude 100 '),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
)
],
),
Copy the code
Well, yes, the first Item in the list will scroll under Header1. In fact, our usual requirement is that the list stay at the bottom of Header1.
Flutter officials have also noticed this problem and provided SliverOverlapAbsorber to deal with this problem.
SliverOverlapAbsorber
To the parcelPinned
为true
的Sliver
- Used in the body
SliverOverlapInjector
As placeholders - with
NestedScrollView._absorberHandle
To implement theSliverOverlapAbsorber
和SliverOverlapInjector
Information transmission.
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
// The listener calculates the height and will pass nestedscrollView._absorberHandle
// Self height tells SliverOverlapInjector
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100 height '),
height: 100,
color: Colors.red.withOpacity(0.4)))]; }, body: Builder( builder: (BuildContext context) {return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
slivers: <Widget>[
// Placeholder, receive information of SliverOverlapAbsorber
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => ListTile(title: Text('Item $index')),
childCount: 30"" "" "" "" }))); }Copy the code
If that sounds unclear to you, let me simplify and say it another way. Let’s also add a placeholder of 100. In practice, it is impossible to do this, because it would leave 100 empty Spaces at the top of the list when initializing.
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header0: 100 height '),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header1: Pinned height 100 '),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header2: 100 height '),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: <Widget>[
// I was equivalent to SliverOverlapAbsorber
Container(
height: 100,
),
Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('Body: The contents inside$index, altitude 100 '),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
],
),
)
],
),
Copy the code
Then the problem comes. If the NestedScrollView Header contains multiple slivers Pinned to true, then SliverOverlapAbsorber is unable to act as an Issue portal.
To solve
Let’s review what the NestedScrollView looks like, and you can see that this problem is related to the outerController. As shown in the previous simple demo, we can keep the list Pinned to the bottom of Header1 as long as we keep the outside scroll less than 100.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
Copy the code
maxScrollExtent
Let’s think again, what would affect the final distance of a rolling component?
The answer is ScrollPosition maxScrollExtent
Now that we know what the impact is, all we have to do is change the value at the right time, so how do we get the timing?
Place the following code
@override
double getmaxScrollExtent => _maxScrollExtent! ;double? _maxScrollExtent;
Copy the code
Change to the following code
@override
double getmaxScrollExtent => _maxScrollExtent! ;//double? _maxScrollExtent;
double? __maxScrollExtent;
double? get _maxScrollExtent => __maxScrollExtent;
set _maxScrollExtent(double? value) {
if (__maxScrollExtent != value) {
__maxScrollExtent = value;
}
}
Copy the code
So we can put a debug breakpoint in the set method and see when _maxScrollExtent is assigned.
Running the example yields the following Call Stack.
At this point, we should know that we can reset maxScrollExtent with the Override applyContentDimensions method
ScrollPosition
To override applyContentDimensions you need to know when the ScrollPosition was created, continue debugging, and put a breakpoint on the construction of the ScrollPosition.
graph TD
ScrollController.createScrollPosition --> ScrollPositionWithSingleContext --> ScrollPosition
Can see if it is not a specific ScrollPosition, we are using the default ScrollPositionWithSingleContext at ordinary times, And is created in the ScrollController createScrollPosition method.
Add the following code and add MyScrollController to the CustomScrollView in demo. Let’s run the demo again. Did we get what we wanted?
class MyScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
returnMyScrollPosition( physics: physics, context: context, initialPixels: initialScrollOffset, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); }}class MyScrollPosition extends ScrollPositionWithSingleContext {
MyScrollPosition({
@required ScrollPhysics physics,
@required ScrollContext context,
double initialPixels = 0.0.bool keepScrollOffset = true,
ScrollPosition oldPosition,
String debugLabel,
}) : super(
physics: physics,
context: context,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
initialPixels: initialPixels,
);
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
return super.applyContentDimensions(minScrollExtent, maxScrollExtent - 100); }}Copy the code
_NestedScrollPosition
Corresponding to NestedScrollView, you can add the following methods for _NestedScrollPosition.
PinnedHeaderSliverHeightBuilder callback is to obtain the Header of a total of what Pinned in silver.
- For the SliverAppbar, the final fixed height should include
Height of the status bar
(MediaQuery. Of (context). Padding. Top) andThe height of the navigation bar
(kToolbarHeight) - for
SliverPersistentHeader
(Pinned to true), the final fixed height should beminExtent
- If there are more than one such Sliver, it should be the sum of their final fixed heights.
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (debugLabel == 'outer'&& coordinator.pinnedHeaderSliverHeightBuilder ! =null) { maxScrollExtent = maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder! (a); maxScrollExtent = math.max(0.0, maxScrollExtent);
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
Copy the code
The problem of multiple list scrolling interactions in the Body
I’m sure you’ll want to, if you’re in a TabbarView or a PageView, you want to keep the scroll of the list when you’re switching. The use of AutomaticKeepAliveClientMixin, very simple.
But if you put a TabbarView or a PageView in the body of a NestedScrollView, and you scroll through one of the lists, you’ll see that the other lists change position as well. Issue portal
Analysis of the
Let’s start with the pseudo code for NestedScrollView. NestedScrollView interacts with the innerController and outerController.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
Copy the code
The innerController takes care of the Body, and then attaches the ScrollPosition of the list in the Body that the controller is not attached to, through the attach method.
When using the list cache, the original list will not be disposed when switching tabs and will not be detach from the Controller. InnerController. The positions will be more than one. The linkage calculation between outerController and innerController is based on positions. That’s what’s causing this problem.
The specific code is shown at github.com/flutter/flu…
if(innerDelta ! =0.0) {
for (final _NestedScrollPosition position in _innerPositions)
position.applyFullDragUpdate(innerDelta);
}
Copy the code
To solve
Looking at this question three years ago or now, the first impression is that it would be easy to just find the list that is currently displayed and just let it scroll.
True, but it just seems easy, after all, this issue has been open for 3 years.
The old plan
- in
ScrollPosition
attach
When to passcontext
Find the flag corresponding to this list, andTabbarView
orPageView
For comparison.
NestedScrollView (2) List scroll synchronization solution
- Determine the current by calculating the relative position of the list
According to
In the list.
You want to know the visible area, relative position, size of the Widget (juejin.cn)
In general,
- 1. The scheme is more accurate, but the usage is complicated.
- 2 scheme affected by animation, in some special cases will lead to incorrect calculation.
A new scheme
First, let’s prepare a demo to reproduce the problem.
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: [
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
children: <Widget>[
ListItem(
tag: 'Tab0',
),
ListItem(
tag: 'Tab1'"" "" "" "" "" ""class ListItem extends StatefulWidget {
const ListItem({
Key key,
this.tag,
}) : super(key: key);
final String tag;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
itemBuilder: (BuildContext buildContext, int index) =>
Center(child: Text('${widget.tag}---$index')),
itemCount: 1000,); }@override
bool get wantKeepAlive => true;
}
Copy the code
Drag
Now looking at the question, I’m thinking, what list did I scroll through and I don’t know?
Those of you who read the FlexGrid locking column in the previous post should know that dragging a list generates a Drag. So doesn’t the ScrollPosition with this Drag correspond to the list that’s being displayed?
In terms of code, let’s try logging,
Github.com/flutter/flu…
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
print(debugLabel);
return coordinator.drag(details, dragCancelCallback);
}
Copy the code
The ideal is great, but the reality is very thin, whether I scroll through the Header or the Body, I just print the Outer. That means all the gestures in the Body have been eaten??
Let’s open DevTools and look at the state of ScrollableState in the ListView. (Read the FlexGrid (Juejin.cn) for details on why you want to read this.)
So, what seems to be gestures is that there are no registered gestures in the Body.
Github.com/flutter/flu… In the setCanDrag method, we can see that only when canDrag equals false, we are not registering the gesture. There’s also the possibility that setCanDrag might never be called, and the default _gestureRecognizers is empty.
@override
@protected
void setCanDrag(bool canDrag) {
if(canDrag == _lastCanDrag && (! canDrag || widget.axis == _lastAxisDirection))return;
if(! canDrag) { _gestureRecognizers =const <Type, GestureRecognizerFactory>{};
// Cancel the active hold/drag (if any) because the gesture recognizers
// will soon be disposed by our RawGestureDetector, and we won't be
// receiving pointer up events to cancel the hold/drag.
_handleDragCancel();
} else {
switch (widget.axis) {
case Axis.vertical:
_gestureRecognizers = <Type, GestureRecognizerFactory>{ VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>( () => VerticalDragGestureRecognizer(), (VerticalDragGestureRecognizer instance) { instance .. onDown = _handleDragDown .. onStart = _handleDragStart .. onUpdate = _handleDragUpdate .. onEnd = _handleDragEnd .. onCancel = _handleDragCancel .. minFlingDistance = _physics? .minFlingDistance .. minFlingVelocity = _physics? .minFlingVelocity .. maxFlingVelocity = _physics? .maxFlingVelocity .. velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) .. dragStartBehavior = widget.dragStartBehavior; })};break;
case Axis.horizontal:
_gestureRecognizers = <Type, GestureRecognizerFactory>{ HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( () => HorizontalDragGestureRecognizer(), (HorizontalDragGestureRecognizer instance) { instance .. onDown = _handleDragDown .. onStart = _handleDragStart .. onUpdate = _handleDragUpdate .. onEnd = _handleDragEnd .. onCancel = _handleDragCancel .. minFlingDistance = _physics? .minFlingDistance .. minFlingVelocity = _physics? .minFlingVelocity .. maxFlingVelocity = _physics? .maxFlingVelocity .. velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context) .. dragStartBehavior = widget.dragStartBehavior; })};break;
}
}
_lastCanDrag = canDrag;
_lastAxisDirection = widget.axis;
if(_gestureDetectorKey.currentState ! =null) _gestureDetectorKey.currentState! .replaceGestureRecognizers(_gestureRecognizers); }Copy the code
Let’s put a breakpoint in the setCanDrag method and see when we call it.
- RenderViewport.performLayout
The performLayout method calculates the minimum and maximum value of the current ScrollPosition
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
Copy the code
- ScrollPosition.applyContentDimensions
Call the applyNewDimensions method
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent ! =null);
assert(maxScrollExtent ! =null);
assert(haveDimensions == (_lastMetrics ! =null));
if(! nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || ! nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || _didChangeViewportDimensionOrReceiveCorrection) {assert(minScrollExtent ! =null);
assert(maxScrollExtent ! =null);
assert(minScrollExtent <= maxScrollExtent);
_minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent;
final ScrollMetrics? currentMetrics = haveDimensions ? copyWith() : null;
_didChangeViewportDimensionOrReceiveCorrection = false;
_pendingDimensions = true;
if(haveDimensions && ! correctForNewDimensions(_lastMetrics! , currentMetrics!) ) {return false;
}
_haveDimensions = true;
}
assert(haveDimensions);
if (_pendingDimensions) {
applyNewDimensions();
_pendingDimensions = false;
}
assert(! _didChangeViewportDimensionOrReceiveCorrection,'Use correctForNewDimensions() (and return true) to change the scroll offset during applyContentDimensions().');
_lastMetrics = copyWith();
return true;
}
Copy the code
- ScrollPositionWithSingleContext.applyNewDimensions
No special definition, the default ScrollPosition ScrollPositionWithSingleContext. So who is context? Of course is ScrollableState
@override
void applyNewDimensions() {
super.applyNewDimensions();
context.setCanDrag(physics.shouldAcceptUserOffset(this));
}
Copy the code
Here mentioned a little, usually some students ask. Less than one screen of the list controller registration does not trigger or the NotificationListener listener does not trigger. Here, why physics. ShouldAcceptUserOffset (this) returns false. And our processing way is to set the physics to AlwaysScrollableScrollPhysics, shouldAcceptUserOffset put
AlwaysScrollableScrollPhysics shouldAcceptUserOffset method always returns true.
class AlwaysScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that always lets the user scroll.
const AlwaysScrollableScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);
@override
AlwaysScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) => true;
}
Copy the code
- ScrollableState.setCanDrag
And finally we get here, and we go to the canDrag and axis
_NestedScrollCoordinator
So, let’s go to the NestedScrollView code.
Github.com/flutter/flu…
@override
void applyNewDimensions() {
super.applyNewDimensions();
coordinator.updateCanDrag();
}
Copy the code
Here we see the call coordinator. UpdateCanDrag ().
So first of all, what is coordinator? It’s not hard to see, it’s coordinating the outerController and innerController.
class _NestedScrollCoordinator
implements ScrollActivityDelegate.ScrollHoldController {
_NestedScrollCoordinator(
this._state,
this._parent,
this._onHasScrolledBodyChanged,
this._floatHeaderSlivers,
) {
final doubleinitialScrollOffset = _parent? .initialScrollOffset ??0.0;
_outerController = _NestedScrollController(
this,
initialScrollOffset: initialScrollOffset,
debugLabel: 'outer',); _innerController = _NestedScrollController(this,
initialScrollOffset: 0.0,
debugLabel: 'inner',); }Copy the code
So let’s see what’s going on in the updateCanDrag method.
void updateCanDrag() {
if(! _outerPosition! .haveDimensions)return;
double maxInnerExtent = 0.0;
for (final _NestedScrollPosition position in _innerPositions) {
if(! position.haveDimensions)return;
maxInnerExtent = math.max(
maxInnerExtent,
position.maxScrollExtent - position.minScrollExtent,
);
}
// _NestedScrollPosition.updateCanDrag_outerPosition! .updateCanDrag(maxInnerExtent); }Copy the code
_NestedScrollPosition.updateCanDrag
void updateCanDrag(double totalExtent) {
// Call the setCanDrag method of ScrollableStatecontext.setCanDrag(totalExtent > (viewportDimension - maxScrollExtent) || minScrollExtent ! = maxScrollExtent); }Copy the code
Now that we know why, let’s try to change it.
- Modify the
_NestedScrollCoordinator.updateCanDrag
As the following:
void updateCanDrag({_NestedScrollPosition? position}) {
double maxInnerExtent = 0.0;
if(position ! =null && position.debugLabel == 'inner') {
if(position.haveDimensions) { maxInnerExtent = math.max( maxInnerExtent, position.maxScrollExtent - position.minScrollExtent, ); position.updateCanDrag(maxInnerExtent); }}if(! _outerPosition! .haveDimensions) {return;
}
for (final _NestedScrollPosition position in _innerPositions) {
if(! position.haveDimensions) {return; } maxInnerExtent = math.max( maxInnerExtent, position.maxScrollExtent - position.minScrollExtent, ); } _outerPosition! .updateCanDrag(maxInnerExtent); }Copy the code
- Modify the
_NestedScrollPosition.drag
The method is as follows:
bool _isActived = false;
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
_isActived = true;
return coordinator.drag(details, () {
dragCancelCallback();
_isActived = false;
});
}
/// Whether is actived now
bool get isActived {
return _isActived;
}
Copy the code
- Modify the
_NestedScrollCoordinator._innerPositions
As the following:
可迭代<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final 可迭代<_NestedScrollPosition> actived = _innerController
.nestedPositions
.where((_NestedScrollPosition element) => element.isActived);
print('${actived.length}');
if (actived.isNotEmpty) return actived;
}
return _innerController.nestedPositions;
}
Copy the code
Now run demo again, scroll through the list and see if it’s 👌? The results were disappointing.
- Even though we are
drag
When you’re doing it, you can actually tell who’s active, but when the finger goes up and starts to slide,dragCancelCallback
The callback has been triggered,_isActived
Has been set tofalse
。 - When we operate
PageView
The yellow area at the top (normally, this part would beTabbar
), because it is not done on the listdrag
Operation, so this timeactived
The list is 0.
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: [
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
children: <Widget>[
ListItem(
tag: 'Tab0',
),
ListItem(
tag: 'Tab1'"" "" "" "" "" ""Copy the code
Whether or not visible
The problem seems to be the old one, how to tell if a view is visible.
So first of all, the most straightforward thing we can get here is _NestedScrollPosition, so let’s see what this guy has to work with.
At first glance, you see a context(ScrollableState), which is a ScrollContext, and ScrollableState implements ScrollContext.
/// Where the scrolling is taking place.
///
/// Typically implemented by [ScrollableState].
final ScrollContext context;
Copy the code
At a glance at the ScrollContext, notificationContext and storageContext should be related.
abstract class ScrollContext {
/// The [BuildContext] that should be used when dispatching
/// [ScrollNotification]s.
///
/// This context is typically different that the context of the scrollable
/// widget itself. For example, [Scrollable] uses a context outside the
/// [Viewport] but inside the widgets created by
/// [ScrollBehavior.buildOverscrollIndicator] and [ScrollBehavior.buildScrollbar].
BuildContext? get notificationContext;
/// The [BuildContext] that should be used when searching for a [PageStorage].
///
/// This context is typically the context of the scrollable widget itself. In
/// particular, it should involve any [GlobalKey]s that are dynamically
/// created as part of creating the scrolling widget, since those would be
/// different each time the widget is created.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
BuildContext get storageContext;
/// A [TickerProvider] to use when animating the scroll position.
TickerProvider get vsync;
/// The direction in which the widget scrolls.
AxisDirection get axisDirection;
/// Whether the contents of the widget should ignore [PointerEvent] inputs.
///
/// Setting this value to true prevents the use from interacting with the
/// contents of the widget with pointer events. The widget itself is still
/// interactive.
///
/// For example, if the scroll position is being driven by an animation, it
/// might be appropriate to set this value to ignore pointer events to
/// prevent the user from accidentally interacting with the contents of the
/// widget as it animates. The user will still be able to touch the widget,
/// potentially stopping the animation.
void setIgnorePointer(bool value);
/// Whether the user can drag the widget, for example to initiate a scroll.
void setCanDrag(bool value);
/// Set the [SemanticsAction]s that should be expose to the semantics tree.
void setSemanticsActions(Set<SemanticsAction> actions);
/// Called by the [ScrollPosition] whenever scrolling ends to persist the
/// provided scroll `offset` for state restoration purposes.
///
/// The [ScrollContext] may pass the value back to a [ScrollPosition] by
/// calling [ScrollPosition.restoreOffset] at a later point in time or after
/// the application has restarted to restore the scroll offset.
void saveOffset(double offset);
}
Copy the code
Look again at the implementation in ScrollableState.
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin.RestorationMixin
implements ScrollContext {
@override
BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
@override
BuildContext get storageContext => context;
}
Copy the code
storageContext
Is actually
ScrollableState context.
notificationContext
Look for references and you can see.
Sure enough, who triggered the event, of course is ScrollableState inside the RawGestureDetector.
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollNotification) {
/// The build context of the widget that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
// final BuildContext? context;
print(scrollNotification.context);
return false; });Copy the code
Finally, we have to work on storageContext. In the # Flutter Lifetime Enemy of Silver series we have been brushing up on silver-related knowledge. The element currently displayed in TabbarView or PageView should be unique in RenderSliverFillViewport (unless you set viewportFraction to less than 1). We can go up to RenderSliverFillViewport with the Context of _NestedScrollPosition, Let’s see if the Child in RenderSliverFillViewport is the Context of _NestedScrollPosition.
- Modify the
_NestedScrollCoordinator._innerPositions
As the following:
可迭代<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final 可迭代<_NestedScrollPosition> actived = _innerController
.nestedPositions
.where((_NestedScrollPosition element) => element.isActived);
if (actived.isEmpty) {
for (final _NestedScrollPosition scrollPosition
in _innerController.nestedPositions) {
final RenderObject? renderObject =
scrollPosition.context.storageContext.findRenderObject();
if (renderObject == null| |! renderObject.attached) {continue;
}
if (renderObjectIsVisible(renderObject, Axis.horizontal)) {
return<_NestedScrollPosition>[scrollPosition]; }}return _innerController.nestedPositions;
}
return actived;
} else {
return_innerController.nestedPositions; }}Copy the code
- in
renderObjectIsVisible
Method to see if theTabbarView
orPageView
In, and itsaxis
与ScrollPosition
的axis
Mutually perpendicular. If you have one, useRenderViewport
The currentchild
callchildIsVisible
Method to verify whether theScrollPosition
The correspondingRenderObject
. Notice, it’s calledrenderObjectIsVisible
Because there may be nested (multi-level)TabbarView
orPageView
.
bool renderObjectIsVisible(RenderObject renderObject, Axis axis) {
final RenderViewport? parent = findParentRenderViewport(renderObject);
if(parent ! =null && parent.axis == axis) {
for (final RenderSliver childrenInPaint
in parent.childrenInHitTestOrder) {
returnchildIsVisible(childrenInPaint, renderObject) && renderObjectIsVisible(parent, axis); }}return true;
}
Copy the code
- Looking for upward
RenderViewport
, we onlyNestedScrollView
的body
In the search, until_ExtendedRenderSliverFillRemainingWithScrollable
.
RenderViewport? findParentRenderViewport(RenderObject? object) {
if (object == null) {
return null;
}
object = object.parent asRenderObject? ;while(object ! =null) {
// Look only in the body
if (object is _ExtendedRenderSliverFillRemainingWithScrollable) {
return null;
}
if (object is RenderViewport) {
return object;
}
object = object.parent asRenderObject? ; }return null;
}
Copy the code
- call
visitChildrenForSemantics
traversechildren
See if you can find itScrollPosition
The correspondingRenderObject
/// Return whether renderObject is visible in parent
bool childIsVisible(
RenderObject parent,
RenderObject renderObject,
) {
bool visible = false;
// The implementation has to return the children in paint order skipping all
// children that are not semantically relevant (e.g. because they are
// invisible).
parent.visitChildrenForSemantics((RenderObject child) {
if (renderObject == child) {
visible = true;
} else{ visible = childIsVisible(child, renderObject); }});return visible;
}
Copy the code
Any other plans
If you just want the list to stay in place, you can use PageStorageKey to keep the scrolling list in place. In this case, when the TabbarView or PageView is switched, ScrollableState is disposed and detach the ScrollPosition from the innerController.
@override
void dispose() {
if(widget.controller ! =null) { widget.controller! .detach(position); }else{ _fallbackScrollController? .detach(position); _fallbackScrollController? .dispose(); } position.dispose(); _persistedScrollOffset.dispose();super.dispose();
}
Copy the code
And you need to do is in a layer, use such as provider | Flutter Package (Flutter – IO. Cn) to keep a list of data or any other state.
NestedScrollView(
headerSliverBuilder: (
BuildContext buildContext,
bool innerBoxIsScrolled,
) =>
<Widget>[
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 200,
),
)
],
body: Column(
children: <Widget>[
Container(
color: Colors.yellow,
height: 200,
),
Expanded(
child: PageView(
/ / controller: PageController (viewportFraction: 0.8),
children: <Widget>[
ListView.builder(
//store Page state
key: const PageStorageKey<String> ('Tab0'),
physics: const ClampingScrollPhysics(),
itemBuilder: (BuildContext c, int i) {
return Container(
alignment: Alignment.center,
height: 60.0,
child:
Text(const Key('Tab0').toString() + ': ListView$i')); }, itemCount:50,
),
ListView.builder(
//store Page state
key: const PageStorageKey<String> ('Tab1'),
physics: const ClampingScrollPhysics(),
itemBuilder: (BuildContext c, int i) {
return Container(
alignment: Alignment.center,
height: 60.0,
child:
Text(const Key('Tab1').toString() + ': ListView$i')); }, itemCount:50"" "" "" "" "" ""Copy the code
Refactor the code
Physical strength live
Within 3 years, I had written 18 Flutter component libraries and 3 Flutter related tools.
-
like_button | Flutter Package (flutter-io.cn)
-
extended_image_library | Flutter Package (pub.dev)
-
extended_nested_scroll_view | Flutter Package (flutter-io.cn)
-
extended_text | Flutter Package (flutter-io.cn)
-
extended_text_field | Flutter Package (flutter-io.cn)
-
extended_image | Flutter Package (flutter-io.cn)
-
extended_sliver | Flutter Package (flutter-io.cn)
-
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
-
waterfall_flow | Flutter Package (flutter-io.cn)
-
loading_more_list | Flutter Package (flutter-io.cn)
-
extended_tabs | Flutter Package (flutter-io.cn)
-
http_client_helper | Dart Package (flutter-io.cn)
-
extended_text_library | Flutter Package (flutter-io.cn)
-
extended_list | Flutter Package (flutter-io.cn)
-
extended_list_library | Flutter Package (flutter-io.cn)
-
ff_annotation_route_library | Flutter Package (flutter-io.cn)
-
loading_more_list_library | Dart Package (flutter-io.cn)
-
ff_annotation_route | Dart Package (flutter-io.cn)
-
ff_annotation_route_core | Dart Package (flutter-io.cn)
-
flex_grid | Flutter Package (flutter-io.cn)
-
assets_generator | Dart Package (flutter-io.cn)
-
Fluttercandies/JsonToDart: The tool to convert a json to dart code, support for Windows, Mac, Web. (github.com)
Every time Stable is released officially, it’s a physical task for me. In particular, for extended_nested_scroll_view, extended_TEXT, extended_TEXt_Field, and Extended_image, merge code is not just manual work, It also requires careful understanding of the changes.
remodeling
This time, I took the opportunity to change the whole structure.
-
SRC /extended_nested_scroll_view.dart is the official source code with some necessary changes. Such as adding parameters, replacing extension types. Maintain the structure and format of the official source code to the greatest extent possible.
-
SRC /extended_nested_scroll_view_part.dart is part of the code that extends the functions of the official component. Add the following three extension classes to implement our corresponding extension methods.
class _ExtendedNestedScrollCoordinator extends _NestedScrollCoordinator
Copy the code
class _ExtendedNestedScrollController extends _NestedScrollController
Copy the code
class _ExtendedNestedScrollPosition extends _NestedScrollPosition
Copy the code
Finally, modify the initialization code at SRC /extended_nested_scroll_view.dart. In the future, I only need to merge the official code with SRC /extended_nested_scroll_view.dart.
_NestedScrollCoordinator? _coordinator;
@override
void initState() {
super.initState();
_coordinator = _ExtendedNestedScrollCoordinator(
this,
widget.controller,
_handleHasScrolledBodyChanged,
widget.floatHeaderSlivers,
widget.pinnedHeaderSliverHeightBuilder,
widget.onlyOneScrollInBody,
widget.scrollDirection,
);
}
Copy the code
Small candy 🍬
If you’re here, you’ve read 6,000 words, thank you. Send some skills, hope can be helpful to you.
CustomScrollView center
Customscrollview. center is a property I actually talked about a long time ago, the Lifetime Enemy of Flutter Sliver (ScrollView) (juejin.cn). In a nutshell:
center
Is the place to start drawing, both drawing inzero scroll offset
Where, forward is negative, backward is positive.center
Before theSliver
It’s drawn in reverse order.
Take the following code for example, what do you think the final result will look like?
CustomScrollView(
center: key,
slivers: <Widget>[
SliverList(),
SliverGrid(key:key),
]
)
Copy the code
The renderings are shown below, with the SliverGrid drawn at the start position. You can scroll down and at this point the SliverList above will be displayed.
Customscrollview. anchor controls the location of the center. 0 = leading of viewport, 1 = Trailing, so this is the proportion of the viewport’s vertical (horizontal) height. For example, if it is 0.5, the place to draw the SliverGrid will be in the middle of the viewport.
With these two attributes, we can create some interesting effects.
Chat list
Flutter_instant_messaging /main.dart at master · FlutterCandies/flutter_instant_Messaging (github.com) Dart at main · FlutterCandies /flutter_challenges (github.com) Unified maintenance.
Ios reverse album
Dart at main · FlutterCandies /flutter_challenges (github.com)
Originated in the horse the teacher give wechat_assets_picker | Flutter Package (Flutter – IO. Cn) demand (balance payment “), to make photo album to check the effect in the same way as Ios native. Ios design is really different, learning (Chao).
Betta home page scrolling effect
Dart at main · FlutterCandies /flutter_challenges (github.com) Here is the code for flutter_challenges.
We have to mention again, NotificationListener, which is the listener of the Notification. With Notification.dispatch, notifications are passed up the current node (BuildContext) as a bubble, and you can use NotificationListener at the parent node to receive notifications. A common use of Flutter is the ScrollNotification, In addition to SizeChangedLayoutNotification, KeepAliveNotification, LayoutChangedNotification etc. You can also define a notification yourself.
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return OKToast(
child: MaterialApp(
title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ), ); }}class MyHomePage extends StatefulWidget {
const MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return NotificationListener<TextNotification>(
onNotification: (TextNotification notification) {
showToast('The star received notice:${notification.text}');
return true;
},
child: Scaffold(
appBar: AppBar(),
body: NotificationListener<TextNotification>(
onNotification: (TextNotification notification) {
showToast('Dabao received a notice:${notification.text}');
// If this is true, the star will not receive any information.
return false;
},
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
TextNotification('Off duty! ').. dispatch(context); }, child: Text('am I')); },),)),); }}class TextNotification extends Notification {
TextNotification(this.text);
final String text;
}
Copy the code
The usual pull-down refresh and pull-up load of more components can also be done by listening for ScrollNotification.
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
loading_more_list | Flutter Package (flutter-io.cn)
ScrollPosition.ensureVisible
To do this, most people should be able to do it. In fact, it is necessary to find the corresponding RenderAbstractViewport through the RenderObject of the current object, and then obtain the relative position through the getOffsetToReveal method.
/// Animates the position such that the given object is as visible as possible
/// by just scrolling this position.
///
/// See also:
///
/// * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is
/// applied, and the way the given `object` is aligned.
Future<void> ensureVisible(
RenderObject object, {
double alignment = 0.0.Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
}) {
assert(alignmentPolicy ! =null);
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport ! =null);
double target;
switch (alignmentPolicy) {
case ScrollPositionAlignmentPolicy.explicit:
target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent) as double;
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
target = viewport.getOffsetToReveal(object, 1.0).offset.clamp(minScrollExtent, maxScrollExtent) as double;
if (target < pixels) {
target = pixels;
}
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
target = viewport.getOffsetToReveal(object, 0.0).offset.clamp(minScrollExtent, maxScrollExtent) as double;
if (target > pixels) {
target = pixels;
}
break;
}
if (target == pixels)
return Future<void>.value();
if (duration == Duration.zero) {
jumpTo(target);
return Future<void>.value();
}
return animateTo(target, duration: duration, curve: curve);
}
Copy the code
EnsureVisible (github.com)
One question, guess what happens when you click on the button I jump to the top where I’m fixed.
Flutter challenge
I have mentioned to nuggets before whether we can add “you ask me answer/you set me challenge” module to increase communication between programmers, programmers are not willing to lose, should be 🔥? It’s exciting to think about. I created a new FlutterChallenges QQ group 321954965 to communicate; A repository to discuss and store these little challenge codes. Collect some examples of practical scenes that are difficult, not just show techniques. Enter the group needs to pass the recommendation or verification, welcome to toss about their own children’s shoes.
Valentine’s day + Chinese Valentine’s Day is it a coincidence?
Meituan Ele. me order page
Requirements:
- Left and right 2 lists can linkage, the whole home page scrolling linkage
- Universal, can be components
So if you look at NestedScrollView, I think there’s a way to do this.
Increase click area
Increasing the click area is something you would normally encounter, so how do you do that in Flutter?
Original code address: Increase click area (github.com)
For testing convenience, please add the OkToast of the Financial Dragon in pubspec.yaml.
oktoast: any
Copy the code
Requirements:
- Don’t change the entire structure and size.
- Don’t directly
Stack
The wholeItem
Rewrite. - Versatility.
The resulting effect is as follows, with the expanded range theoretically set at will.
conclusion
The first time to nugget dry burst, only part of the article code are moved togist.github.com/zmtzawqlp(Suddenly think of some say what swastika article title party is not a little slap in the face ah, I see close to 9000 can not write again). This is a lot of writing, just write what comes to mind. Whatever the technology, you have to go deep to understand it. Maintaining open source components can be tiring. But it will constantly force you to learn, and in the process of constantly updating and iterating, you will learn some knowledge that is not easy to access. Every pound of sand makes a towerFlutter
Source code is no longer a dream.
loveFlutter
Love,candy
Welcome aboardFlutter CandiesTogether they produce cute little candies called FlutterQQ group: 181398081
Finally put the Flutter on the bucket. It smells good.