7c9627fffb
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
742 lines
21 KiB
Dart
742 lines
21 KiB
Dart
import 'dart:ui' show ImageFilter;
|
|
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/providers/config.dart';
|
|
import 'package:fl_clash/providers/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
class PreviewChoice<T> {
|
|
const PreviewChoice({
|
|
required this.value,
|
|
required this.label,
|
|
required this.pictogram,
|
|
});
|
|
|
|
final T value;
|
|
final String label;
|
|
final Widget pictogram;
|
|
}
|
|
|
|
class PreviewChoiceGroup<T> extends StatelessWidget {
|
|
const PreviewChoiceGroup({
|
|
super.key,
|
|
required this.info,
|
|
required this.choices,
|
|
required this.value,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final Info info;
|
|
final List<PreviewChoice<T>> choices;
|
|
final T value;
|
|
final ValueChanged<T> onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
InfoHeader(info: info),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
child: generateSection(
|
|
items: [
|
|
for (final choice in choices)
|
|
DecorationListItem(
|
|
leading: ExcludeSemantics(child: choice.pictogram),
|
|
title: Text(choice.label),
|
|
isSelected: choice.value == value,
|
|
trailing: choice.value == value
|
|
? const SelectionCheck()
|
|
: null,
|
|
onPressed: () => onChanged(choice.value),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The foot of a [MiniScreen], where the options differ, drawn at the size of
|
|
/// an icon.
|
|
class MiniScreenThumb extends StatelessWidget {
|
|
const MiniScreenThumb({super.key, required this.screen});
|
|
|
|
static const Size _screenSize = Size(96, 128);
|
|
static const double _shownHeight = 66;
|
|
static const double _height = 30;
|
|
|
|
final Widget screen;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
height: _height,
|
|
width: _height * _screenSize.width / _shownHeight,
|
|
child: ClipRSuperellipse(
|
|
borderRadius: AppRadius.sm,
|
|
child: FittedBox(
|
|
child: SizedBox(
|
|
width: _screenSize.width,
|
|
height: _shownHeight,
|
|
child: ClipRect(
|
|
child: OverflowBox(
|
|
alignment: Alignment.bottomCenter,
|
|
minHeight: _screenSize.height,
|
|
maxHeight: _screenSize.height,
|
|
child: screen,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ColorSchemeTween extends Tween<ColorScheme> {
|
|
_ColorSchemeTween({super.end});
|
|
|
|
@override
|
|
ColorScheme lerp(double t) => ColorScheme.lerp(begin!, end!, t);
|
|
}
|
|
|
|
/// The home page as the theme settings will draw it; its destinations can be
|
|
/// tapped to try the tab animation.
|
|
class ThemeLivePreview extends ConsumerStatefulWidget {
|
|
const ThemeLivePreview({super.key});
|
|
|
|
@override
|
|
ConsumerState<ThemeLivePreview> createState() => _ThemeLivePreviewState();
|
|
}
|
|
|
|
class _ThemeLivePreviewState extends ConsumerState<ThemeLivePreview>
|
|
with SingleTickerProviderStateMixin {
|
|
static const double _phoneWidth = 168;
|
|
static const _duration = Duration(milliseconds: 300);
|
|
|
|
late final AnimationController _slide = AnimationController(
|
|
vsync: this,
|
|
duration: kTabScrollDuration,
|
|
);
|
|
late final CurvedAnimation _slideCurve = CurvedAnimation(
|
|
parent: _slide,
|
|
curve: Curves.easeOut,
|
|
);
|
|
int _selected = 0;
|
|
int? _previous;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
ref.listenManual(
|
|
appSettingProvider.select((state) => state.tabAnimation),
|
|
(_, _) => _select((_selected + 1) % MiniScreen.destinationCount),
|
|
);
|
|
}
|
|
|
|
Future<void> _select(int index) async {
|
|
if (index == _selected) {
|
|
return;
|
|
}
|
|
final tabAnimation = ref.read(appSettingProvider).tabAnimation;
|
|
final fade = tabAnimation == TabAnimation.fade;
|
|
_slide.duration = fade ? fadeTabDuration : kTabScrollDuration;
|
|
_slideCurve.curve = fade ? fadeTabCurve : Curves.easeOut;
|
|
setState(() {
|
|
_previous = _selected;
|
|
_selected = index;
|
|
});
|
|
await _slide.forward(from: 0).orCancel.catchError((_) {});
|
|
if (mounted && _selected == index) {
|
|
setState(() => _previous = null);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_slideCurve.dispose();
|
|
_slide.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final themeMode = ref.watch(
|
|
themeSettingProvider.select((state) => state.themeMode),
|
|
);
|
|
final brightness = switch (themeMode) {
|
|
ThemeMode.light => Brightness.light,
|
|
ThemeMode.dark => Brightness.dark,
|
|
ThemeMode.system => MediaQuery.platformBrightnessOf(context),
|
|
};
|
|
final colorScheme = ref.watch(genColorSchemeProvider(brightness));
|
|
final floatingBar = ref.watch(
|
|
appSettingProvider.select((state) => state.floatingNavigationBar),
|
|
);
|
|
final tabAnimation = ref.watch(
|
|
appSettingProvider.select((state) => state.tabAnimation),
|
|
);
|
|
final liquidGlass = ref.watch(
|
|
themeSettingProvider.select((state) => state.liquidGlass),
|
|
);
|
|
final corner = AppCorner.fit(_phoneWidth);
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
alignment: Alignment.center,
|
|
decoration: ShapeDecoration(
|
|
color: context.raisedSurfaceLevel.colorOf(context.colorScheme),
|
|
shape: AppShape.xxl,
|
|
),
|
|
child: SizedBox(
|
|
width: _phoneWidth,
|
|
child: AspectRatio(
|
|
aspectRatio: 9 / 17,
|
|
child: ClipRSuperellipse(
|
|
borderRadius: AppRadius.all(corner),
|
|
child: TweenAnimationBuilder<ColorScheme>(
|
|
tween: _ColorSchemeTween(end: colorScheme),
|
|
duration: _duration,
|
|
builder: (_, colorScheme, _) => AnimatedSwitcher(
|
|
duration: _duration,
|
|
child: AnimatedBuilder(
|
|
key: ValueKey((floatingBar, liquidGlass)),
|
|
animation: _slideCurve,
|
|
builder: (_, _) => MiniScreen(
|
|
colorScheme: colorScheme,
|
|
floatingBar: floatingBar,
|
|
liquidGlass: liquidGlass,
|
|
selected: _selected,
|
|
previous: _previous,
|
|
progress: _previous == null ? 1 : _slideCurve.value,
|
|
tabAnimation: tabAnimation,
|
|
onSelect: _select,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
enum _MiniPage { cards, list }
|
|
|
|
/// A phone-shaped sketch of the home page, drawn in [colorScheme].
|
|
///
|
|
/// With [previous], the page is caught [progress] of the way through a switch
|
|
/// from it, drawn as [tabAnimation] moves the pages.
|
|
class MiniScreen extends StatelessWidget {
|
|
const MiniScreen({
|
|
super.key,
|
|
required this.colorScheme,
|
|
required this.floatingBar,
|
|
this.liquidGlass = false,
|
|
this.selected = 0,
|
|
this.previous,
|
|
this.progress = 1,
|
|
this.tabAnimation = TabAnimation.slide,
|
|
this.onSelect,
|
|
});
|
|
|
|
static const int destinationCount = 4;
|
|
|
|
final ColorScheme colorScheme;
|
|
final bool floatingBar;
|
|
|
|
/// Draws a floating bar as glass; a docked one stays a fill, as the app's
|
|
/// does.
|
|
final bool liquidGlass;
|
|
|
|
final int selected;
|
|
final int? previous;
|
|
final double progress;
|
|
final TabAnimation tabAnimation;
|
|
final ValueChanged<int>? onSelect;
|
|
|
|
bool get _hasFab {
|
|
final previous = this.previous;
|
|
return previous == null || progress > 0.5 ? selected == 0 : previous == 0;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final sketch = SizedBox.expand(
|
|
child: CustomPaint(
|
|
painter: _MiniScreenPainter(
|
|
colorScheme: colorScheme,
|
|
floatingBar: floatingBar,
|
|
liquidGlass: liquidGlass,
|
|
selected: selected,
|
|
previous: previous,
|
|
progress: progress,
|
|
tabAnimation: tabAnimation,
|
|
hasFab: _hasFab,
|
|
),
|
|
),
|
|
);
|
|
final onSelect = this.onSelect;
|
|
if (onSelect == null) {
|
|
return sketch;
|
|
}
|
|
return LayoutBuilder(
|
|
builder: (_, constraints) {
|
|
final geometry = _MiniGeometry(
|
|
constraints.biggest,
|
|
floatingBar: floatingBar,
|
|
hasFab: _hasFab,
|
|
);
|
|
return Stack(
|
|
children: [
|
|
sketch,
|
|
Positioned.fromRect(
|
|
rect: geometry.bar,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
for (var i = 0; i < destinationCount; i++)
|
|
Expanded(
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => onSelect(i),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class MiniSplitScreen extends StatelessWidget {
|
|
const MiniSplitScreen({super.key, required this.light, required this.dark});
|
|
|
|
final Widget light;
|
|
final Widget dark;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
light,
|
|
ClipPath(clipper: _DiagonalClipper(), child: dark),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DiagonalClipper extends CustomClipper<Path> {
|
|
@override
|
|
Path getClip(Size size) {
|
|
return Path()
|
|
..moveTo(size.width * 0.7, 0)
|
|
..lineTo(size.width, 0)
|
|
..lineTo(size.width, size.height)
|
|
..lineTo(size.width * 0.3, size.height)
|
|
..close();
|
|
}
|
|
|
|
@override
|
|
bool shouldReclip(_DiagonalClipper oldClipper) => false;
|
|
}
|
|
|
|
class _MiniGeometry {
|
|
_MiniGeometry(this.size, {required bool floatingBar, required bool hasFab})
|
|
: unit = size.width / 20 {
|
|
margin = unit * 1.5;
|
|
fabSize = unit * 3.2;
|
|
final barHeight = floatingBar ? unit * 3.2 : unit * 3.6;
|
|
pageBottom = floatingBar ? 0 : barHeight;
|
|
if (floatingBar) {
|
|
final right = margin + (hasFab ? fabSize + unit * 0.6 : 0);
|
|
bar = Rect.fromLTWH(
|
|
margin,
|
|
size.height - margin - barHeight,
|
|
size.width - right - margin,
|
|
barHeight,
|
|
);
|
|
fab = Rect.fromLTWH(
|
|
size.width - margin - fabSize,
|
|
size.height - margin - fabSize,
|
|
fabSize,
|
|
fabSize,
|
|
);
|
|
} else {
|
|
bar = Rect.fromLTWH(0, size.height - barHeight, size.width, barHeight);
|
|
fab = Rect.fromLTWH(
|
|
size.width - margin - fabSize * 1.6,
|
|
size.height - (barHeight + margin) - fabSize,
|
|
fabSize * 1.6,
|
|
fabSize,
|
|
);
|
|
}
|
|
}
|
|
|
|
final Size size;
|
|
final double unit;
|
|
late final double margin;
|
|
late final double fabSize;
|
|
late final double pageBottom;
|
|
late final Rect bar;
|
|
late final Rect fab;
|
|
}
|
|
|
|
class _MiniScreenPainter extends CustomPainter {
|
|
const _MiniScreenPainter({
|
|
required this.colorScheme,
|
|
required this.floatingBar,
|
|
required this.liquidGlass,
|
|
required this.selected,
|
|
required this.previous,
|
|
required this.progress,
|
|
required this.tabAnimation,
|
|
required this.hasFab,
|
|
});
|
|
|
|
final ColorScheme colorScheme;
|
|
final bool floatingBar;
|
|
final bool liquidGlass;
|
|
final int selected;
|
|
final int? previous;
|
|
final double progress;
|
|
final TabAnimation tabAnimation;
|
|
final bool hasFab;
|
|
|
|
bool get _glass => floatingBar && liquidGlass;
|
|
|
|
static _MiniPage _pageOf(int index) =>
|
|
index.isEven ? _MiniPage.cards : _MiniPage.list;
|
|
|
|
static void _fill(
|
|
Canvas canvas,
|
|
Rect rect,
|
|
Color color,
|
|
ShapeBorder shape, {
|
|
List<BoxShadow>? shadows,
|
|
}) {
|
|
ShapeDecoration(color: color, shape: shape, shadows: shadows)
|
|
.createBoxPainter(() {})
|
|
.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
|
|
}
|
|
|
|
static void _block(Canvas canvas, Rect rect, Color color, double extent) {
|
|
_fill(canvas, rect, color, AppShape.all(AppCorner.fit(extent)));
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final geometry = _MiniGeometry(
|
|
size,
|
|
floatingBar: floatingBar,
|
|
hasFab: hasFab,
|
|
);
|
|
_paintPages(canvas, geometry);
|
|
final unit = geometry.unit;
|
|
final dock = SurfaceLevel.floatingOf(colorScheme).colorOf(colorScheme);
|
|
if (hasFab && !floatingBar) {
|
|
_block(
|
|
canvas,
|
|
geometry.fab,
|
|
colorScheme.primaryContainer,
|
|
geometry.fabSize,
|
|
);
|
|
}
|
|
final shadow = BoxShadow(
|
|
color: colorScheme.shadow.withValues(alpha: 0.12),
|
|
blurRadius: unit,
|
|
offset: Offset(0, unit * 0.3),
|
|
);
|
|
if (_glass) {
|
|
_paintGlassBar(canvas, geometry, shadow);
|
|
} else if (floatingBar) {
|
|
_fill(canvas, geometry.bar, dock, AppShape.full, shadows: [shadow]);
|
|
} else {
|
|
canvas.drawRect(geometry.bar, Paint()..color = dock);
|
|
}
|
|
_paintDestinations(canvas, geometry.bar, unit);
|
|
if (hasFab && floatingBar) {
|
|
_fill(canvas, geometry.fab, colorScheme.primaryContainer, AppShape.full);
|
|
}
|
|
}
|
|
|
|
void _paintPages(Canvas canvas, _MiniGeometry geometry) {
|
|
final size = geometry.size;
|
|
canvas.drawRect(
|
|
Offset.zero & size,
|
|
Paint()..color = SurfaceLevel.page.colorOf(colorScheme),
|
|
);
|
|
canvas.save();
|
|
canvas.clipRect(Offset.zero & size, doAntiAlias: false);
|
|
final previous = this.previous;
|
|
if (previous == null) {
|
|
_paintPage(canvas, geometry, selected, 0);
|
|
} else if (tabAnimation == TabAnimation.fade) {
|
|
_paintPage(canvas, geometry, previous, 0, opacity: 1 - progress);
|
|
_paintPage(canvas, geometry, selected, 0, opacity: progress);
|
|
} else {
|
|
final direction = previous > selected ? -1 : 1;
|
|
_paintPage(
|
|
canvas,
|
|
geometry,
|
|
previous,
|
|
-direction * size.width * progress,
|
|
);
|
|
_paintPage(
|
|
canvas,
|
|
geometry,
|
|
selected,
|
|
direction * size.width * (1 - progress),
|
|
);
|
|
}
|
|
canvas.restore();
|
|
}
|
|
|
|
/// The page frosted under a tint, ringed with light and casting its shadow
|
|
/// only outside, after what [LiquidGlass] does at full size.
|
|
void _paintGlassBar(Canvas canvas, _MiniGeometry geometry, BoxShadow shadow) {
|
|
final bar = geometry.bar;
|
|
final unit = geometry.unit;
|
|
final path = AppShape.full.getOuterPath(bar);
|
|
final style = LiquidGlassStyle.pill(colorScheme);
|
|
final blur = unit * 0.4;
|
|
canvas
|
|
..save()
|
|
..clipPath(
|
|
Path()
|
|
..fillType = PathFillType.evenOdd
|
|
..addRect(bar.inflate(unit * 3))
|
|
..addPath(path, Offset.zero),
|
|
)
|
|
..drawPath(path.shift(shadow.offset), shadow.toPaint())
|
|
..restore()
|
|
..save()
|
|
..clipPath(path)
|
|
..saveLayer(
|
|
bar,
|
|
Paint()..imageFilter = ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
|
);
|
|
_paintPages(canvas, geometry);
|
|
final rimWidth = unit * 0.15;
|
|
canvas
|
|
..restore()
|
|
..drawPath(path, Paint()..color = style.tint)
|
|
..drawPath(
|
|
AppShape.full.getOuterPath(bar.deflate(rimWidth / 2)),
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = rimWidth
|
|
..shader = LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Colors.white.withValues(alpha: style.rim),
|
|
Colors.white.withValues(alpha: style.rim * 0.15),
|
|
Colors.white.withValues(alpha: style.rim * 0.5),
|
|
],
|
|
).createShader(bar),
|
|
)
|
|
..restore();
|
|
}
|
|
|
|
void _paintPage(
|
|
Canvas canvas,
|
|
_MiniGeometry geometry,
|
|
int index,
|
|
double offset, {
|
|
double opacity = 1,
|
|
}) {
|
|
final alpha = Color.getAlphaFromOpacity(opacity);
|
|
if (alpha == 0) {
|
|
return;
|
|
}
|
|
final size = geometry.size;
|
|
final unit = geometry.unit;
|
|
final margin = geometry.margin;
|
|
final content = Rect.fromLTRB(
|
|
offset + margin,
|
|
margin,
|
|
offset + size.width - margin,
|
|
size.height - geometry.pageBottom,
|
|
);
|
|
if (alpha < 255) {
|
|
canvas.saveLayer(null, Paint()..color = Color.fromARGB(alpha, 0, 0, 0));
|
|
} else {
|
|
canvas.save();
|
|
}
|
|
canvas.clipRect(content, doAntiAlias: false);
|
|
final left = content.left;
|
|
final width = content.width;
|
|
final card = SurfaceLevel.card.colorOf(colorScheme);
|
|
final line = colorScheme.onSurfaceVariant.withValues(alpha: 0.4);
|
|
var top = content.top;
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(left, top, unit * 7, unit * 1.1),
|
|
colorScheme.onSurface.withValues(alpha: 0.72),
|
|
AppShape.full,
|
|
);
|
|
top += unit * 1.1 + unit * 0.5 + unit;
|
|
switch (_pageOf(index)) {
|
|
case _MiniPage.cards:
|
|
_block(
|
|
canvas,
|
|
Rect.fromLTWH(left, top, width, unit * 5),
|
|
card,
|
|
unit * 5,
|
|
);
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(
|
|
left + unit,
|
|
top + (unit * 5 - unit * 2.2) / 2,
|
|
unit * 2.2,
|
|
unit * 2.2,
|
|
),
|
|
colorScheme.primary,
|
|
AppShape.circle,
|
|
);
|
|
final lineLeft = left + unit + unit * 2.2 + unit;
|
|
final lineTop = top + (unit * 5 - (unit + unit * 0.6 + unit * 0.8)) / 2;
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(lineLeft, lineTop, unit * 6, unit),
|
|
line,
|
|
AppShape.full,
|
|
);
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(
|
|
lineLeft,
|
|
lineTop + unit + unit * 0.6,
|
|
unit * 3.5,
|
|
unit * 0.8,
|
|
),
|
|
line,
|
|
AppShape.full,
|
|
);
|
|
top += unit * 5 + unit;
|
|
final half = (width - unit) / 2;
|
|
_block(
|
|
canvas,
|
|
Rect.fromLTWH(left, top, half, unit * 4),
|
|
card,
|
|
unit * 4,
|
|
);
|
|
final chosen = Rect.fromLTWH(left + half + unit, top, half, unit * 4);
|
|
_block(canvas, chosen, card, unit * 4);
|
|
_fill(
|
|
canvas,
|
|
Rect.fromCircle(
|
|
center: chosen.topRight + Offset(-unit * 1.3, unit * 1.3),
|
|
radius: unit * 0.7,
|
|
),
|
|
colorScheme.primary,
|
|
AppShape.circle,
|
|
);
|
|
top += unit * 4 + unit;
|
|
for (var i = 0; i < 4; i++) {
|
|
_block(
|
|
canvas,
|
|
Rect.fromLTWH(left, top, width, unit * 4.5),
|
|
card,
|
|
unit * 4.5,
|
|
);
|
|
top += unit * 4.5 + unit;
|
|
}
|
|
case _MiniPage.list:
|
|
final lineLeft = left + unit * 2.2 + unit;
|
|
for (var i = 0; i < 9; i++) {
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(
|
|
left,
|
|
top + (unit * 2.6 - unit * 2.2) / 2,
|
|
unit * 2.2,
|
|
unit * 2.2,
|
|
),
|
|
SurfaceLevel.inset.colorOf(colorScheme),
|
|
AppShape.circle,
|
|
);
|
|
final lineTop =
|
|
top + (unit * 2.6 - (unit * 0.8 + unit * 0.5 + unit * 0.6)) / 2;
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(
|
|
lineLeft,
|
|
lineTop,
|
|
unit * (8 - (i + index) % 3 * 1.5),
|
|
unit * 0.8,
|
|
),
|
|
line,
|
|
AppShape.full,
|
|
);
|
|
_fill(
|
|
canvas,
|
|
Rect.fromLTWH(
|
|
lineLeft,
|
|
lineTop + unit * 0.8 + unit * 0.5,
|
|
unit * 4,
|
|
unit * 0.6,
|
|
),
|
|
line.withValues(alpha: 0.2),
|
|
AppShape.full,
|
|
);
|
|
top += unit * 2.6 + unit;
|
|
}
|
|
}
|
|
canvas.restore();
|
|
}
|
|
|
|
void _paintDestinations(Canvas canvas, Rect bar, double unit) {
|
|
final cell = bar.width / MiniScreen.destinationCount;
|
|
for (var i = 0; i < MiniScreen.destinationCount; i++) {
|
|
final center = Offset(bar.left + cell * i + cell / 2, bar.center.dy);
|
|
final dot = Rect.fromCenter(
|
|
center: center,
|
|
width: unit * 0.9,
|
|
height: unit * 0.9,
|
|
);
|
|
if (i != selected) {
|
|
_fill(canvas, dot, colorScheme.onSurfaceVariant, AppShape.circle);
|
|
continue;
|
|
}
|
|
final dark = colorScheme.brightness == Brightness.dark;
|
|
_fill(
|
|
canvas,
|
|
Rect.fromCenter(center: center, width: unit * 2.8, height: unit * 1.6),
|
|
_glass
|
|
? colorScheme.onSurface.withValues(alpha: dark ? 0.14 : 0.08)
|
|
: SurfaceLevel.floatingOf(colorScheme).next.colorOf(colorScheme),
|
|
AppShape.full,
|
|
);
|
|
_fill(canvas, dot, colorScheme.primary, AppShape.circle);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_MiniScreenPainter oldDelegate) {
|
|
return oldDelegate.colorScheme != colorScheme ||
|
|
oldDelegate.floatingBar != floatingBar ||
|
|
oldDelegate.liquidGlass != liquidGlass ||
|
|
oldDelegate.selected != selected ||
|
|
oldDelegate.previous != previous ||
|
|
oldDelegate.progress != progress ||
|
|
oldDelegate.tabAnimation != tabAnimation ||
|
|
oldDelegate.hasFab != hasFab;
|
|
}
|
|
}
|