-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
compose_box [nfc]: Add InsetShadowBox
This casts fixed size shadows on top of a child widget from its top edge and bottom edge. Signed-off-by: Zixuan James Li <[email protected]>
- Loading branch information
Showing
3 changed files
with
127 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import 'package:flutter/widgets.dart'; | ||
|
||
/// A widget that overlays rectangular inset shadows on a child. | ||
/// | ||
/// The use case of this is casting shadows on scrollable UI elements. | ||
/// For example, when there is a list of items, the shadows could be | ||
/// visual indicators for over scrolled areas. | ||
/// | ||
/// Note that this is a bit different from the CSS `box-shadow: inset`, | ||
/// because it only supports rectangular shadows. | ||
/// | ||
/// See also: | ||
/// * https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3860-11890&node-type=frame&t=oOVTdwGZgtvKv9i8-0 | ||
/// * https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset | ||
class InsetShadowBox extends StatelessWidget { | ||
const InsetShadowBox({ | ||
super.key, | ||
this.top = 0, | ||
this.bottom = 0, | ||
required this.color, | ||
required this.child, | ||
}); | ||
|
||
/// The distance that the shadow from the child's top edge grows downwards. | ||
/// | ||
/// This does not pad the child widget. | ||
final double top; | ||
|
||
/// The distance that the shadow from the child's bottom edge grows upwards. | ||
/// | ||
/// This does not pad the child widget. | ||
final double bottom; | ||
|
||
/// The shadow color to fade into transparency from the top and bottom borders. | ||
final Color color; | ||
|
||
final Widget child; | ||
|
||
BoxDecoration _shadowFrom(AlignmentGeometry begin) { | ||
return BoxDecoration(gradient: LinearGradient( | ||
begin: begin, end: -begin, | ||
colors: [color, color.withValues(alpha: 0)])); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Stack( | ||
// This is necessary to pass the constraints as-is, | ||
// so that the [Stack] is transparent during layout. | ||
fit: StackFit.passthrough, | ||
children: [ | ||
child, | ||
Positioned(top: 0, height: top, left: 0, right: 0, | ||
child: DecoratedBox(decoration: _shadowFrom(Alignment.topCenter))), | ||
Positioned(bottom: 0, height: bottom, left: 0, right: 0, | ||
child: DecoratedBox(decoration: _shadowFrom(Alignment.bottomCenter))), | ||
]); | ||
} | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import 'dart:ui' as ui; | ||
import 'package:checks/checks.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
import 'package:legacy_checks/legacy_checks.dart'; | ||
import 'package:zulip/widgets/inset_shadow.dart'; | ||
|
||
import '../flutter_checks.dart'; | ||
|
||
void main() { | ||
testWidgets('constraints from the parent are not modified', (tester) async { | ||
await tester.pumpWidget(const Directionality( | ||
textDirection: TextDirection.ltr, | ||
child: Align( | ||
// Position child at the top-left corner of the box at (0, 0) | ||
// to ease the check on [Rect] later. | ||
alignment: Alignment.topLeft, | ||
child: SizedBox(width: 20, height: 20, | ||
child: InsetShadowBox(top: 7, bottom: 3, | ||
color: Colors.red, | ||
child: SizedBox.shrink()))))); | ||
|
||
// We expect that the child of [InsetShadowBox] gets the constraints | ||
// from [InsetShadowBox]'s parent unmodified, so that the only effect of | ||
// the widget is adding shadows. | ||
final parentRect = tester.getRect(find.byType(SizedBox).at(0)); | ||
final childRect = tester.getRect(find.byType(SizedBox).at(1)); | ||
check(parentRect).equals(const Rect.fromLTRB(0, 0, 20, 20)); | ||
check(childRect).equals(parentRect); | ||
}); | ||
|
||
testWidgets('render shadow correctly', (tester) async { | ||
PaintPatternPredicate paintGradient({required Rect rect}) { | ||
// This is inspired by | ||
// https://github.com/flutter/flutter/blob/7b5462cc34af903e2f2de4be7540ff858685cdfc/packages/flutter/test/cupertino/route_test.dart#L1449-L1475 | ||
return (Symbol methodName, List<dynamic> arguments) { | ||
check(methodName).equals(#drawRect); | ||
check(arguments[0]).isA<Rect>().equals(rect); | ||
// We can't further check [ui.Gradient] because it is opaque: | ||
// https://github.com/flutter/engine/blob/07d01ad1199522fa5889a10c1688c4e1812b6625/lib/ui/painting.dart#L4487 | ||
check(arguments[1]).isA<Paint>().shader.isA<ui.Gradient>(); | ||
return true; | ||
}; | ||
} | ||
|
||
await tester.pumpWidget(const Directionality( | ||
textDirection: TextDirection.ltr, | ||
child: Center( | ||
// This would be forced to fill up the screen | ||
// if not wrapped in a widget like [Center]. | ||
child: SizedBox(width: 100, height: 100, | ||
child: InsetShadowBox(top: 3, bottom: 7, | ||
color: Colors.red, | ||
child: SizedBox(width: 30, height: 30)))))); | ||
|
||
final box = tester.renderObject(find.byType(InsetShadowBox)); | ||
check(box).legacyMatcher((paints | ||
// The coordinate system of these [Rect]'s is relative to the parent | ||
// of the [Gradient] from [InsetShadowBox], not the entire [FlutterView]. | ||
..something(paintGradient(rect: const Rect.fromLTRB(0, 0, 100, 0+3))) | ||
..something(paintGradient(rect: const Rect.fromLTRB(0, 100-7, 100, 100))) | ||
) as Matcher); | ||
}); | ||
} |