Refine reference liquid glass and resting grey cursor

This commit is contained in:
Codex
2026-10-14 08:52:33 +08:00
parent 57eb2c0505
commit 499c6a905e
5 changed files with 127 additions and 44 deletions
+41 -28
View File
@@ -117,7 +117,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
Widget _glass({required Widget child, bool lens = false}) {
final pressure = _pressure.clamp(0.0, 1.0);
final content = CustomPaint(
foregroundPainter: _GlassRimPainter(lens: lens),
foregroundPainter: _GlassRimPainter(lens: lens, pressure: pressure),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -125,17 +125,26 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
end: Alignment.bottomRight,
colors: lens
? [
const Color(0xFFD8DBE0)
.withValues(alpha: 0.14 * (1 - pressure * 0.82)),
const Color(0xFFC3C7CE)
.withValues(alpha: 0.08 * (1 - pressure * 0.82)),
const Color(0xFFE5E7EB)
.withValues(alpha: 0.12 * (1 - pressure * 0.82)),
Color.lerp(
const Color(0xFFA1A4AA).withValues(alpha: 0.34),
const Color(0xFFD8DBE0).withValues(alpha: 0.0252),
pressure,
)!,
Color.lerp(
const Color(0xFF8E939B).withValues(alpha: 0.26),
const Color(0xFFC3C7CE).withValues(alpha: 0.0144),
pressure,
)!,
Color.lerp(
const Color(0xFFB4B7BC).withValues(alpha: 0.32),
const Color(0xFFE5E7EB).withValues(alpha: 0.0216),
pressure,
)!,
]
: [
Colors.white.withValues(alpha: 0.028),
_blue.withValues(alpha: 0.005),
Colors.white.withValues(alpha: 0.012),
Colors.white.withValues(alpha: 0.05),
_blue.withValues(alpha: 0.008),
Colors.white.withValues(alpha: 0.018),
],
),
),
@@ -145,8 +154,8 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
return ClipRRect(
borderRadius: BorderRadius.circular(40),
child: GlassRefraction(
strength: lens ? 0.70 + pressure * 0.70 : 0.55,
blurSigma: lens ? 1.6 - pressure * 0.6 : 2,
strength: lens ? 0.70 + pressure * 0.70 : 0.95,
blurSigma: lens ? 1.6 - pressure * 0.6 : 1.4,
child: content,
),
);
@@ -292,11 +301,14 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
/// A diffuse silver bevel and thin directional glints show optical thickness.
/// The painted highlights stay on the edge so background colour passes through.
class _GlassRimPainter extends CustomPainter {
const _GlassRimPainter({required this.lens});
const _GlassRimPainter({required this.lens, required this.pressure});
final bool lens;
final double pressure;
@override
void paint(Canvas canvas, Size size) {
final edge = lens ? const Color(0xFF7B838F) : const Color(0xFFAFBECF);
// A quiet grey selection at rest; the glossy lens rim returns on touch.
final opacity = lens ? 0.25 + pressure * 0.75 : 1.0;
final bounds = Offset.zero & size;
final outer = RRect.fromRectAndRadius(
bounds.deflate(0.7),
@@ -311,14 +323,14 @@ class _GlassRimPainter extends CustomPainter {
Paint()
..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.8 : 2.2
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.9)
..maskFilter = MaskFilter.blur(BlurStyle.normal, lens ? 0.9 : 0.45)
..shader = ui.Gradient.linear(
bounds.topLeft,
bounds.bottomRight,
[
Colors.white.withValues(alpha: 0.24),
edge.withValues(alpha: 0.04),
Colors.white.withValues(alpha: 0.14),
Colors.white.withValues(alpha: 0.24 * opacity),
edge.withValues(alpha: 0.04 * opacity),
Colors.white.withValues(alpha: 0.14 * opacity),
],
[0.0, 0.55, 1.0],
),
@@ -331,12 +343,12 @@ class _GlassRimPainter extends CustomPainter {
..shader = ui.Gradient.sweep(
bounds.center,
[
Colors.white.withValues(alpha: 0.90),
edge.withValues(alpha: lens ? 0.30 : 0.15),
Colors.white.withValues(alpha: 0.10),
Colors.white.withValues(alpha: 0.86),
edge.withValues(alpha: 0.12),
Colors.white.withValues(alpha: 0.90),
Colors.white.withValues(alpha: 0.96 * opacity),
edge.withValues(alpha: (lens ? 0.30 : 0.15) * opacity),
Colors.white.withValues(alpha: 0.10 * opacity),
Colors.white.withValues(alpha: 0.94 * opacity),
edge.withValues(alpha: 0.12 * opacity),
Colors.white.withValues(alpha: 0.96 * opacity),
],
[0.0, 0.18, 0.38, 0.60, 0.80, 1.0],
TileMode.clamp,
@@ -353,9 +365,9 @@ class _GlassRimPainter extends CustomPainter {
bounds.topLeft,
bounds.bottomRight,
[
Colors.white.withValues(alpha: 0.20),
edge.withValues(alpha: lens ? 0.13 : 0.07),
Colors.white.withValues(alpha: 0.28),
Colors.white.withValues(alpha: 0.20 * opacity),
edge.withValues(alpha: (lens ? 0.13 : 0.07) * opacity),
Colors.white.withValues(alpha: 0.28 * opacity),
],
[0.0, 0.5, 1.0],
),
@@ -379,7 +391,7 @@ class _GlassRimPainter extends CustomPainter {
Offset(size.width, 0),
[
Colors.white.withValues(alpha: 0),
Colors.white.withValues(alpha: 0.50),
Colors.white.withValues(alpha: 0.50 * opacity),
Colors.white.withValues(alpha: 0),
],
[0.0, 0.5, 1.0],
@@ -388,5 +400,6 @@ class _GlassRimPainter extends CustomPainter {
}
@override
bool shouldRepaint(_GlassRimPainter oldDelegate) => oldDelegate.lens != lens;
bool shouldRepaint(_GlassRimPainter oldDelegate) =>
oldDelegate.lens != lens || oldDelegate.pressure != pressure;
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: hepan_everyday
description: TPSchedule 学生课表
publish_to: none
version: 0.5.1+9
version: 0.5.2+10
environment:
sdk: ^3.13.5
dependencies:
+61 -14
View File
@@ -9,6 +9,28 @@ import 'package:hepan_everyday/widgets/glass_dock.dart';
import 'package:hepan_everyday/widgets/glass_refraction.dart';
void main() {
testWidgets('resting cursor is neutral grey and clears while held', (
tester,
) async {
final resting = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
backdropColor: Colors.white,
preview: true,
);
final selected = _channel(resting, 46, 52, 0);
final base = _channel(resting, 160, 52, 0);
expect(base - selected, greaterThan(20));
expect((_channel(resting, 46, 52, 2) - selected).abs(), lessThan(12));
final held = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
backdropColor: Colors.white,
held: true,
preview: true,
);
expect(_channel(held, 46, 52, 0) - selected, greaterThan(18));
});
testWidgets(
'glass softens fine background detail without whitening its colour',
(tester) async {
@@ -37,13 +59,13 @@ void main() {
GlassDock(selectedIndex: 0, onSelected: (_) {}),
);
// Below the symbol and away from the rim, the selected lens should retain
// most of this saturated cyan backdrop instead of painting a grey pill.
// this saturated cyan backdrop while applying the new resting grey tint.
final red = _channel(pixels, 46, 52, 0);
final green = _channel(pixels, 46, 52, 1);
final blue = _channel(pixels, 46, 52, 2);
expect(red, lessThan(50));
expect(green - red, greaterThan(130));
expect(blue - red, greaterThan(165));
expect(red, lessThan(85));
expect(green - red, greaterThan(95));
expect(blue - red, greaterThan(140));
},
);
@@ -63,7 +85,9 @@ void main() {
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
expect(contrast, lessThan(30), reason: 'background at x=$start');
// Fine stripes start at 180 contrast levels; at least two thirds softens,
// while the stronger curved rim can enlarge individual stripe details.
expect(contrast, lessThan(60), reason: 'background at x=$start');
}
});
@@ -140,6 +164,8 @@ Future<ByteData> _render(
bool gradient = false,
bool preview = false,
Offset offset = Offset.zero,
Color backdropColor = const Color(0xFF14B4E6),
bool held = false,
}) async {
tester.view.physicalSize = const Size(800, 600);
tester.view.devicePixelRatio = 1;
@@ -161,7 +187,12 @@ Future<ByteData> _render(
children: [
Positioned.fill(
child: CustomPaint(
painter: _Backdrop(stripes, gradient, stripeWidth),
painter: _Backdrop(
stripes,
gradient,
stripeWidth,
backdropColor,
),
),
),
Positioned.fill(child: glass),
@@ -180,10 +211,19 @@ Future<ByteData> _render(
);
}
await tester.pumpAndSettle();
final gesture = held
? await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
)
: null;
if (held) {
await tester.pump(const Duration(milliseconds: 110));
await tester.pump(const Duration(milliseconds: 200));
}
if (ui.ImageFilter.isShaderFilterSupported) {
expect(find.byType(BackdropFilter), findsNothing);
}
return (await tester.runAsync(() async {
final result = (await tester.runAsync(() async {
final boundary =
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 1);
@@ -202,7 +242,12 @@ Future<ByteData> _render(
final png = (await previewImage.toByteData(
format: ui.ImageByteFormat.png,
))!;
final output = File('../dist/screenshots/glass-frosted-dock.png');
final name = backdropColor == Colors.white
? held
? 'liquid-glass-held.png'
: 'liquid-glass-rest.png'
: 'liquid-glass-detail.png';
final output = File('../dist/screenshots/$name');
await output.parent.create(recursive: true);
await output.writeAsBytes(png.buffer.asUint8List());
previewImage.dispose();
@@ -225,13 +270,17 @@ Future<ByteData> _render(
image.dispose();
return cropped;
}))!;
await gesture?.cancel();
await tester.pumpAndSettle();
return result;
}
class _Backdrop extends CustomPainter {
const _Backdrop(this.stripes, this.gradient, this.stripeWidth);
const _Backdrop(this.stripes, this.gradient, this.stripeWidth, this.colour);
final bool stripes;
final bool gradient;
final double stripeWidth;
final Color colour;
@override
void paint(Canvas canvas, Size size) {
@@ -245,10 +294,7 @@ class _Backdrop extends CustomPainter {
return;
}
if (!stripes) {
canvas.drawRect(
Offset.zero & size,
Paint()..color = const Color(0xFF14B4E6),
);
canvas.drawRect(Offset.zero & size, Paint()..color = colour);
return;
}
for (var x = 0.0; x < size.width; x += stripeWidth) {
@@ -266,5 +312,6 @@ class _Backdrop extends CustomPainter {
bool shouldRepaint(_Backdrop oldDelegate) =>
oldDelegate.stripes != stripes ||
oldDelegate.gradient != gradient ||
oldDelegate.stripeWidth != stripeWidth;
oldDelegate.stripeWidth != stripeWidth ||
oldDelegate.colour != colour;
}