preface

Connect back to:

PageController source code analysis

This time I will document the process of unpacking pageView. Variables and methods that do not have much to do with each other will be ignored, as well as some that are described in the pageController source code analysis article, which I will note.

PageView

Let's start with the constructor: (it has three constructors, and we use PageView as the entry)Copy the code
  PageView({
    Key key,
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
    PageController controller,
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    List<Widget> children = const <Widget>[],
    this.dragStartBehavior = DragStartBehavior.start,
    this.allowImplicitScrolling = false,})Copy the code
Structure:Copy the code

Controller, physics can see pageController source code analysis

The DragStartBehavior parameter needs to be mentioned.

DragStartBehavior

DragStartBehavior is an enumeration class with the following code:

enum DragStartBehavior {
  down,

  start,
}
Copy the code

The comment says: Configure the offset (position) passed to DragStartDetails. DragStartDetails is often seen in gesture callbacks and notifications.

On further inspection, monodrag.dart has this comment:

  /// Configure the behavior of offsets sent to [onStart].
  ///
  /// If set to [DragStartBehavior.start], the [onStart] callback will be called
  /// at the time and position when this gesture recognizer wins the arena. If
  /// [DragStartBehavior.down], [onStart] will be called at the time and
  /// position when a down event was first detected.
Copy the code

Configure the behavior of “locations “(such as those triggered by your gestures) that are passed to the onStart callback.

If set to.start, the position and time will only be passed to the onStart callback if the gesture recognizer wins in the arena.

If set to.down, the time and location passed to onStart is when the event was first detected.

Such as:

The finger position was (500,500) when pressed on the screen and moved to (510,500) before winning the arena. The onStart callback receives offset (500,500) from dragstartbehavior.down. If dragStartBehavior.start is used, the onStart callback receives offset (510,500).

Gesture recognizers: the various callbacks we set up in GestureDector: tap,longPress, horizontal/vertical sliding, etcCopy the code

What is the arena?

Arena & Gesture disambiguation

I have this link in the comments:

https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation you may need to fq, original text is as follows:Copy the code

With my poor English turn a ha, there is a wrong also please correct.

Definition:

There may be multiple gesture recognizers in one location on the screen. All of these recognizers listen for pointer events coming out of the stream and recognize the gestures they need. Which gestures are recognized is determined by the non-empty callback in the GestureDector widget.

When an event is triggered by a user’s finger at a location on the screen that can be matched by multiple recognizers, framework Disambiguates places the event in the arena, winning as follows:

· Any time there is only one gesture recognizer in the arena, that recognizer wins.

· Any time one of the recognizers wins because of any factor, the remaining recognizers all lose.

For example, in horizontal and vertical drag disambiguation, both recognizers enter the arena as soon as the pressed event occurs (where both horizontal and vertical recognizers are expected to receive the event). The two recognizers then sit still and continue to observe the subsequent event (movement). If the user moves horizontally for a certain distance (logical pixels), the horizontal gesture is declared the winner, and the subsequent gesture is considered horizontal gesture, the same way vertically.

The arena is still very effective with only one gesture recognizer, such as a horizontal (vertical) recognizer. Assuming that there is only one horizontal recognizer in the arena, when the user first touches the screen, the pixels of the touch point are treated as if they were dragged horizontally, rather than determined by the user’s subsequent actions.

That's pageView, because pageView is a Statefulwidget, so let's look at its stateCopy the code

_PageViewState

The code in _PageViewState is very simple. Let’s look directly at the build method, which looks like this:

  @override
  Widget build(BuildContext context) {
    final AxisDirection axisDirection = _getDirection(context);
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
    ).applyTo(widget.pageSnapping
        ? _kPagePhysics.applyTo(widget.physics)
        : widget.physics);

    return NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification notification) {
        if(notification.depth == 0 && widget.onPageChanged ! = null && notification is ScrollUpdateNotification) { final PageMetrics metrics = notification.metrics as PageMetrics; final int currentPage = metrics.page.round();if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
            widget.onPageChanged(currentPage);
          }
        }
        return false;
      },
      child: Scrollable(
        dragStartBehavior: widget.dragStartBehavior,
        axisDirection: axisDirection,
        controller: widget.controller,
        physics: physics,
        viewportBuilder: (BuildContext context, ViewportOffset position) {
          return Viewport(
            // TODO(dnfield): we should provide a way to setcacheExtent // independent of implicit scrolling: // https://github.com/flutter/flutter/issues/45632 cacheExtent: Widget. AllowImplicitScrolling? 1.0:0.0, cacheExtentStyle: cacheExtentStyle. The viewport, axisDirection: axisDirection, offset: position, slivers: <Widget>[ SliverFillViewport( viewportFraction: widget.controller.viewportFraction, delegate: widget.childrenDelegate, ), ], ); },),); }Copy the code

Final AxisDirection AxisDirection = _getDirection(context); Get the direction

2, define the physical effects, this can be seen in Pagecontroller: juejin.cn/post/684490…

A NotificationListener is used to calculate the number of pages in the child widget based on the scroll of the child widget. The child widget is a Scrollable

Scrollable

Scrollable creates a Scrollable wiget with almost the same parameters as PageView, which will not be described here. The widget itself is a statefulWidget. Instead of taking the Child argument, it takes the viewportBuilder argument, which is also interesting: a context and a position.

Let’s look at its state first. The structure is shown as follows:

SetCanDrag (bool), which sets whether it can be dragged and, if so, generates a further recognizer (horizontal/vertical) _updatePosition().Copy the code

PageController source code analysis

Next is the build() method.Copy the code
// DESCRIPTION @override Widget build(BuildContext context) { assert(position ! = null); // _ScrollableScope must be placed above the BuildContext returned by notificationContext // so that we can get this ScrollableState by doing the following: // // ScrollNotification notification; // Scrollable.of(notification.context) // // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope // must be placed above the widget using it: RawGestureDetector Widget result = _ScrollableScope( scrollable: this, position: position, // TODO(ianh): Having all these global keys is sad. child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( key: _gestureDetectorKey, gestures: _gestureRecognizers, behavior: HitTestBehavior.opaque, excludeFromSemantics: widget.excludeFromSemantics, child: Semantics( explicitChildNodes: ! widget.excludeFromSemantics, child: IgnorePointer( key: _ignorePointerKey, ignoring: _shouldIgnorePointer, ignoringSemantics:false, child: widget.viewportBuilder(context, position), ), ), ), ), ); // <! --if (! widget.excludeFromSemantics) {--> <! -- result = _ScrollSemantics(--> <! -- key: _scrollSemanticsKey,--> <! -- child: result,--> <! -- position: position,--> <! -- allowImplicitScrolling: widget? .physics? .allowImplicitScrolling ?? _physics.allowImplicitScrolling,--> <! -- semanticChildCount: widget.semanticChildCount,--> <! -); -- > <! -} -- >return _configuration.buildViewportChrome(context, result, widget.axisDirection);
  }
Copy the code

The build method has a line above it:

/ / the DESCRIPTIONCopy the code

In fact, the build method does not build any new child widgets, just wrap them with aligned widgets and return one:

_configuration.buildViewportChrome(context, result, widget.axisDirection); This method mainly returns different effects according to different systems. For example: Android, scroll to the tail after continuing to scroll, there will be a blue watermarkCopy the code

Let’s look at the _ScrollableScope

_ScrollableScope

We use _ScrollableScope to wrap ourselves with an inheritWidget and store a position. We use _ScrollableScope to wrap ourselves with a static method in Scollable.

Static Future<void> ensureVisible(BuildContext Context, {double alignment = 0.0, Duration Duration = duration.zero, Curve curve = Curves.ease, ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, }) { final List<Future<void>> futures = <Future<void>>[]; ScrollableState scrollable = Scrollable.of(context);while(scrollable ! = null) { futures.add(scrollable.position.ensureVisible( context.findRenderObject(), alignment: alignment, duration: duration, curve: curve, alignmentPolicy: alignmentPolicy, )); context = scrollable.context; scrollable = Scrollable.of(context); }if (futures.isEmpty || duration == Duration.zero)
      return Future<void>.value();
    if (futures.length == 1)
      return futures.single;
    return Future.wait<void>(futures).then<void>((List<void> _) => null);
  }
Copy the code

Can scroll to the specified context, internal calls controller. Position. EnsureVisible through the context, find the corresponding renderObject and scroll to the position.

In fact, on any page that has a scroll component, you call this static method, and you pass in the context of the target item, and you roll through it. But for some that recycle children, like listView, you'll probably end up in the gutter.Copy the code

Because of the above Settings, this method is static. How do I get the ScrollableState of the context and get position in it (so I can call its method ensureVisible)? We can pass.of(context) as follows:

  static ScrollableState of(BuildContext context) {
    final _ScrollableScope widget = context.dependOnInheritedWidgetOfExactType<_ScrollableScope>();
    returnwidget? .scrollable; }Copy the code
Why to take position (ScrollPosition), you can see pageController source analysisCopy the code

. You can see the context dependOnInheritedWidgetOfExactType returned to what we want, but the premise is that return things must inherit from InheritedWidget, That’s why we use _ScrollableScope for the package above.

Go back to the build method in Scrollablestate and look down. The _ScrollableScope child is relatively simple. Wrap the Builder from the parent widget with a Listener and a RawGestureDetector.

Listener

The Listener can distribute events. The structure is as follows:

RawGestureDetector

RawGestureDetector helps Child identify the specified gestures that are generated in the setCanDrag() method above.

ScrollableState is broken down, so let’s take a look at the viewportBuilder for Scrollable, which is the one above that we wrapped several layers around.

viewportBuilder

This method returns a ViewPort, which I understand to be called a window.

The code is as follows:

        viewportBuilder: (BuildContext context, ViewportOffset position) {
          return Viewport(
            // TODO(dnfield): we should provide a way to setcacheExtent // independent of implicit scrolling: // https://github.com/flutter/flutter/issues/45632 cacheExtent: Widget. AllowImplicitScrolling? 1.0:0.0, cacheExtentStyle: cacheExtentStyle. The viewport, axisDirection: axisDirection, offset: position, slivers: <Widget>[ SliverFillViewport( viewportFraction: widget.controller.viewportFraction, delegate: widget.childrenDelegate, ), ], ); },Copy the code

Its inheritance is as follows (top to bottom, child to parent)

Viewport left MultiChildRenderObjectWidget left RenderObjectWidget left WidgetCopy the code

For the record, our common StatelessWidgets and StatefulWidgets also inherit from widgets. RenderObjectWidget and MultiChildRenderObjectWidget content too much is not here, interested can go to consult the relevant information.

A quick introduction to RenderObjectWidget: We know that RenderObject is directly used for rendering and drawing, and RenderObjectWidget is the configuration information for rendering and drawing, and when configuration changes need to be redrawn, updateRenderObject() is called. Its source code:

abstract class RenderObjectWidget extends Widget {

  const RenderObjectWidget({ Key key }) : super(key: key);

  @override
  RenderObjectElement createElement();

  @protected
  RenderObject createRenderObject(BuildContext context);

  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
Copy the code

It also creates an Element, which looks a lot like a status widget, so why is RenderObjectWidget used for viewPort?

The viewPort begins with this sentence:

  /// The viewport listens to the [offset], which means you do not need to
/// rebuild this widget when the [offset] changes.
Copy the code

In other words, it’s just a window, and the children (slivers) you created earlier scroll outside the window, and you browse through the window (which is related to the position (offset) passed in above), and the window doesn’t change. So it’s much simpler to use RenderObjectWidget in one step.

ViewportBuilder (BuildContext Context, ViewportOffset Position)Copy the code

Another argument to Viewport slivers:

slivers: <Widget>[
              SliverFillViewport(
                viewportFraction: widget.controller.viewportFraction,
                delegate: widget.childrenDelegate,
              ),
            ],
Copy the code

The reason why you can pass a SliverFillViewport around your children is just to make sure that your display matches pageView: a Child (sliver) fills a window.

So far we have finished the rough analysis of the whole PageView article is quite long, thank you for watching. If there is a mistake or did not say clearly, please correct, thank you.Copy the code

Related articles

PageController source code analysis

My past articlesCopy the code

Juejin. Cn/post / 684490…