Skip to content

Add SliverClipRect and SliverClipRRect to the widgets library #179002

Description

@manu-sncf

1. Overview

This proposal introduces new widgets, SliverClipRect and SliverClipRRect. Those widgets serve as the Sliver equivalent of the existing ClipRect and ClipRRect widgets. It clips its child sliver using a rectangle / rounded rectangle and include specific logic to handle SliverConstraints.overlap, allowing for the visual removal of content that scrolls underneath pinned headers.

2. Background & Motivation

The Problem

Currently, the ClipRect and ClipRRect widgets only support RenderBox protocol.

There are two primary scenarios where clipping is essential in the Sliver world, but currently difficult to achieve:

  1. Visual Styling: Applying a hard clip to a group of slivers (e.g., inside a SliverMainAxisGroup or SliverDecoratedBox).
  2. Overlap Management: When using pinned headers (like SliverAppBar or SliverPersistentHeader) with transparent or semi-transparent backgrounds. By default, scrolling content paints underneath the pinned header. If the header is transparent, the scrolling content is visible, which is often undesirable. Developers need a way to "clip" the scrolling content exactly at the point where it overlaps with the pinned header.

The Solution

SliverClipRect/SliverClipRRect solve this by creating a RenderSliverClipRect/RenderSliverClipRRect. Those render object:

  1. Accept CustomClipper<Rect> and CustomClipper<RRect>.
  2. Optionally accounts for constraints.overlap to automatically clip the portion of the child obstructed by pinned headers.
  3. Correctly handles hit-testing, ensuring that clipped/overlapped areas do not consume touch events.

3. Design

API Surface

The API mirrors the existing ClipRect and ClipRRect but adds the clipOverlap property specific to the Sliver protocol.

class SliverClipRect extends SingleChildRenderObjectWidget {
  const SliverClipRect({
    super.key,
    super.child,
    this.clipper,
    this.clipBehavior = Clip.hardEdge,
    this.clipOverlap = true,
  });

  /// If non-null, determines which clip rectangle to use.
  final CustomClipper<Rect>? clipper;

  /// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
  final Clip clipBehavior;

  /// Whether to automatically clip the portion of the child that is overlapped
  /// by preceding pinned slivers.
  ///
  /// If true, the clip rect will be adjusted to exclude the area defined by
  /// [SliverConstraints.overlap].
  final bool clipOverlap;
}
class SliverClipRRect extends SingleChildRenderObjectWidget {
  /// Creates a sliver that clips its child using a rounded rectangle.
  const SliverClipRRect({
    super.key,
    required Widget sliver,
    this.borderRadius = BorderRadius.zero,
    this.clipper,
    this.clipBehavior = Clip.antiAlias,
    this.clipOverlap = true,
  }) : super(child: sliver);

  /// The border radius of the rounded corners.
  ///
  /// Values are clamped so that horizontal and vertical radii sums do not
  /// exceed width/height.
  ///
  /// This value is ignored if [clipper] is non-null.
  final BorderRadiusGeometry borderRadius;

  /// If non-null, determines which clip to use.
  final CustomClipper<RRect>? clipper;

  /// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
  final Clip clipBehavior;

  /// Whether to automatically clip the portion of the child that is overlapped
  /// by preceding pinned slivers.
  ///
  /// If true, the clip rect will be adjusted to exclude the area defined by
  /// [SliverConstraints.overlap].
  final bool clipOverlap;
}

Rendering Logic

The RenderSliverClipRect/RenderSliverClipRRect calculate the clip rect/rrect based on the child's paint bounds. If clipOverlap is true, it intersects the calculated clip rect with the visible portion of the viewport (excluding the overlap defined in constraints).

4. Use Cases

Use Case 1: Grouped Decoration with Hard Edges

This scenario involves a group of slivers wrapped in a decoration (e.g., a gradient). We want to ensure that if the decoration has specific bounds, the children are clipped strictly to those bounds, ignoring overlap logic.

Use Case 2: Transparent Pinned Header

This scenario involves a design where the SliverAppBar (or pinned header) has no background (transparent), but the Scaffold has a complex background (e.g., a gradient).

Without clipping, as items scroll up, they would become visible behind the text of the SliverAppBar because the App Bar is transparent. Using SliverClipRect with clipOverlap: true (the default) ensures items disappear the moment they hit the overlap point.

Example

The example below combines the two previous use cases and works with PR #179003.

Scaffold(
  body: DecoratedBox(
    decoration: BoxDecoration(
      gradient: LinearGradient(
        begin: Alignment.topCenter,
        end: Alignment.bottomCenter,
        colors: <Color>[Colors.blue.shade200, Colors.blue.shade800],
      ),
    ),
    child: CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar(
          pinned: true,
          title: Text('Group Clipping'),
          backgroundColor: Colors.transparent,
        ),
        SliverSafeArea(
          top: false,
          sliver: SliverPadding(
            padding: const EdgeInsets.all(16),
            sliver: SliverClipRRect(
              borderRadius: BorderRadius.circular(12),
              sliver: DecoratedSliver(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: <Color>[Colors.blue, Colors.purple],
                ),
                sliver: SliverMainAxisGroup(
                  slivers: <Widget>[
                    const PinnedHeaderSliver(
                      child: ListTile(
                        title: Text('Pinned Header', style: TextStyle(color: Colors.white)),
                      ),
                    ),
                    SliverClipRect(
                      sliver: SliverList.builder(
                        itemCount: 20,
                        itemBuilder: (BuildContext context, int index) => ListTile(
                          title: Text('Item $index', style: const TextStyle(color: Colors.white)),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ],
    ),
  ),
)
demo.webm

Optional proposal

In a new PR, we eventually can add : SliverClipOval, SliverClipPath and SliverRSuperellipse.

Metadata

Metadata

Assignees

Labels

Bot is counting down the days until it unassigns the issueP3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: new widgetMust also have "c: new feature". Request for a new widget.c: proposalA detailed proposal for a change to Flutterf: scrollingViewports, list views, slivers, etc.frameworkflutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework teamwaiting for PR to land (fixed)A fix is in flight

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions