Flutter заново распознает NestedScrollView

Android iOS Flutter

предисловие

extended_nested_scroll_viewмоя первая загрузка вpub.devКомпонент флаттера.

Мюгающий глаз почти три года, прошел через 43 версиях итераций, стабильна, официальная синхронизация кода.

8C0553A1-5925-4E77-BF3B-3C771531B9B6.png

И я недавно готовился к рефакторингу. Как бы это сказать, я общаюсь с Флаттером уже 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Что, тогда зачем мне продлевать официальный компонент?

Проблемы, когда заголовок содержит несколько закрепленных осколков

анализировать

Сначала посмотрите на картинку, как вы думаете, каков конечный результат прокрутки списка вверх? Код ниже.

AB435C89-515B-4FFC-BACF-96511CC72D51.png

    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.

905DB40E-20D4-43FD-BE71-6C0868DA7093.png

Чиновники 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

Давайте еще раз подумаем, что повлияет на конечное расстояние прокрутки компонента прокрутки?

ответScrollPosition.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.

AEF1EF26-CD2B-40B9-A0EC-B6FC921A00BA.png

449A65EF-CCB9-4F44-BECE-556DC94B12E6.png

Смотрите здесь, мы должны знать, что вы можете пройти переопределениеapplyContentDimensionsметод, чтобы сброситьmaxScrollExtent

ScrollPosition

хочу переопределитьapplyContentDimensionsпросто знаюScrollPositionКогда он был создан, продолжите отладку и нажмите точку остановаScrollPositionструктура выше.

3D912727-EDAF-4669-A5C6-342FFB4219F7.png

ScrollController.createScrollPosition
ScrollPositionWithSingleContext
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 года.

старый план
  1. существуетScrollPosition attachпройти, когдаcontextНайдите флаг, соответствующий этому списку, и следуйтеTabbarViewилиPageViewАссоциация индекса для сравнения.

Flutter расширяет NestedScrollView (2) Решение для синхронизации прокрутки списка (juejin.cn)

  1. Определить текущую позицию, вычислив относительную позицию списка显示список.

Видимая область виджета Flutter, относительное положение, размер, который вы хотите знать (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不就对应正在显示的列表吗? ?

Конкретно для кода, давайте попробуем войти, чтобы увидеть,

GitHub.com/flutter/Appendix…

  @override
  Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
    print(debugLabel);
    return coordinator.drag(details, dragCancelCallback);
  }

2F350159-7545-4D81-8720-ABED291428FB.png

Идеал хорош, но реальность худая, что бы я ни каталсяHeaderещеBody, все просто распечататьouter. Значит ли это, что все жесты в Теле съедены? ?

Не волнуйтесь, мы откроемDevTools,посмотриListViewвнутриScrollableStateположение дел. (По конкретным причинам вы можете прочитать это здесь.Flutter FlexGrid с заблокированной строкой и столбцом (juejin.cn))

6D8E3694-613E-4489-AB67-D0773ADD5051.png

Ха-ха,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Нажмите точку останова в методе, чтобы увидеть, когда будет сделан вызов.

443ACE15-B343-4B07-98A8-DB00FFCA3AF8.png

  1. RenderViewport.performLayout

Вычислить ток в методе PerformLayoutScrollPositionминимум и максимум

     if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           ))
  1. 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;
  }
  1. 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;
}
  1. ScrollableState.setCanDrag

Наконец добрался сюда, иди поcanDragа такжеaxis(Горизонтальный и вертикальный)

_NestedScrollCoordinator

Тогда впередNestedScrollViewПосмотрите в коде.

GitHub.com/flutter/Appendix…

  @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;
  }

Теперь снова запустите демо, прокрутите список после переключения, это 👌? Результат разочаровал.

  1. Хотя мыdragПри работе действительно можно определить, кто активен, но когда палец вверх, и начинается инерционное слайд,dragCancelCallbackОбратный вызов сработал,_isActivedбыл установлен наfalse.
  2. когда мы работаем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, давайте посмотрим, что у этого парня есть что использовать.

截屏2021-08-09 下午4.44.08.png

увидел с первого взгляда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Найдите ссылку ниже, вы можете увидеть.

截屏2021-08-09 下午5.00.01.png

Конечно же, кто спровоцировал событие, конечно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.

  1. like_button | Flutter Package (flutter-io.cn)

  2. extended_image_library | Flutter Package (pub.dev)

  3. extended_nested_scroll_view | Flutter Package (flutter-io.cn)

  4. extended_text | Flutter Package (flutter-io.cn)

  5. extended_text_field | Flutter Package (flutter-io.cn)

  6. extended_image | Flutter Package (flutter-io.cn)

  7. extended_sliver | Flutter Package (flutter-io.cn)

  8. pull_to_refresh_notification | Flutter Package (flutter-io.cn)

  9. waterfall_flow | Flutter Package (flutter-io.cn)

  10. loading_more_list | Flutter Package (flutter-io.cn)

  11. extended_tabs | Flutter Package (flutter-io.cn)

  12. http_client_helper | Dart Package (flutter-io.cn)

  13. extended_text_library | Flutter Package (flutter-io.cn)

  14. extended_list | Flutter Package (flutter-io.cn)

  15. extended_list_library | Flutter Package (flutter-io.cn)

  16. ff_annotation_route_library | Flutter Package (flutter-io.cn)

  17. loading_more_list_library | Dart Package (flutter-io.cn)

  18. ff_annotation_route | Dart Package (flutter-io.cn)

  19. ff_annotation_route_core | Dart Package (flutter-io.cn)

  20. flex_grid | Flutter Package (flutter-io.cn)

  21. assets_generator | Dart Package (flutter-io.cn)

  22. fluttercandies/JsonToDart: инструмент для преобразования json в код dart, поддержка Windows, Mac, Web (github.com).

Можно сказать, что каждый официальный релизStableВерсия, для меня это все физическая работа. особенноextended_nested_scroll_view,extended_text , extended_text_field , extended_imageЭти 4 библиотеки,mergeКод — это не только физическая работа, но и требует тщательного и тщательного понимания новых изменений.

Реструктуризация

На этот раз, воспользовавшись этим изменением, я скорректировал всю структуру.

E5A53922-AF95-4B13-805A-36F19286344D.png

  • 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,
    );
  }

конфеты 🍬

F794DA85-4559-45EC-92B0-FFA16D2B87FE.png

Если вы видите это, вы прочитали 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будет отображаться.

F2C1BFDA-2919-45BD-B71A-F601F248797B.png

CustomScrollView.anchorв состоянии контролироватьcenterпозиция. 0 — это начало окна просмотра, а 1 — это конец окна просмотра, который представляет собой пропорцию высоты окна просмотра по вертикали (ширины по горизонтали). Например, если это 0,5, то нарисуйтеSliverGridместо будетviewportсреднее положение.

С помощью этих двух свойств мы можем создать несколько интересных эффектов.

список чатов

chatList.gif

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 обратный фотоальбом

iosPhotoAlbum.gif

flutter_challenges/ios_photo album.dart at main · fluttercandies/flutter_challenges (github.com)Код здесь.

Происходит от Мастера Маwechat_assets_picker | Flutter Package (flutter-io.cn)Требования упомянутые (окончательный платеж не урегулирован), эффект от просмотра альбома должен быть таким же, как у Ios. Дизайн Ios действительно другой, просто учусь (чао).

image.png

Эффект прокрутки домашней страницы Betta

float_scroll.gif

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

QQ20210812-205510-HD.gif

Для этого большинство людей должно уметь. На самом деле он неотделим от него, через текущий объект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общаться;склад, используемый для обсуждения и хранения этих маленьких кодов вызовов. Обычно я собираю несколько примеров реальных сцен, которые обычно сложны, не только для того, чтобы продемонстрировать свои навыки. Вступление в группу должно быть рекомендовано или проверено, добро пожаловать, чтобы бросить свою детскую обувь .

765C2B11D14A50B6A967989C3F389F89.jpg

39661F87CC0AF9632C8AF35A1D28334A.jpg

День святого Валентина + Танабата Это совпадение? ?

144135BF9EF5646F6ACB8352387C7FB9.jpg

Страница заказа Meituan Ele.me

image.png

Требовать:

  1. Левый и правый списки можно связать, а всю домашнюю страницу можно прокручивать вверх и вниз.
  2. Универсальность, возможность сборки из компонентов

Если вы внимательно читаетеNestedScrollView, я думаю, должен быть способ сделать эту функцию.

Увеличить область клика

Увеличение области щелчка должно быть требованием, с которым следует сталкиваться в обычное время, так как же это должно быть реализовано во Flutter?

1.gif

Оригинальный кодовый адрес:Увеличение области клика (github.com)

Для удобства тестирования добавьтеpubspec.yamlДобавить финансовый драконoktoast.

  oktoast: any

Требовать:

  1. Не изменяйте всю конструкцию и размеры.
  2. не прямоStackположить весьItemпереписать.
  3. Универсальность.

Завершенный эффект выглядит следующим образом, а расширенный диапазон теоретически можно установить по желанию.

2.gif

Эпилог

image.pngПервый раз засушиваю наггетсы, могу только часть кода статьи перенести наgist.github.com/zmtzawqlp(Вдруг вспомнил, что некоторые заголовки про 40-символьные статьи были чем-то вроде пощечины, по-моему близко к 9000 и больше не могу писать). В этой статье написано больше, пишите что приходит в голову. Независимо от того, какая это технология, вы можете понять истину, только если углубитесь в нее. Поддержка компонентов с открытым исходным кодом действительно утомительна. Но это будет постоянно заставлять вас учиться, и в постоянном обновлении и итерации вы узнаете некоторые знания, к которым нелегко получить доступ. Накопить песок в башню, раскатать его по всемуFlutterИсходный код больше не мечта.

ЛюбовьFlutter,Любовь糖果,Добро пожаловатьFlutter Candies, чтобы вместе производить милые конфеты FlutterГруппа QQ: 181398081

надевать в последнюю очередьFlutter CandiesВся семейная бочка, действительно ароматная.