parent
c7158b5507
commit
4f0fa755b1
4 changed files with 445 additions and 2 deletions
@ -0,0 +1,369 @@ |
|||||||
|
// Copyright 2014 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:math' as math; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/rendering.dart'; |
||||||
|
import 'package:flutter/widgets.dart'; |
||||||
|
|
||||||
|
/// Very similar to [SingleChildView] but with a [ViewportBuilder] argument |
||||||
|
/// instead of a [Widget] |
||||||
|
/// |
||||||
|
/// Useful when child needs [ViewportOffset] (e.g. [RenderEditor]) |
||||||
|
/// see: [SingleChildScrollView] |
||||||
|
class QuillSingleChildScrollView extends StatelessWidget { |
||||||
|
/// Creates a box in which a single widget can be scrolled. |
||||||
|
const QuillSingleChildScrollView({ |
||||||
|
required this.controller, |
||||||
|
required this.viewportBuilder, |
||||||
|
Key? key, |
||||||
|
this.physics, |
||||||
|
this.restorationId, |
||||||
|
}) : super(key: key); |
||||||
|
|
||||||
|
/// An object that can be used to control the position to which this scroll |
||||||
|
/// view is scrolled. |
||||||
|
/// |
||||||
|
/// Must be null if [primary] is true. |
||||||
|
/// |
||||||
|
/// A [ScrollController] serves several purposes. It can be used to control |
||||||
|
/// the initial scroll position (see [ScrollController.initialScrollOffset]). |
||||||
|
/// It can be used to control whether the scroll view should automatically |
||||||
|
/// save and restore its scroll position in the [PageStorage] (see |
||||||
|
/// [ScrollController.keepScrollOffset]). It can be used to read the current |
||||||
|
/// scroll position (see [ScrollController.offset]), or change it (see |
||||||
|
/// [ScrollController.animateTo]). |
||||||
|
final ScrollController controller; |
||||||
|
|
||||||
|
/// How the scroll view should respond to user input. |
||||||
|
/// |
||||||
|
/// For example, determines how the scroll view continues to animate after the |
||||||
|
/// user stops dragging the scroll view. |
||||||
|
/// |
||||||
|
/// Defaults to matching platform conventions. |
||||||
|
final ScrollPhysics? physics; |
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.restorationId} |
||||||
|
final String? restorationId; |
||||||
|
|
||||||
|
final ViewportBuilder viewportBuilder; |
||||||
|
|
||||||
|
AxisDirection _getDirection(BuildContext context) { |
||||||
|
return getAxisDirectionFromAxisReverseAndDirectionality( |
||||||
|
context, Axis.vertical, false); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final axisDirection = _getDirection(context); |
||||||
|
final scrollController = controller; |
||||||
|
final scrollable = Scrollable( |
||||||
|
axisDirection: axisDirection, |
||||||
|
controller: scrollController, |
||||||
|
physics: physics, |
||||||
|
restorationId: restorationId, |
||||||
|
viewportBuilder: (context, offset) { |
||||||
|
return _SingleChildViewport( |
||||||
|
offset: offset, |
||||||
|
child: viewportBuilder(context, offset), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
return scrollable; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class _SingleChildViewport extends SingleChildRenderObjectWidget { |
||||||
|
const _SingleChildViewport({ |
||||||
|
required this.offset, |
||||||
|
Key? key, |
||||||
|
Widget? child, |
||||||
|
}) : super(key: key, child: child); |
||||||
|
|
||||||
|
final ViewportOffset offset; |
||||||
|
|
||||||
|
@override |
||||||
|
_RenderSingleChildViewport createRenderObject(BuildContext context) { |
||||||
|
return _RenderSingleChildViewport( |
||||||
|
offset: offset, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void updateRenderObject( |
||||||
|
BuildContext context, _RenderSingleChildViewport renderObject) { |
||||||
|
// Order dependency: The offset setter reads the axis direction. |
||||||
|
renderObject.offset = offset; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class _RenderSingleChildViewport extends RenderBox |
||||||
|
with RenderObjectWithChildMixin<RenderBox> |
||||||
|
implements RenderAbstractViewport { |
||||||
|
_RenderSingleChildViewport({ |
||||||
|
required ViewportOffset offset, |
||||||
|
double cacheExtent = RenderAbstractViewport.defaultCacheExtent, |
||||||
|
RenderBox? child, |
||||||
|
}) : _offset = offset, |
||||||
|
_cacheExtent = cacheExtent { |
||||||
|
this.child = child; |
||||||
|
} |
||||||
|
|
||||||
|
ViewportOffset get offset => _offset; |
||||||
|
ViewportOffset _offset; |
||||||
|
|
||||||
|
set offset(ViewportOffset value) { |
||||||
|
if (value == _offset) return; |
||||||
|
if (attached) _offset.removeListener(_hasScrolled); |
||||||
|
_offset = value; |
||||||
|
if (attached) _offset.addListener(_hasScrolled); |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent} |
||||||
|
double get cacheExtent => _cacheExtent; |
||||||
|
double _cacheExtent; |
||||||
|
|
||||||
|
set cacheExtent(double value) { |
||||||
|
if (value == _cacheExtent) return; |
||||||
|
_cacheExtent = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
void _hasScrolled() { |
||||||
|
markNeedsPaint(); |
||||||
|
markNeedsSemanticsUpdate(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void setupParentData(RenderObject child) { |
||||||
|
// We don't actually use the offset argument in BoxParentData, so let's |
||||||
|
// avoid allocating it at all. |
||||||
|
if (child.parentData is! ParentData) child.parentData = ParentData(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void attach(PipelineOwner owner) { |
||||||
|
super.attach(owner); |
||||||
|
_offset.addListener(_hasScrolled); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void detach() { |
||||||
|
_offset.removeListener(_hasScrolled); |
||||||
|
super.detach(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
bool get isRepaintBoundary => true; |
||||||
|
|
||||||
|
double get _viewportExtent { |
||||||
|
assert(hasSize); |
||||||
|
return size.height; |
||||||
|
} |
||||||
|
|
||||||
|
double get _minScrollExtent { |
||||||
|
assert(hasSize); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
double get _maxScrollExtent { |
||||||
|
assert(hasSize); |
||||||
|
if (child == null) return 0; |
||||||
|
return math.max(0, child!.size.height - size.height); |
||||||
|
} |
||||||
|
|
||||||
|
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
||||||
|
return constraints.widthConstraints(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double computeMinIntrinsicWidth(double height) { |
||||||
|
if (child != null) return child!.getMinIntrinsicWidth(height); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double computeMaxIntrinsicWidth(double height) { |
||||||
|
if (child != null) return child!.getMaxIntrinsicWidth(height); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double computeMinIntrinsicHeight(double width) { |
||||||
|
if (child != null) return child!.getMinIntrinsicHeight(width); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double computeMaxIntrinsicHeight(double width) { |
||||||
|
if (child != null) return child!.getMaxIntrinsicHeight(width); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
// We don't override computeDistanceToActualBaseline(), because we |
||||||
|
// want the default behavior (returning null). Otherwise, as you |
||||||
|
// scroll, it would shift in its parent if the parent was baseline-aligned, |
||||||
|
// which makes no sense. |
||||||
|
|
||||||
|
@override |
||||||
|
Size computeDryLayout(BoxConstraints constraints) { |
||||||
|
if (child == null) { |
||||||
|
return constraints.smallest; |
||||||
|
} |
||||||
|
final childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
||||||
|
return constraints.constrain(childSize); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void performLayout() { |
||||||
|
final constraints = this.constraints; |
||||||
|
if (child == null) { |
||||||
|
size = constraints.smallest; |
||||||
|
} else { |
||||||
|
child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
||||||
|
size = constraints.constrain(child!.size); |
||||||
|
} |
||||||
|
|
||||||
|
offset.applyViewportDimension(_viewportExtent); |
||||||
|
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
||||||
|
} |
||||||
|
|
||||||
|
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); |
||||||
|
|
||||||
|
Offset _paintOffsetForPosition(double position) { |
||||||
|
return Offset(0, -position); |
||||||
|
} |
||||||
|
|
||||||
|
bool _shouldClipAtPaintOffset(Offset paintOffset) { |
||||||
|
assert(child != null); |
||||||
|
return paintOffset.dx < 0 || |
||||||
|
paintOffset.dy < 0 || |
||||||
|
paintOffset.dx + child!.size.width > size.width || |
||||||
|
paintOffset.dy + child!.size.height > size.height; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void paint(PaintingContext context, Offset offset) { |
||||||
|
if (child != null) { |
||||||
|
final paintOffset = _paintOffset; |
||||||
|
|
||||||
|
void paintContents(PaintingContext context, Offset offset) { |
||||||
|
context.paintChild(child!, offset + paintOffset); |
||||||
|
} |
||||||
|
|
||||||
|
if (_shouldClipAtPaintOffset(paintOffset)) { |
||||||
|
_clipRectLayer.layer = context.pushClipRect( |
||||||
|
needsCompositing, |
||||||
|
offset, |
||||||
|
Offset.zero & size, |
||||||
|
paintContents, |
||||||
|
oldLayer: _clipRectLayer.layer, |
||||||
|
); |
||||||
|
} else { |
||||||
|
_clipRectLayer.layer = null; |
||||||
|
paintContents(context, offset); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final _clipRectLayer = LayerHandle<ClipRectLayer>(); |
||||||
|
|
||||||
|
@override |
||||||
|
void applyPaintTransform(RenderBox child, Matrix4 transform) { |
||||||
|
final paintOffset = _paintOffset; |
||||||
|
transform.translate(paintOffset.dx, paintOffset.dy); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Rect? describeApproximatePaintClip(RenderObject? child) { |
||||||
|
if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { |
||||||
|
return Offset.zero & size; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
||||||
|
if (child != null) { |
||||||
|
return result.addWithPaintOffset( |
||||||
|
offset: _paintOffset, |
||||||
|
position: position, |
||||||
|
hitTest: (result, transformed) { |
||||||
|
assert(transformed == position + -_paintOffset); |
||||||
|
return child!.hitTest(result, position: transformed); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, |
||||||
|
{Rect? rect}) { |
||||||
|
rect ??= target.paintBounds; |
||||||
|
if (target is! RenderBox) { |
||||||
|
return RevealedOffset(offset: offset.pixels, rect: rect); |
||||||
|
} |
||||||
|
|
||||||
|
final targetBox = target; |
||||||
|
final transform = targetBox.getTransformTo(child); |
||||||
|
final bounds = MatrixUtils.transformRect(transform, rect); |
||||||
|
|
||||||
|
final double leadingScrollOffset; |
||||||
|
final double targetMainAxisExtent; |
||||||
|
final double mainAxisExtent; |
||||||
|
|
||||||
|
mainAxisExtent = size.height; |
||||||
|
leadingScrollOffset = bounds.top; |
||||||
|
targetMainAxisExtent = bounds.height; |
||||||
|
|
||||||
|
final targetOffset = leadingScrollOffset - |
||||||
|
(mainAxisExtent - targetMainAxisExtent) * alignment; |
||||||
|
final targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); |
||||||
|
return RevealedOffset(offset: targetOffset, rect: targetRect); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void showOnScreen({ |
||||||
|
RenderObject? descendant, |
||||||
|
Rect? rect, |
||||||
|
Duration duration = Duration.zero, |
||||||
|
Curve curve = Curves.ease, |
||||||
|
}) { |
||||||
|
if (!offset.allowImplicitScrolling) { |
||||||
|
return super.showOnScreen( |
||||||
|
descendant: descendant, |
||||||
|
rect: rect, |
||||||
|
duration: duration, |
||||||
|
curve: curve, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
final newRect = RenderViewportBase.showInViewport( |
||||||
|
descendant: descendant, |
||||||
|
viewport: this, |
||||||
|
offset: offset, |
||||||
|
rect: rect, |
||||||
|
duration: duration, |
||||||
|
curve: curve, |
||||||
|
); |
||||||
|
super.showOnScreen( |
||||||
|
rect: newRect, |
||||||
|
duration: duration, |
||||||
|
curve: curve, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Rect describeSemanticsClip(RenderObject child) { |
||||||
|
return Rect.fromLTRB( |
||||||
|
semanticBounds.left, |
||||||
|
semanticBounds.top - cacheExtent, |
||||||
|
semanticBounds.right, |
||||||
|
semanticBounds.bottom + cacheExtent, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue