dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
10 KiB
366 lines
10 KiB
4 years ago
|
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,
|
||
|
);
|
||
|
}
|
||
|
}
|