Created
February 18, 2026 18:11
-
-
Save Piinks/f08c63b86a0749aef73c08456d9c8bee to your computer and use it in GitHub Desktop.
Sliver clip Rect and RRect etst cases
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
| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| import 'package:flutter_test/flutter_test.dart'; | |
| void main() { | |
| testWidgets('SliverClipRRect should have a straight cut at overlap, not rounded', (WidgetTester tester) async { | |
| final ScrollController controller = ScrollController(); | |
| await tester.pumpWidget( | |
| WidgetsApp( | |
| color: const Color(0xffffffff), | |
| onGenerateRoute: (settings) => PageRouteBuilder( | |
| pageBuilder: (_, _, _) => CustomScrollView( | |
| controller: controller, | |
| slivers: <Widget>[ | |
| const SliverPersistentHeader( | |
| delegate: _SliverPersistentHeaderDelegate(height: 100), | |
| pinned: true, | |
| ), | |
| SliverClipRRect( | |
| borderRadius: const BorderRadius.all(Radius.circular(50)), | |
| sliver: SliverToBoxAdapter( | |
| child: Container( | |
| height: 200, | |
| color: const Color(0xFF2196F3), | |
| key: const Key('sliver_child'), | |
| ), | |
| ), | |
| ), | |
| const SliverToBoxAdapter(child: SizedBox(height: 1000)), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| // Scroll so that the SliverClipRRect is partially under the pinned header. | |
| // Header is 100px. Scroll by 50px. | |
| // The SliverClipRRect now starts at viewport 50. | |
| // It is overlapped by the header from viewport 50 to 100 (50px of overlap). | |
| controller.jumpTo(50); | |
| await tester.pump(); | |
| final RenderSliverClipRRect renderSliver = tester.renderObject(find.byType(SliverClipRRect)); | |
| expect(renderSliver.constraints.overlap, equals(50.0)); | |
| // The SliverClipRRect is at viewport 50. Local y=0 is at viewport 50. | |
| // Overlap is 50px, so local y=0 to y=50 are overlapped. | |
| // The cut should be at local y=50. | |
| // If the cut is STRAIGHT, then local (1, 51) should be INSIDE the clip. | |
| // If the cut is ROUNDED (radius 50), then local (1, 51) is in the corner and should be OUTSIDE. | |
| // Note: mainAxisPosition is relative to the start of the sliver. | |
| // crossAxisPosition is relative to the left edge (in LTR). | |
| final result = SliverHitTestResult(); | |
| final bool hit = renderSliver.hitTest( | |
| result, | |
| mainAxisPosition: 51.0, // Just below the overlap cut | |
| crossAxisPosition: 1.0, // Near the left edge | |
| ); | |
| // If hit is false, it means it was clipped. | |
| // We WANT hit to be true for a straight cut. | |
| expect(hit, isTrue, reason: 'Overlap cut should be straight, but it seems to be rounded (clipped the corner).'); | |
| }); | |
| testWidgets('RenderSliverClipRRect.buildClip should use total height, not middleRect.height', (WidgetTester tester) async { | |
| // This test verifies Issue 2: usage of middleRect.height in buildClip. | |
| // If it uses middleRect.height, the clipOrigin calculation will be wrong when scrollOffset is large. | |
| final ScrollController controller = ScrollController(); | |
| await tester.pumpWidget( | |
| WidgetsApp( | |
| color: const Color(0xffffffff), | |
| onGenerateRoute: (settings) => PageRouteBuilder( | |
| pageBuilder: (_, _, _) => CustomScrollView( | |
| controller: controller, | |
| slivers: <Widget>[ | |
| const SliverPersistentHeader( | |
| delegate: _SliverPersistentHeaderDelegate(height: 100), | |
| pinned: true, | |
| ), | |
| SliverClipRRect( | |
| borderRadius: const BorderRadius.all(Radius.circular(40)), | |
| sliver: SliverToBoxAdapter( | |
| child: Container( | |
| height: 100, // Total height 100. middleRect.height is 100 - 40 - 40 = 20. | |
| color: const Color(0xFF2196F3), | |
| ), | |
| ), | |
| ), | |
| const SliverToBoxAdapter(child: SizedBox(height: 1000)), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| // Sliver is 100px tall. borderRadius 40. middleRect height is 20. | |
| // Header 100px. | |
| // If we scroll 90px. | |
| // Sliver starts at -90. Header covers 0 to 100. | |
| // Sliver is under header from viewport 0 to 10 (local 90 to 100). | |
| // Actually, local 0 to 90 are off-screen. | |
| // Local 90 to 100 are under header. | |
| // We should clip local 0 to 100? (Wait, I need to check my manual math again). | |
| // Let's just check if it's clipped when it SHOULD be. | |
| // If scrollOffset = 50. Overlap = 100. | |
| // Correct clipOrigin should be 100 (if we clip everything under header). | |
| // Formula: overlap (100) - max(scrollOffset (50) + overlap (100) - clipExtent, 0). | |
| // If clipExtent is 100: clipOrigin = 100 - max(50, 0) = 50. | |
| // Wait, if scrollOffset is 50 and overlap is 100. | |
| // Sliver local 0 to 50 are off-screen. | |
| // Local 50 is at viewport 0. | |
| // Header covers viewport 0 to 100. | |
| // So local 50 to 150 are under header? (But sliver only goes to 100). | |
| // So local 50 to 100 are under header. | |
| // We should clip up to local 100. | |
| // Formula gave 50. Why? | |
| // If clipOrigin is 50, we clip local 0 to 50. | |
| // But local 0 to 50 are off-screen anyway! | |
| // So we clip NOTHING that is on-screen. | |
| // So the entire sliver (from viewport 0 to 50) is visible UNDER the header. | |
| // THIS IS WRONG. The formula itself seems suspect, OR I misunderstand 'overlap'. | |
| // Regardless, if clipExtent is 20 (middleRect.height) instead of 100: | |
| // clipOrigin = 100 - max(50 + 100 - 20, 0) = 100 - 130 = -30 -> clamped? No, formula doesn't clamp yet. | |
| // If it's -30, it definitely clips less than 50. | |
| // So it's even worse. | |
| // Let's see what happens with current implementation. | |
| controller.jumpTo(50); | |
| await tester.pump(); | |
| final RenderSliverClipRRect renderSliver = tester.renderObject(find.byType(SliverClipRRect)); | |
| // If middleRect.height was used, clipExtent is 20. | |
| // clipOrigin = 100 - max(50+100-20, 0) = -30. | |
| // AxisDirection.down => newClip.copyWith(top: -30). | |
| // Original top was 0. New top is -30. So it clips LESS. | |
| // Let's test hit at local 25. Viewport position: 50 - 25 = -25? No. | |
| // Local 0 is at viewport -50. | |
| // Local 25 is at viewport -25. | |
| // Local 50 is at viewport 0. | |
| // Local 75 is at viewport 25. | |
| // Viewport 0 to 100 is covered by header. | |
| // So local 50 to 100 are covered by header. | |
| // We should NOT be able to hit at local 75. | |
| final result = SliverHitTestResult(); | |
| final bool hit = renderSliver.hitTest( | |
| result, | |
| mainAxisPosition: 75.0, | |
| crossAxisPosition: 400.0, | |
| ); | |
| expect(hit, isFalse, reason: 'Should NOT hit at local 75 because it is under the 100px header (sliver starts at -50)'); | |
| }); | |
| } | |
| class _SliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { | |
| const _SliverPersistentHeaderDelegate({required this.height}); | |
| final double height; | |
| @override | |
| Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => | |
| SizedBox(height: height, child: Container(color: const Color(0xFFFF0000))); | |
| @override | |
| double get maxExtent => height; | |
| @override | |
| double get minExtent => height; | |
| @override | |
| bool shouldRebuild(covariant _SliverPersistentHeaderDelegate oldDelegate) => height != oldDelegate.height; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment