Skip to content

Instantly share code, notes, and snippets.

@jaysonss
Last active December 21, 2021 02:52
Show Gist options
  • Save jaysonss/0cfc71572687ce0207782af4099cc809 to your computer and use it in GitHub Desktop.
Save jaysonss/0cfc71572687ce0207782af4099cc809 to your computer and use it in GitHub Desktop.
inner_scroll_webview.dart
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class WebViewport extends SingleChildRenderObjectWidget {
const WebViewport({
Key key,
this.axisDirection = AxisDirection.down,
@required this.offset,
Widget child,
@required this.clipBehavior,
@required this.onScroll,
@required this.contentHeight,
}) : assert(axisDirection != null),
assert(clipBehavior != null),
super(key: key, child: child);
final AxisDirection axisDirection;
final ViewportOffset offset;
final Clip clipBehavior;
final ValueChanged<Offset> onScroll;
final double contentHeight;
@override
_RenderSingleChildViewport createRenderObject(BuildContext context) {
return _RenderSingleChildViewport(
axisDirection: axisDirection,
offset: offset,
clipBehavior: clipBehavior,
onScroll: onScroll,
contentHeight: contentHeight,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderSingleChildViewport renderObject) {
// Order dependency: The offset setter reads the axis direction.
renderObject
..axisDirection = axisDirection
..offset = offset
..contentHeight = contentHeight
..clipBehavior = clipBehavior;
}
}
class _RenderSingleChildViewport extends RenderBox
with RenderObjectWithChildMixin<RenderBox>
implements RenderAbstractViewport {
_RenderSingleChildViewport({
AxisDirection axisDirection = AxisDirection.down,
@required ViewportOffset offset,
double cacheExtent = RenderAbstractViewport.defaultCacheExtent,
RenderBox child,
@required Clip clipBehavior,
@required this.onScroll,
@required this.contentHeight,
}) : assert(axisDirection != null),
assert(offset != null),
assert(cacheExtent != null),
assert(clipBehavior != null),
_axisDirection = axisDirection,
_offset = offset,
_cacheExtent = cacheExtent,
_clipBehavior = clipBehavior {
this.child = child;
}
final ValueChanged<Offset> onScroll;
double contentHeight;
AxisDirection get axisDirection => _axisDirection;
AxisDirection _axisDirection;
set axisDirection(AxisDirection value) {
assert(value != null);
if (value == _axisDirection) return;
_axisDirection = value;
markNeedsLayout();
}
Axis get axis => axisDirectionToAxis(axisDirection);
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
assert(value != null);
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) {
assert(value != null);
if (value == _cacheExtent) return;
_cacheExtent = value;
markNeedsLayout();
}
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
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);
switch (axis) {
case Axis.horizontal:
return size.width;
case Axis.vertical:
return size.height;
}
return 0.0;
}
double get _minScrollExtent {
assert(hasSize);
return 0.0;
}
double get _maxScrollExtent {
assert(hasSize);
if (child == null) return 0.0;
switch (axis) {
case Axis.horizontal:
return math.max(0.0, child.size.width - size.width);
case Axis.vertical:
return math.max(0.0, contentHeight - size.height);
}
return 0.0;
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
switch (axis) {
case Axis.horizontal:
return constraints.heightConstraints();
case Axis.vertical:
return constraints.widthConstraints();
}
return null;
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) return child.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) return child.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) return child.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) return child.getMaxIntrinsicHeight(width);
return 0.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 Size childSize =
child.getDryLayout(_getInnerConstraints(constraints));
return constraints.constrain(childSize);
}
@override
void performLayout() {
final BoxConstraints 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) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return Offset(0.0, position - child.size.height + size.height);
case AxisDirection.down:
return Offset(0.0, -position);
case AxisDirection.left:
return Offset(position - child.size.width + size.width, 0.0);
case AxisDirection.right:
return Offset(-position, 0.0);
}
return Offset.zero;
}
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 Offset paintOffset = _paintOffset;
onScroll(paintOffset);
void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset);
}
if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
paintContents(context, offset);
}
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer =
LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(0.0, 0.0);
}
@override
Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtPaintOffset(_paintOffset))
return Offset.zero & size;
return null;
}
@override
bool hitTest(BoxHitTestResult result, {Offset position}) {
if (size.contains(position)) {
_addPositionToChild(child, result, position - _paintOffset);
return true;
}
return false;
}
void _addPositionToChild(
RenderObject obj, BoxHitTestResult result, Offset position) {
if (obj is RenderAndroidView || obj is RenderUiKitView) {
result.add(BoxHitTestEntry(obj, position));
return;
}
obj.visitChildren((child) {
_addPositionToChild(child, result, position);
});
}
@override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment,
{Rect rect}) {
rect ??= target.paintBounds;
if (target is! RenderBox)
return RevealedOffset(offset: offset.pixels, rect: rect);
final RenderBox targetBox = target;
final Matrix4 transform = targetBox.getTransformTo(child);
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Size contentSize = child.size;
double leadingScrollOffset;
double targetMainAxisExtent;
double mainAxisExtent;
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
mainAxisExtent = size.height;
leadingScrollOffset = contentSize.height - bounds.bottom;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.right:
mainAxisExtent = size.width;
leadingScrollOffset = bounds.left;
targetMainAxisExtent = bounds.width;
break;
case AxisDirection.down:
mainAxisExtent = size.height;
leadingScrollOffset = bounds.top;
targetMainAxisExtent = bounds.height;
break;
case AxisDirection.left:
mainAxisExtent = size.width;
leadingScrollOffset = contentSize.width - bounds.right;
targetMainAxisExtent = bounds.width;
break;
}
final double targetOffset = leadingScrollOffset -
(mainAxisExtent - targetMainAxisExtent) * alignment;
final Rect 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 Rect 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) {
assert(axis != null);
switch (axis) {
case Axis.vertical:
return Rect.fromLTRB(
semanticBounds.left,
semanticBounds.top - cacheExtent,
semanticBounds.right,
semanticBounds.bottom + cacheExtent,
);
case Axis.horizontal:
return Rect.fromLTRB(
semanticBounds.left - cacheExtent,
semanticBounds.top,
semanticBounds.right + cacheExtent,
semanticBounds.bottom,
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment