Last active
April 20, 2025 20:38
-
-
Save AlexV525/289a04caf088058137259b23b5dde6e0 to your computer and use it in GitHub Desktop.
SliverClipRRect
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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