предисловие
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 года.
старый план
- существует
ScrollPositionattachпройти, когда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Вся семейная бочка, действительно ароматная.