Skip to content

Instantly share code, notes, and snippets.

@AlexV525
Last active April 20, 2025 20:38
Show Gist options
  • Save AlexV525/289a04caf088058137259b23b5dde6e0 to your computer and use it in GitHub Desktop.
Save AlexV525/289a04caf088058137259b23b5dde6e0 to your computer and use it in GitHub Desktop.
SliverClipRRect
// Author: Alex Li (https://github.com/AlexV525)
// Date: 2025/04/20
//
// Complete by the Gemini 2.5 Flash within 5 talks.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// A sliver that clips its child using a rounded rectangle.
///
/// This sliver is similar to [ClipRRect], but works specifically on a [Sliver].
class SliverClipRRect extends SingleChildRenderObjectWidget {
/// Creates a sliver that clips its child using a rounded rectangle.
const SliverClipRRect({
super.key,
required this.borderRadius,
required Widget sliver,
}) : super(child: sliver);
/// The border radius of the rounded corners.
final BorderRadiusGeometry borderRadius;
/// We need to resolve the [BorderRadiusGeometry] to a concrete
/// [BorderRadius] because [RenderObject] doesn't have access to the
/// [BuildContext] (which is needed for [Directionality]).
BorderRadius _resolveBorderRadiusFromDirectionality(BuildContext context) {
final direction = Directionality.of(context);
final resolvedBorderRadius = borderRadius.resolve(direction);
return resolvedBorderRadius;
}
@override
void updateRenderObject(
BuildContext context,
RenderSliverClipRRect renderObject,
) {
renderObject.borderRadius = _resolveBorderRadiusFromDirectionality(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<BorderRadiusGeometry>('borderRadius', borderRadius),
);
}
@override
RenderSliverClipRRect createRenderObject(BuildContext context) {
final borderRadius = _resolveBorderRadiusFromDirectionality(context);
return RenderSliverClipRRect(borderRadius: borderRadius);
}
}
/// The custom [RenderSliver] that performs the clipping.
class RenderSliverClipRRect extends RenderSliver
with RenderObjectWithChildMixin<RenderSliver> {
RenderSliverClipRRect({
required BorderRadius borderRadius,
}) : _borderRadius = borderRadius;
BorderRadius _borderRadius;
BorderRadius get borderRadius => _borderRadius;
set borderRadius(BorderRadius value) {
if (_borderRadius == value) {
return;
}
_borderRadius = value;
// If the border radius changes, we need to repaint.
markNeedsPaint();
}
// Layout is simple: just lay out the child with the same constraints.
@override
void performLayout() {
child?.layout(constraints);
// Copy the geometry from the child. The clipping doesn't affect the
// layout or how much space the sliver takes.
geometry = child?.geometry ?? SliverGeometry.zero;
}
// Paint the sliver, applying a clip before painting the child.
@override
void paint(PaintingContext context, Offset offset) {
// If there's no child or nothing to paint, do nothing.
if (child == null || geometry!.paintExtent <= 0) {
return;
}
// Determine the bounds of the area to clip.
// This is the area the sliver is currently painting within the viewport.
// The rect is relative to the sliver's paint origin (which is at offset).
final Rect bounds =
Offset.zero & Size(constraints.crossAxisExtent, geometry!.paintExtent);
// Convert the BorderRadius to an RRect based on the bounds.
final RRect clipRRect = borderRadius.toRRect(bounds);
// Apply the clip using the PaintingContext.
context.pushClipRRect(
true, // Clipping typically requires a separate layer.
offset, // The offset where the sliver starts painting.
bounds, // Relative to the offset that define the clipping area.
clipRRect, // The [RRect] defining the rounded corners within the bounds.
(context, offset) {
// Paint the child within the clipped area.
child!.paint(context, offset);
},
);
}
/// Since we override paint and use [pushClipRRect],
/// we need to handle hit testing.
///
/// If the child is null, we don't hit test. Otherwise, delegate to the child.
@override
bool hitTestChildren(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
if (child == null) {
return false;
}
// The hit testing position needs to be relative to the child's origin.
// In this case, the child's origin is the same as the parent's paint origin.
return child!.hitTest(
result,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
);
}
/// This method applies the paint transform from a descendant's
/// coordinate space up to this [RenderObject]'s coordinate space.
/// Since this clipper doesn't change the child's coordinate space
/// relative to its paint origin, we just delegate the call to the child.
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
// The descendant is asking for the transform from its space up to
// this object's space. Since the child's paint space is the same as
// this object's paint space relative to their respective paint origins,
// we just pass the call to the child.
this.child?.applyPaintTransform(child, transform);
}
/// Set up [ParentData] for the child.
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalParentData) {
child.parentData = SliverPhysicalParentData();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment