• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7/// Implements vertical and horizontal scrolling functionality for semantics
8/// objects.
9///
10/// Scrolling is implemented using a "joystick" method. The absolute value of
11/// "scrollTop" in HTML is not important. We only need to know in whether the
12/// value changed in the positive or negative direction. If it changes in the
13/// positive direction we send a [ui.SemanticsAction.scrollUp]. Otherwise, we
14/// send [ui.SemanticsAction.scrollDown]. The actual scrolling is then handled
15/// by the framework and we receive a [ui.SemanticsUpdate] containing the new
16/// [scrollPosition] and child positions.
17///
18/// "scrollTop" or "scrollLeft" is always reset to an arbitrarily chosen non-
19/// zero "neutral" scroll position value. This is done so we have a
20/// predictable range of DOM scroll position values. When the amount of
21/// contents is less than the size of the viewport the browser snaps
22/// "scrollTop" back to zero. If there is more content than available in the
23/// viewport "scrollTop" may take positive values.
24class Scrollable extends RoleManager {
25  Scrollable(SemanticsObject semanticsObject)
26      : super(Role.scrollable, semanticsObject);
27
28  /// Disables browser-driven scrolling in the presence of pointer events.
29  GestureModeCallback _gestureModeListener;
30
31  /// Listens to HTML "scroll" gestures detected by the browser.
32  ///
33  /// This gesture is converted to [ui.SemanticsAction.scrollUp] or
34  /// [ui.SemanticsAction.scrollDown], depending on the direction.
35  html.EventListener _scrollListener;
36
37  /// The value of the "scrollTop" or "scrollLeft" property of this object's
38  /// [element] that has zero offset relative to the [scrollPosition].
39  int _effectiveNeutralScrollPosition = 0;
40
41  /// Responds to browser-detected "scroll" gestures.
42  void _recomputeScrollPosition() {
43    if (_domScrollPosition != _effectiveNeutralScrollPosition) {
44      if (!semanticsObject.owner.shouldAcceptBrowserGesture('scroll')) {
45        return;
46      }
47      final bool doScrollForward =
48          _domScrollPosition > _effectiveNeutralScrollPosition;
49      _neutralizeDomScrollPosition();
50      semanticsObject.recomputePositionAndSize();
51
52      final int semanticsId = semanticsObject.id;
53      if (doScrollForward) {
54        if (semanticsObject.isVerticalScrollContainer) {
55          ui.window.onSemanticsAction(
56              semanticsId, ui.SemanticsAction.scrollUp, null);
57        } else {
58          assert(semanticsObject.isHorizontalScrollContainer);
59          ui.window.onSemanticsAction(
60              semanticsId, ui.SemanticsAction.scrollLeft, null);
61        }
62      } else {
63        if (semanticsObject.isVerticalScrollContainer) {
64          ui.window.onSemanticsAction(
65              semanticsId, ui.SemanticsAction.scrollDown, null);
66        } else {
67          assert(semanticsObject.isHorizontalScrollContainer);
68          ui.window.onSemanticsAction(
69              semanticsId, ui.SemanticsAction.scrollRight, null);
70        }
71      }
72    }
73  }
74
75  @override
76  void update() {
77    if (_scrollListener == null) {
78      // We need to set touch-action:none explicitly here, despite the fact
79      // that we already have it on the <body> tag because overflow:scroll
80      // still causes the browser to take over pointer events in order to
81      // process scrolling. We don't want that when scrolling is handled by
82      // the framework.
83      //
84      // This is effective only in Chrome. Safari does not implement this
85      // CSS property. In Safari the `PointerBinding` uses `preventDefault`
86      // to prevent browser scrolling.
87      semanticsObject.element.style.touchAction = 'none';
88      _gestureModeDidChange();
89
90      // We neutralize the scroll position after all children have been
91      // updated. Otherwise the browser does not yet have the sizes of the
92      // child nodes and resets the scrollTop value back to zero.
93      semanticsObject.owner.addOneTimePostUpdateCallback(() {
94        _neutralizeDomScrollPosition();
95      });
96
97      // Memoize the tear-off because Dart does not guarantee that two
98      // tear-offs of a method on the same instance will produce the same
99      // object.
100      _gestureModeListener = (_) {
101        _gestureModeDidChange();
102      };
103      semanticsObject.owner.addGestureModeListener(_gestureModeListener);
104
105      _scrollListener = (_) {
106        _recomputeScrollPosition();
107      };
108      semanticsObject.element.addEventListener('scroll', _scrollListener);
109    }
110  }
111
112  /// The value of "scrollTop" or "scrollLeft", depending on the scroll axis.
113  int get _domScrollPosition {
114    if (semanticsObject.isVerticalScrollContainer) {
115      return semanticsObject.element.scrollTop;
116    } else {
117      assert(semanticsObject.isHorizontalScrollContainer);
118      return semanticsObject.element.scrollLeft;
119    }
120  }
121
122  /// Resets the scroll position (top or left) to the neutral value.
123  ///
124  /// The scroll position of the scrollable HTML node that's considered to
125  /// have zero offset relative to Flutter's notion of scroll position is
126  /// referred to as "neutral scroll position".
127  ///
128  /// We always set the the scroll position to a non-zero value in order to
129  /// be able to scroll in the negative direction. When scrollTop/scrollLeft is
130  /// zero the browser will refuse to scroll back even when there is more
131  /// content available.
132  void _neutralizeDomScrollPosition() {
133    // This value is arbitrary.
134    const int _canonicalNeutralScrollPosition = 10;
135
136    final html.Element element = semanticsObject.element;
137    if (semanticsObject.isVerticalScrollContainer) {
138      element.scrollTop = _canonicalNeutralScrollPosition;
139      // Read back because the effective value depends on the amount of content.
140      _effectiveNeutralScrollPosition = element.scrollTop;
141      semanticsObject
142        ..verticalContainerAdjustment =
143            _effectiveNeutralScrollPosition.toDouble()
144        ..horizontalContainerAdjustment = 0.0;
145    } else {
146      element.scrollLeft = _canonicalNeutralScrollPosition;
147      // Read back because the effective value depends on the amount of content.
148      _effectiveNeutralScrollPosition = element.scrollLeft;
149      semanticsObject
150        ..verticalContainerAdjustment = 0.0
151        ..horizontalContainerAdjustment =
152            _effectiveNeutralScrollPosition.toDouble();
153    }
154  }
155
156  void _gestureModeDidChange() {
157    final html.Element element = semanticsObject.element;
158    switch (semanticsObject.owner.gestureMode) {
159      case GestureMode.browserGestures:
160        // overflow:scroll will cause the browser report "scroll" events when
161        // the accessibility focus shifts outside the visible bounds.
162        //
163        // Note that on Android overflow:hidden also works. However, we prefer
164        // "scroll" because it works both on Android and iOS.
165        if (semanticsObject.isVerticalScrollContainer) {
166          element.style.overflowY = 'scroll';
167        } else {
168          assert(semanticsObject.isHorizontalScrollContainer);
169          element.style.overflowX = 'scroll';
170        }
171        break;
172      case GestureMode.pointerEvents:
173        // We use "hidden" instead of "scroll" so that the browser does
174        // not "steal" pointer events. Flutter gesture recognizers need
175        // all pointer events in order to recognize gestures correctly.
176        if (semanticsObject.isVerticalScrollContainer) {
177          element.style.overflowY = 'hidden';
178        } else {
179          assert(semanticsObject.isHorizontalScrollContainer);
180          element.style.overflowX = 'hidden';
181        }
182        break;
183    }
184  }
185
186  @override
187  void dispose() {
188    final html.CssStyleDeclaration style = semanticsObject.element.style;
189    assert(_gestureModeListener != null);
190    style.removeProperty('overflowY');
191    style.removeProperty('overflowX');
192    style.removeProperty('touch-action');
193    if (_scrollListener != null) {
194      semanticsObject.element.removeEventListener('scroll', _scrollListener);
195    }
196    semanticsObject.owner.removeGestureModeListener(_gestureModeListener);
197    _gestureModeListener = null;
198  }
199}
200