这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 85 additions & 105 deletions engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:ui/src/engine.dart';
Expand All @@ -9,20 +10,10 @@ import 'package:ui/ui.dart' as ui;
/// Implements vertical and horizontal scrolling functionality for semantics
/// objects.
///
/// Scrolling is implemented using a "joystick" method. The absolute value of
/// "scrollTop" in HTML is not important. We only need to know in whether the
/// value changed in the positive or negative direction. If it changes in the
/// positive direction we send a [ui.SemanticsAction.scrollUp]. Otherwise, we
/// send [ui.SemanticsAction.scrollDown]. The actual scrolling is then handled
/// by the framework and we receive a [ui.SemanticsUpdate] containing the new
/// [scrollPosition] and child positions.
///
/// "scrollTop" or "scrollLeft" is always reset to an arbitrarily chosen non-
/// zero "neutral" scroll position value. This is done so we have a
/// predictable range of DOM scroll position values. When the amount of
/// contents is less than the size of the viewport the browser snaps
/// "scrollTop" back to zero. If there is more content than available in the
/// viewport "scrollTop" may take positive values.
/// Scrolling is controlled by sending the current DOM scroll position in a
/// [ui.SemanticsAction.scrollToOffset] to the framework where it applies the
/// value to its scrollable and the engine receives a [ui.SemanticsUpdate]
/// containing the new [SemanticsObject.scrollPosition] and child positions.
class SemanticScrollable extends SemanticRole {
SemanticScrollable(SemanticsObject semanticsObject)
: super.withBasics(
Expand All @@ -39,81 +30,61 @@ class SemanticScrollable extends SemanticRole {
/// Disables browser-driven scrolling in the presence of pointer events.
GestureModeCallback? _gestureModeListener;

/// DOM element used as a workaround for: https://github.com/flutter/flutter/issues/104036
///
/// When the assistive technology gets to the last element of the scrollable
/// list, the browser thinks the scrollable area doesn't have any more content,
/// so it overrides the value of "scrollTop"/"scrollLeft" with zero. As a result,
/// the user can't scroll back up/left.
///
/// As a workaround, we add this DOM element and set its size to
/// [canonicalNeutralScrollPosition] so the browser believes
/// that the scrollable area still has some more content, and doesn't override
/// scrollTop/scrollLetf with zero.
/// DOM element used to indicate to the browser the total quantity of available
/// content under this scrollable area. This element is sized based on the
/// total scroll extent calculated by scrollExtentMax - scrollExtentMin + rect.height
/// of the [SemanticsObject] managed by this scrollable.
final DomElement _scrollOverflowElement = createDomElement('flt-semantics-scroll-overflow');

/// Listens to HTML "scroll" gestures detected by the browser.
///
/// This gesture is converted to [ui.SemanticsAction.scrollUp] or
/// [ui.SemanticsAction.scrollDown], depending on the direction.
/// When the browser detects a "scroll" gesture we send the updated DOM scroll position
/// to the framework in a [ui.SemanticsAction.scrollToOffset].
@visibleForTesting
DomEventListener? scrollListener;

/// The value of the "scrollTop" or "scrollLeft" property of this object's
/// [element] that has zero offset relative to the [scrollPosition].
int _effectiveNeutralScrollPosition = 0;

/// Whether this scrollable can scroll vertically or horizontally.
bool get _canScroll =>
semanticsObject.isVerticalScrollContainer || semanticsObject.isHorizontalScrollContainer;

/// The previous value of the "scrollTop" or "scrollLeft" property of this object's
/// [element], used to determine if the content was scrolled.
int _previousDomScrollPosition = 0;

/// Responds to browser-detected "scroll" gestures.
void _recomputeScrollPosition() {
if (_domScrollPosition != _effectiveNeutralScrollPosition) {
if (_domScrollPosition != _previousDomScrollPosition) {
if (!EngineSemantics.instance.shouldAcceptBrowserGesture('scroll')) {
return;
}
final bool doScrollForward = _domScrollPosition > _effectiveNeutralScrollPosition;
_neutralizeDomScrollPosition();

_previousDomScrollPosition = _domScrollPosition;
_updateScrollableState();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();

final int semanticsId = semanticsObject.id;
if (doScrollForward) {
if (semanticsObject.isVerticalScrollContainer) {
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollUp,
null,
);
} else {
assert(semanticsObject.isHorizontalScrollContainer);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollLeft,
null,
);
}
final Float64List offsets = Float64List(2);

// Either SemanticsObject.isVerticalScrollContainer or
// SemanticsObject.isHorizontalScrollContainer should be
// true otherwise scrollToOffset cannot be called.
if (semanticsObject.isVerticalScrollContainer) {
offsets[0] = 0.0;
offsets[1] = element.scrollTop;
} else {
if (semanticsObject.isVerticalScrollContainer) {
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollDown,
null,
);
} else {
assert(semanticsObject.isHorizontalScrollContainer);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollRight,
null,
);
}
assert(semanticsObject.isHorizontalScrollContainer);
offsets[0] = element.scrollLeft;
offsets[1] = 0.0;
}

final ByteData? message = const StandardMessageCodec().encodeMessage(offsets);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
viewId,
semanticsId,
ui.SemanticsAction.scrollToOffset,
message,
);
}
}

Expand All @@ -122,6 +93,22 @@ class SemanticScrollable extends SemanticRole {
// Scrolling is controlled by setting overflow-y/overflow-x to 'scroll`. The
// default overflow = "visible" needs to be unset.
semanticsObject.element.style.overflow = '';
// On macOS the scrollbar behavior which can be set in the settings application
// may sometimes insert scrollbars into an application when a peripheral like a
// mouse or keyboard is plugged in. This causes the clientHeight or clientWidth
// of the scrollable DOM element to be offset by the width of the scrollbar.
// This causes issues in the vertical scrolling context because the max scroll
// extent is calculated by the element's scrollHeight - clientHeight, so when
// the clientHeight is offset by scrollbar width the browser may there is
// a greater scroll extent then what is actually available.
//
// The scrollbar is already made transparent in SemanticsRole._initElement so here
// set scrollbar-width to "none" to prevent it from affecting the max scroll extent.
//
// Support for scrollbar-width was only added to Safari v18.2+, so versions before
// that may still experience overscroll issues when macOS inserts scrollbars
// into the application.
semanticsObject.element.style.scrollbarWidth = 'none';

_scrollOverflowElement.style
..position = 'absolute'
Expand All @@ -136,7 +123,15 @@ class SemanticScrollable extends SemanticRole {
super.update();

semanticsObject.owner.addOneTimePostUpdateCallback(() {
_neutralizeDomScrollPosition();
if (_canScroll) {
final double? scrollPosition = semanticsObject.scrollPosition;
assert(scrollPosition != null);
if (scrollPosition != _domScrollPosition) {
element.scrollTop = scrollPosition!;
_previousDomScrollPosition = _domScrollPosition;
}
}
_updateScrollableState();
semanticsObject.recomputePositionAndSize();
semanticsObject.updateChildrenPositionAndSize();
});
Expand Down Expand Up @@ -183,64 +178,49 @@ class SemanticScrollable extends SemanticRole {
}
}

/// Resets the scroll position (top or left) to the neutral value.
///
/// The scroll position of the scrollable HTML node that's considered to
/// have zero offset relative to Flutter's notion of scroll position is
/// referred to as "neutral scroll position".
///
/// We always set the scroll position to a non-zero value in order to
/// be able to scroll in the negative direction. When scrollTop/scrollLeft is
/// zero the browser will refuse to scroll back even when there is more
/// content available.
void _neutralizeDomScrollPosition() {
void _updateScrollableState() {
// This value is arbitrary.
const int canonicalNeutralScrollPosition = 10;
final ui.Rect? rect = semanticsObject.rect;
if (rect == null) {
printWarning('Warning! the rect attribute of semanticsObject is null');
return;
}
final double? scrollExtentMax = semanticsObject.scrollExtentMax;
final double? scrollExtentMin = semanticsObject.scrollExtentMin;
assert(scrollExtentMax != null);
assert(scrollExtentMin != null);
final double scrollExtentTotal =
scrollExtentMax! -
scrollExtentMin! +
(semanticsObject.isVerticalScrollContainer ? rect.height : rect.width);
// Place the _scrollOverflowElement at the beginning of the content
// and size it based on the total scroll extent so the browser
// knows how much scrollable content there is.
if (semanticsObject.isVerticalScrollContainer) {
// Place the _scrollOverflowElement at the end of the content and
// make sure that when we neutralize the scrolling position,
// it doesn't scroll into the visible area.
final int verticalOffset = rect.height.ceil() + canonicalNeutralScrollPosition;
_scrollOverflowElement.style
..transform = 'translate(0px,${verticalOffset}px)'
..width = '${rect.width.round()}px'
..height = '${canonicalNeutralScrollPosition}px';

element.scrollTop = canonicalNeutralScrollPosition.toDouble();
// Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollTop.toInt();
// The cross axis size should be non-zero so it is taken into
// account in the scrollable elements scrollHeight.
..width = '1px'
..height = '${scrollExtentTotal.toStringAsFixed(1)}px';
semanticsObject
..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble()
..verticalScrollAdjustment = element.scrollTop
..horizontalScrollAdjustment = 0.0;
} else if (semanticsObject.isHorizontalScrollContainer) {
// Place the _scrollOverflowElement at the end of the content and
// make sure that when we neutralize the scrolling position,
// it doesn't scroll into the visible area.
final int horizontalOffset = rect.width.ceil() + canonicalNeutralScrollPosition;
_scrollOverflowElement.style
..transform = 'translate(${horizontalOffset}px,0px)'
..width = '${canonicalNeutralScrollPosition}px'
..height = '${rect.height.round()}px';

element.scrollLeft = canonicalNeutralScrollPosition.toDouble();
// Read back because the effective value depends on the amount of content.
_effectiveNeutralScrollPosition = element.scrollLeft.toInt();
..width = '${scrollExtentTotal.toStringAsFixed(1)}px'
// The cross axis size should be non-zero so it is taken into
// account in the scrollable elements scrollHeight.
..height = '1px';
semanticsObject
..verticalScrollAdjustment = 0.0
..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble();
..horizontalScrollAdjustment = element.scrollLeft;
} else {
_scrollOverflowElement.style
..transform = 'translate(0px,0px)'
..width = '0px'
..height = '0px';
element.scrollLeft = 0.0;
element.scrollTop = 0.0;
_effectiveNeutralScrollPosition = 0;
semanticsObject
..verticalScrollAdjustment = 0.0
..horizontalScrollAdjustment = 0.0;
Expand Down
Loading