предисловие
extended_nested_scroll_viewмоя первая загрузка вpub.devКомпонент флаттера.
Мюгающий глаз почти три года, прошел через 43 версиях итераций, стабильна, официальная синхронизация кода.
И я недавно готовился к рефакторингу. Как бы это сказать, я общаюсь с Флаттером уже 3 года, и мое познание тоже отличается от оригинала. Я считаю, что если я столкнусь с этим сейчасNestedScrollView
проблема, с которой я должен справиться лучше.
Примечание: используется позжеSliverPinnedToBoxAdapter
даextended_sliverВнутри компонента вы рассматриваете его какSliverPersistentHeader
(Закреплено верно, minExtent = maxExtent) просто отлично.
Что такое NestedScrollView
A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.
Свяжите внешнюю прокрутку (часть заголовка) и внутреннюю прокрутку (часть тела). Не могу катиться внутри, катайтесь снаружи. Прокрутка снаружи исчезла, прокрутите внутрь. ТакNestedScrollView
Как это делается?
NestedScrollView
На самом делеCustomScrollView
, псевдокод ниже.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
- внешнийконтроллер
CustomScrollView
изcontroller
, с иерархической точки зрения, является внешним - используется здесь
PrimaryScrollController
,Такbody
Любые компоненты прокрутки внутри, без кастомныхcontroller
, будет общедоступнымinnerController
.
Что касается того, почему это так, сначала посмотрите на свойства, которые имеет каждый компонент прокрутки.primary, значение которого по умолчанию равно true, если контроллер имеет значение null и это вертикальный метод.
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
затем вscroll_view.dart, еслиprimary
это правда, просто получитьPrimaryScrollController
контроллер.
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) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
Это также объясняет, почему некоторые учащиеся настраивают контроллер для компонента прокрутки в теле и обнаруживают, что внутренняя и внешняя прокрутка больше не связаны.
Зачем продлевать официальную
пониматьNestedScrollView
Что, тогда зачем мне продлевать официальный компонент?
Проблемы, когда заголовок содержит несколько закрепленных осколков
анализировать
Сначала посмотрите на картинку, как вы думаете, каков конечный результат прокрутки списка вверх? Код ниже.
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100高度'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100高度'),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: 100高度'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('body: 里面的内容$index,高度100'),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
)
],
),
Что ж, правильно, первый пункт списка прокручивается ниже Header1. Но на самом деле наше обычное требование состоит в том, чтобы список оставался внизу заголовка Header1.
Чиновники Flutter также заметили эту проблему и предоставилиSliverOverlapAbsorber
SliverOverlapInjector
чтобы справиться с этой проблемой,
-
SliverOverlapAbsorber
обернутьPinned
дляtrue
изSliver
- использовать в теле
SliverOverlapInjector
занять место - использовать
NestedScrollView._absorberHandle
реализоватьSliverOverlapAbsorber
а такжеSliverOverlapInjector
передача информации.
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
// 监听计算高度,并且通过 NestedScrollView._absorberHandle 将
// 自身的高度 告诉 SliverOverlapInjector
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header: Pinned 100高度'),
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>[
// 占位,接收 SliverOverlapAbsorber 的信息
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => ListTile(title: Text('Item $index')),
childCount: 30,
),
),
],
);
}
)
)
);
}
Если вы считаете этот метод непонятным, то я упрощу его и выражу по-другому. Мы также добавляем заполнитель 100. Однако на практике это сделать невозможно, в результате чего при инициализации появится вакансия 100 над списком.
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header0: 100高度'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverPinnedToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header1: Pinned 100高度'),
height: 100,
color: Colors.red.withOpacity(0.4),
),
),
SliverToBoxAdapter(
child: Container(
alignment: Alignment.center,
child: Text('Header2: 100高度'),
height: 100,
color: Colors.yellow.withOpacity(0.4),
),
),
SliverFillRemaining(
child: Column(
children: <Widget>[
// 我相当于 SliverOverlapAbsorber
Container(
height: 100,
),
Column(
children: List.generate(
100,
(index) => Container(
alignment: Alignment.topCenter,
child: Text('body: 里面的内容$index,高度100'),
height: 100,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.4),
border: Border.all(
color: Colors.black,
)),
)),
),
],
),
)
],
),
Вопрос в том, еслиNestedScrollView
изHeader
содержит несколькоPinned
дляtrue
изSliver
, ТакSliverOverlapAbsorber
будет бессилен,Выпуск портал.
решить
Давайте рассмотримNestedScrollView
Как это выглядит, видно, что эта проблема должна быть связана сouterController
Есть отношения. Ссылаясь на предыдущую простую демонстрацию, пока мы уменьшаем внешнюю прокрутку на 100, мы можем сделать так, чтобы список оставался внизу закрепленного заголовка 1.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
maxScrollExtent
Давайте еще раз подумаем, что повлияет на конечное расстояние прокрутки компонента прокрутки?
Зная, что влияет, все, что нам нужно сделать, это изменить это значение в нужное время, так как же получить время?
поместите следующий код
@override
double get maxScrollExtent => _maxScrollExtent!;
double? _maxScrollExtent;
Измените на следующий код
@override
double get maxScrollExtent => _maxScrollExtent!;
//double? _maxScrollExtent;
double? __maxScrollExtent;
double? get _maxScrollExtent => __maxScrollExtent;
set _maxScrollExtent(double? value) {
if (__maxScrollExtent != value) {
__maxScrollExtent = value;
}
}
Таким образом, мы можем поставить точку останова отладки в методе set, чтобы увидеть, когда он_maxScrollExtent
назначенный.
Запуск примера дает следующееCall Stack
.
Смотрите здесь, мы должны знать, что вы можете пройти переопределениеapplyContentDimensions
метод, чтобы сброситьmaxScrollExtent
ScrollPosition
хочу переопределитьapplyContentDimensions
просто знаюScrollPosition
Когда он был создан, продолжите отладку и нажмите точку остановаScrollPosition
структура выше.
можно посмотреть, если не конкретноScrollPosition
, мы обычно используем значение по умолчаниюScrollPositionWithSingleContext
, И вScrollController
изcreateScrollPosition
метод создан.
Добавьте следующий код и передайте его демоCustomScrollView
Добавить кcontroller
дляMyScrollController
, снова запускаем демо, получаем ли мы желаемый эффект?
class MyScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
return MyScrollPosition(
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);
}
}
_NestedScrollPosition
соответствуетNestedScrollView
, возможно_NestedScrollPositionДобавьте следующий метод.
pinnedHeaderSliverHeightBuilder
Обратный вызов должен получитьHeader
Каковы итогиPinned
изSliver
.
- Для SliverAppbar окончательная фиксированная высота должна включать
状态栏的高度
(MediaQuery.of(context).padding.top) и导航栏的高度
(kToolbarHeight) - для
SliverPersistentHeader
( Закреплено верно ), окончательная закрепленная высота должна бытьminExtent
- Если таких Щепок несколько, это должна быть сумма их окончательных фиксированных высот.
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (debugLabel == 'outer' &&
coordinator.pinnedHeaderSliverHeightBuilder != null) {
maxScrollExtent =
maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder!();
maxScrollExtent = math.max(0.0, maxScrollExtent);
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
Проблема прокрутки нескольких списков, влияющих друг на друга в Body
У каждого должна быть эта потребность,TabbarView
илиPageView
В списке положение прокрутки списка должно сохраняться при переключении. это использованиеAutomaticKeepAliveClientMixin
,очень простой.
Но если положитьTabbarView
илиPageView
помещатьNestedScrollView
изbody
Внутри, если вы прокрутите один из списков, вы обнаружите, что другие списки также изменят свои позиции.Портал выпуска
анализировать
Первый взглядNestedScrollView
псевдокод.NestedScrollView
Причина, по которой он может быть связан внутренне и внешне, заключается в том, чтоouterController
а такжеinnerController
связь.
CustomScrollView(
controller: outerController,
slivers: [
...<Widget>[Header1,Header2],
SliverFillRemaining()(
child: PrimaryScrollController(
controller: innerController,
child: body,
),
),
],
);
innerController
Быть ответственным заBody
,БудуBody
Список контроллеров, которые не были установлены вScrollPosition
пройти черезattach
метод, загруженный в.
При использовании кеша списка при переключении вкладок исходный список не будет
dispose
, он не будет вызываться из контроллераdetach
. innerController.positions будет больше одного. а такжеouterController
а такжеinnerController
Расчет связи основан на позициях. Вот что вызывает эту проблему.
Конкретный код отражен вGitHub.com/flutter/Appendix…
if (innerDelta != 0.0) {
for (final _NestedScrollPosition position in _innerPositions)
position.applyFullDragUpdate(innerDelta);
}
решить
Независимо от того, смотрю ли я на этот вопрос 3 года назад или сейчас, мое первое впечатление, что мне просто нужно найти текущий
显示
, просто дайте ему прокрутить, не так ли просто?
Верно, но это только кажется легким, ведь этот вопрос открыт уже 3 года.
старый план
- существует
ScrollPosition
attach
пройти, когдаcontext
Найдите флаг, соответствующий этому списку, и следуйтеTabbarView
илиPageView
Ассоциация индекса для сравнения.
Flutter расширяет NestedScrollView (2) Решение для синхронизации прокрутки списка (juejin.cn)
- Определить текущую позицию, вычислив относительную позицию списка
显示
список.
В основном,
- 1 Схема более точная, но использование более громоздкое.
- 2 На схему влияет анимация, что в некоторых частных случаях может привести к некорректному расчету.
Новый план
Во-первых, давайте подготовим демо, чтобы воспроизвести проблему.
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;
}
Drag
Глядя сейчас на вопрос, думаю, какой список пролистала сама, не знаю? ?
прочитать предыдущую статьюFlutter блокирует строку и столбец FlexGrid — самородки (juejin.cn), вы должны знать, что когда вы перетаскиваете список,Drag
из. тогда есть этоDrag
изScrollPosition
不就对应正在显示的列表吗? ?
Конкретно для кода, давайте попробуем войти, чтобы увидеть,
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
print(debugLabel);
return coordinator.drag(details, dragCancelCallback);
}
Идеал хорош, но реальность худая, что бы я ни каталсяHeader
ещеBody
, все просто распечататьouter
. Значит ли это, что все жесты в Теле съедены? ?
Не волнуйтесь, мы откроемDevTools
,посмотриListView
внутриScrollableStateположение дел. (По конкретным причинам вы можете прочитать это здесь.Flutter FlexGrid с заблокированной строкой и столбцом (juejin.cn))
Ха-ха,gestures
на самом деле дляnone
,то естьBody
Внутри нет прописанных жестов.
GitHub.com/flutter/Appendix… setCanDrag
метод, мы видим, что толькоcanDrag
равныйfalse
, жесты мы не регистрировали. Конечно, есть также возможность,setCanDrag
Может быть, он не был вызван, по умолчанию_gestureRecognizers
пусто.
@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);
}
мы вsetCanDrag
Нажмите точку останова в методе, чтобы увидеть, когда будет сделан вызов.
- RenderViewport.performLayout
Вычислить ток в методе PerformLayoutScrollPosition
минимум и максимум
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
- ScrollPosition.applyContentDimensions
передачаapplyNewDimensions
метод
@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;
}
- ScrollPositionWithSingleContext.applyNewDimensions
Если специально не определено, по умолчаниюScrollPosition
обеScrollPositionWithSingleContext
.context
Кто это?
конечноScrollableState
@override
void applyNewDimensions() {
super.applyNewDimensions();
context.setCanDrag(physics.shouldAcceptUserOffset(this));
}
Я упомянул это здесь, и меня обычно спрашивают мои одноклассники. Список менее чем одной регистрации контроллера экрана не запускается или мониторинг NotificationListener не запускается. Вот почему физика.shouldAcceptUserOffset(this) возвращает
false
. И наше решение состоит в том, чтобы настроить физику наAlwaysScrollableScrollPhysics
, следует поставитьAcceptUserOffset
AlwaysScrollableScrollPhysics
изshouldAcceptUserOffset
метод всегда возвращает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;
}
- ScrollableState.setCanDrag
Наконец добрался сюда, иди поcanDrag
а такжеaxis
(Горизонтальный и вертикальный)
_NestedScrollCoordinator
Тогда впередNestedScrollView
Посмотрите в коде.
@override
void applyNewDimensions() {
super.applyNewDimensions();
coordinator.updateCanDrag();
}
Здесь мы видим вызовcoordinator.updateCanDrag()
.
Сначала посмотримcoordinator
что это такое? Нетрудно увидеть, используется для координацииouterController
а такжеinnerController
из.
class _NestedScrollCoordinator
implements ScrollActivityDelegate, ScrollHoldController {
_NestedScrollCoordinator(
this._state,
this._parent,
this._onHasScrolledBodyChanged,
this._floatHeaderSlivers,
) {
final double initialScrollOffset = _parent?.initialScrollOffset ?? 0.0;
_outerController = _NestedScrollController(
this,
initialScrollOffset: initialScrollOffset,
debugLabel: 'outer',
);
_innerController = _NestedScrollController(
this,
initialScrollOffset: 0.0,
debugLabel: 'inner',
);
}
тогда посмотримupdateCanDrag
Что делается в методе.
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);
}
_NestedScrollPosition.updateCanDrag
void updateCanDrag(double totalExtent) {
// 调用 ScrollableState 的 setCanDrag 方法
context.setCanDrag(totalExtent > (viewportDimension - maxScrollExtent) ||
minScrollExtent != maxScrollExtent);
}
Узнав причину, попробуем ее изменить.
- Исправлять
_NestedScrollCoordinator.updateCanDrag
для следующего:
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);
}
- Исправлять
_NestedScrollPosition.drag
Метод заключается в следующем:
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;
}
- Исправлять
_NestedScrollCoordinator._innerPositions
для следующего:
Iterable<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final Iterable<_NestedScrollPosition> actived = _innerController
.nestedPositions
.where((_NestedScrollPosition element) => element.isActived);
print('${actived.length}');
if (actived.isNotEmpty) return actived;
}
return _innerController.nestedPositions;
}
Теперь снова запустите демо, прокрутите список после переключения, это 👌? Результат разочаровал.
- Хотя мы
drag
При работе действительно можно определить, кто активен, но когда палец вверх, и начинается инерционное слайд,dragCancelCallback
Обратный вызов сработал,_isActived
был установлен наfalse
. - когда мы работаем
PageView
Когда верхняя желтая область (обычно эта часть можетTabbar
), так как он не выполняется в спискеdrag
операция, так что на этот разactived
В списке 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',
),
],
),
),
],
),
),
Это видно
Проблема похоже снова ушла на старое место, как судить вид виден.
Прежде всего, самое прямое, что мы можем здесь получить, это_NestedScrollPosition
, давайте посмотрим, что у этого парня есть что использовать.
увидел с первого взглядаcontext(ScrollableState)
,ЯвляетсяScrollContext
,а такжеScrollableState
ДостигнутоScrollContext
.
/// Where the scrolling is taking place.
///
/// Typically implemented by [ScrollableState].
final ScrollContext context;
посмотриScrollContext
,notificationContext
а такжеstorageContext
должно быть актуальным.
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);
}
посмотри сноваScrollableState
реализация в.
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
implements ScrollContext {
@override
BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
@override
BuildContext get storageContext => context;
}
-
storageContext
Фактически
ScrollableState
изcontext
.
-
notificationContext
Найдите ссылку ниже, вы можете увидеть.
Конечно же, кто спровоцировал событие, конечноScrollableState
внутри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;
},
);
В конце концов, мы все ещеstorageContext
Усилия были предприняты выше. До #Враг жизни Флаттера Сливера# В сериале у нас естьSliver
Соответствующие знания были отсортированы. дляTabbarView
илиPageView
текущий отображаемый элемент, вRenderSliverFillViewport
должен быть уникальным (если только вы не поместитеviewportFraction
установлено значение меньше, чем1
стоимость ). мы можем пройти_NestedScrollPosition
изContext
найдитеRenderSliverFillViewport
,посмотриRenderSliverFillViewport
Ребенок в_NestedScrollPosition
изContext
.
- Исправлять
_NestedScrollCoordinator._innerPositions
для следующего:
Iterable<_NestedScrollPosition> get _innerPositions {
if (_innerController.nestedPositions.length > 1) {
final Iterable<_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;
}
}
- существует
renderObjectIsVisible
метод, чтобы увидеть, существует ли он вTabbarView
илиPageView
в и егоaxis
а такжеScrollPosition
изaxis
перпендикуляр. Если есть, используйтеRenderViewport
токchild
передачаchildIsVisible
способ убедиться, что он содержитScrollPosition
соответствующийRenderObject
.注意,这里调用了renderObjectIsVisible
потому что могут быть вложенными (многоуровневыми)TabbarView
илиPageView
.
bool renderObjectIsVisible(RenderObject renderObject, Axis axis) {
final RenderViewport? parent = findParentRenderViewport(renderObject);
if (parent != null && parent.axis == axis) {
for (final RenderSliver childrenInPaint
in parent.childrenInHitTestOrder) {
return childIsVisible(childrenInPaint, renderObject) &&
renderObjectIsVisible(parent, axis);
}
}
return true;
}
- Погляди
RenderViewport
, мы толькоNestedScrollView
изbody
, до_ExtendedRenderSliverFillRemainingWithScrollable
.
RenderViewport? findParentRenderViewport(RenderObject? object) {
if (object == null) {
return null;
}
object = object.parent as RenderObject?;
while (object != null) {
// 只在 body 中寻找
if (object is _ExtendedRenderSliverFillRemainingWithScrollable) {
return null;
}
if (object is RenderViewport) {
return object;
}
object = object.parent as RenderObject?;
}
return null;
}
- передача
visitChildrenForSemantics
траверсchildren
, посмотри, сможешь ли ты найтиScrollPosition
соответствующийRenderObject
/// 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;
}
Есть ли другие варианты?
На самом деле дляПроблема прокрутки нескольких списков, влияющих друг на друга в Body, если вы просто хотите, чтобы список сохранял свою позицию, вы можете использоватьPageStorageKey
для сохранения положения прокручиваемого списка. В этом случае,TabbarView
илиPageView
При переключении,ScrollableState
Могуdispose
, и изScrollPosition
отinnerController
серединаdetach
Терять.
@override
void dispose() {
if (widget.controller != null) {
widget.controller!.detach(position);
} else {
_fallbackScrollController?.detach(position);
_fallbackScrollController?.dispose();
}
position.dispose();
_persistedScrollOffset.dispose();
super.dispose();
}
И то, что вам нужно сделать, находится на верхнем уровне, используйте что-то вродеprovider | Flutter Package (flutter-io.cn)чтобы сохранить данные списка или другое состояние данных.
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,
),
],
),
),
],
),
),
перестроить код
физическая активность
За 3 года я написал 18 библиотек компонентов Flutter и 3 инструмента, связанных с Flutter.
-
extended_nested_scroll_view | Flutter Package (flutter-io.cn)
-
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
-
ff_annotation_route_library | Flutter Package (flutter-io.cn)
Можно сказать, что каждый официальный релизStable
Версия, для меня это все физическая работа. особенноextended_nested_scroll_view,extended_text
, extended_text_field
, extended_imageЭти 4 библиотеки,merge
Код — это не только физическая работа, но и требует тщательного и тщательного понимания новых изменений.
Реструктуризация
На этот раз, воспользовавшись этим изменением, я скорректировал всю структуру.
-
src/extended_nested_scroll_view.dart
В официальный исходный код внесены лишь некоторые необходимые изменения. Например, добавление параметров и замена расширенных типов. Сохраняйте структуру и формат официального исходного кода в максимально возможной степени. -
src/extended_nested_scroll_view_part.dart
Часть кода для расширения функционала официального компонента. Добавьте следующие три класса расширения, чтобы реализовать соответствующие методы расширения.
class _ExtendedNestedScrollCoordinator extends _NestedScrollCoordinator
class _ExtendedNestedScrollController extends _NestedScrollController
class _ExtendedNestedScrollPosition extends _NestedScrollPosition
Наконец вsrc/extended_nested_scroll_view.dart
Просто измените код инициализации. мне просто нужно использоватьsrc/extended_nested_scroll_view.dart
с официальным кодомmerge
Вот и все.
_NestedScrollCoordinator? _coordinator;
@override
void initState() {
super.initState();
_coordinator = _ExtendedNestedScrollCoordinator(
this,
widget.controller,
_handleHasScrolledBodyChanged,
widget.floatHeaderSlivers,
widget.pinnedHeaderSliverHeightBuilder,
widget.onlyOneScrollInBody,
widget.scrollDirection,
);
}
конфеты 🍬
Если вы видите это, вы прочитали 6000 слов, спасибо. Отправьте несколько советов, я надеюсь помочь вам.
CustomScrollView center
CustomScrollView.center
Я действительно говорил об этом свойстве давным-давно.Враг жизни Флаттера Щепки (ScrollView) (juejin.cn).
Проще говоря:
-
center
это место для начала рисования, оба нарисованы вzero scroll offset
, вперед отрицательно, назад положительно. -
center
предыдущийSliver
рисуется в обратном порядке.
Например, следующий код, как вы думаете, как выглядит окончательный эффект?
CustomScrollView(
center: key,
slivers: <Widget>[
SliverList(),
SliverGrid(key:key),
]
)
Схема эффекта выглядит следующим образом:SliverGrid
рисуется в исходном положении. Вы можете прокрутить вниз, на этот раз вышеSliverList
будет отображаться.
CustomScrollView.anchor
в состоянии контролироватьcenter
позиция.
0 — это начало окна просмотра, а 1 — это конец окна просмотра, который представляет собой пропорцию высоты окна просмотра по вертикали (ширины по горизонтали). Например, если это 0,5, то нарисуйтеSliverGrid
место будетviewport
среднее положение.
С помощью этих двух свойств мы можем создать несколько интересных эффектов.
список чатов
flutter_instant_messaging/main.dart at master · fluttercandies/flutter_instant_messaging (github.com)Небольшое демо, написанное год назад, теперь перемещено вflutter_challenges/chat_sample.dart at main · fluttercandies/flutter_challenges (github.com)Единое обслуживание.
iOS обратный фотоальбом
flutter_challenges/ios_photo album.dart at main · fluttercandies/flutter_challenges (github.com)Код здесь.
Происходит от Мастера Маwechat_assets_picker | Flutter Package (flutter-io.cn)Требования упомянутые (окончательный платеж не урегулирован), эффект от просмотра альбома должен быть таким же, как у Ios. Дизайн Ios действительно другой, просто учусь (чао).
Эффект прокрутки домашней страницы Betta
flutter_challenges/float_scroll.dart at main · fluttercandies/flutter_challenges (github.com)Код здесь.
Я должен упомянуть об этом снова,NotificationListener
,этоNotification
слушатель. пройти черезNotification.dispatch
, уведомление будет передано вверх по текущему узлу (BuildContext), как и всплывающее окно, вы можете использовать родительский узел для использованияNotificationListener
чтобы получить уведомление. Во Flutter часто используетсяScrollNotification
,Кроме тогоSizeChangedLayoutNotification
,KeepAliveNotification
,LayoutChangedNotification
Ждать. Вы также можете определить уведомление самостоятельно.
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('星星收到了通知: ${notification.text}');
return true;
},
child: Scaffold(
appBar: AppBar(),
body: NotificationListener<TextNotification>(
onNotification: (TextNotification notification) {
showToast('大宝收到了通知: ${notification.text}');
// 如果这里改成 true, 星星就收不到信息了,
return false;
},
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
onPressed: () {
TextNotification('下班了!')..dispatch(context);
},
child: Text('点我'),
);
},
),
),
)),
);
}
}
class TextNotification extends Notification {
TextNotification(this.text);
final String text;
}
И мы часто используем подтягивающее обновление, а подтягивание для загрузки большего количества компонентов также можно отслеживать, прослушиваяScrollNotification
Что нужно сделать.
pull_to_refresh_notification | Flutter Package (flutter-io.cn)
loading_more_list | Flutter Package (flutter-io.cn)
ScrollPosition.ensureVisible
Для этого большинство людей должно уметь. На самом деле он неотделим от него, через текущий объектRenderObject
найти соответствующийRenderAbstractViewport
, то черезgetOffsetToReveal
метод получения относительного положения.
/// 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);
}
Адрес демо-кода:демо-версия sureVisible (github.com)
оставить вопрос при нажатии点我跳转顶部,我是固定的
Когда эта кнопка нажата, угадайте, что происходит.
Флаттер вызов
Ранее я упомянул официальному лицу Nuggets, можно ли его увеличить.你问我答
/ 你出题我挑战
Модули, увеличьте коммуникацию между программистами, программисты не хотят признать поражение, должны ли они быть 🔥? Об этом интересно думать. я создаю новыйГруппа FlutterChallenges qq 321954965общаться;склад, используемый для обсуждения и хранения этих маленьких кодов вызовов. Обычно я собираю несколько примеров реальных сцен, которые обычно сложны, не только для того, чтобы продемонстрировать свои навыки. Вступление в группу должно быть рекомендовано или проверено, добро пожаловать, чтобы бросить свою детскую обувь
.
День святого Валентина + Танабата Это совпадение? ?
Страница заказа Meituan Ele.me
Требовать:
- Левый и правый списки можно связать, а всю домашнюю страницу можно прокручивать вверх и вниз.
- Универсальность, возможность сборки из компонентов
Если вы внимательно читаетеNestedScrollView
, я думаю, должен быть способ сделать эту функцию.
Увеличить область клика
Увеличение области щелчка должно быть требованием, с которым следует сталкиваться в обычное время, так как же это должно быть реализовано во Flutter?
Оригинальный кодовый адрес:Увеличение области клика (github.com)
Для удобства тестирования добавьтеpubspec.yaml
Добавить финансовый драконoktoast
.
oktoast: any
Требовать:
- Не изменяйте всю конструкцию и размеры.
- не прямо
Stack
положить весьItem
переписать. - Универсальность.
Завершенный эффект выглядит следующим образом, а расширенный диапазон теоретически можно установить по желанию.
Эпилог
Первый раз засушиваю наггетсы, могу только часть кода статьи перенести наgist.github.com/zmtzawqlp(Вдруг вспомнил, что некоторые заголовки про 40-символьные статьи были чем-то вроде пощечины, по-моему близко к 9000 и больше не могу писать). В этой статье написано больше, пишите что приходит в голову. Независимо от того, какая это технология, вы можете понять истину, только если углубитесь в нее. Поддержка компонентов с открытым исходным кодом действительно утомительна. Но это будет постоянно заставлять вас учиться, и в постоянном обновлении и итерации вы узнаете некоторые знания, к которым нелегко получить доступ. Накопить песок в башню, раскатать его по всемуFlutter
Исходный код больше не мечта.
ЛюбовьFlutter
,Любовь糖果
,Добро пожаловатьFlutter Candies, чтобы вместе производить милые конфеты FlutterГруппа QQ: 181398081
надевать в последнюю очередьFlutter CandiesВся семейная бочка, действительно ароматная.