import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:pomotimer/app_text_style.dart'; import '../../generated/l10n.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { var theme = Theme.of(context); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: TopBar(theme), body: const HomeBody(), ); } } class TopBar extends AppBar { TopBar(ThemeData theme, {super.key}) : super( backgroundColor: Colors.transparent, leading: Container( margin: const EdgeInsets.only(left: 15), child: IconButton( iconSize: 25, tooltip: S.current.settingBtnTooltip, icon: Icon(LucideIcons.settings, color: theme.colorScheme.onBackground), onPressed: () { // TODO 跳转到设置 }, ), )); } class HomeBody extends StatelessWidget { const HomeBody({super.key}); @override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints.expand(), child: Center( child: Container( constraints: const BoxConstraints.tightFor(width: 300, height: 500), child: const TimerController()), ), ); } } class TimerController extends StatefulWidget { const TimerController({super.key}); @override State createState() => _TimerControllerState(); } class _TimerControllerState extends State { var selected = 0; @override Widget build(BuildContext context) { var theme = Theme.of(context); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AttributeSwitcher( selected: selected, onSelected: (selected) => debugPrint('selected: $selected'), ), ], ); } } class AttributeSwitcher extends StatefulWidget { AttributeSwitcher( {super.key, required this.selected, required this.onSelected}) { if (selected < 0 || selected > 2) { throw Exception('selected must be in [0, 2]'); } } final int selected; final void Function(int) onSelected; @override State createState() => _AttributeSwitcherState(); } class _AttributeSwitcherState extends State { int _selected = 0; @override void initState() { super.initState(); _selected = widget.selected; } void _updateSelected(int selected) { setState(() { _selected = selected; }); widget.onSelected(selected); } Widget _createSticky({ required double posX, required double posY, required double height, required double width, required ColorScheme colorScheme, }) { return AnimatedPositioned( duration: const Duration(milliseconds: 500), top: posY, left: posX, child: UnconstrainedBox( child: AnimatedContainer( height: height, width: width, decoration: BoxDecoration( color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(80), ), duration: const Duration(milliseconds: 500), ), )); } OverlayEntry _createRow({required List btnKeys}) { return OverlayEntry( builder: (context) => Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ AttributeBtn( btnKey: btnKeys[0], text: '专注', onPressed: () => _updateSelected(0)), const AttributeSplitter(), AttributeBtn( btnKey: btnKeys[1], text: '小休息', onPressed: () => _updateSelected(1), ), const AttributeSplitter(), AttributeBtn( btnKey: btnKeys[2], text: '大休息', onPressed: () => _updateSelected(2), ), ], ), ); } @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; List btnKeys = [GlobalKey(), GlobalKey(), GlobalKey()]; List btnPos = []; List btnSize = []; GlobalKey overlayKey = GlobalKey(); OverlayEntry row = _createRow(btnKeys: btnKeys); SchedulerBinding.instance.addPostFrameCallback((_) { OverlayState state = overlayKey.currentState as OverlayState; if (overlayKey.currentContext == null) { throw Exception('overlayKey.currentContext is null'); } var overlayPos = (overlayKey.currentContext?.findRenderObject() as RenderBox) .localToGlobal(Offset.zero); for (var element in btnKeys) { if (element.currentContext == null) { throw Exception('element.currentContext is null'); } var readerBox = element.currentContext?.findRenderObject() as RenderBox; var readerSize = readerBox.size; var readerPos = readerBox.localToGlobal(Offset(-overlayPos.dx, -overlayPos.dy)); btnPos.add(readerPos); btnSize.add(readerSize); } state.insert(OverlayEntry(builder: (context) { return _createSticky( posX: btnPos[_selected].dx, posY: btnPos[_selected].dy, height: btnSize[_selected].height, width: btnSize[_selected].width, colorScheme: colorScheme, ); }), below: row); }); return SizedBox( width: 300, height: 50, child: Overlay(key: overlayKey, initialEntries: [row]), ); } } class AttributeBtn extends StatelessWidget { const AttributeBtn( {super.key, this.btnKey, this.onPressed, required this.text}); final void Function()? onPressed; final String text; final GlobalKey? btnKey; @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); TextTheme textTheme = theme.textTheme; return Container( margin: const EdgeInsets.symmetric(horizontal: 5), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: double.infinity, minHeight: 0, maxWidth: double.infinity, minWidth: 70, ), child: TextButton( key: btnKey, onPressed: onPressed, child: Text( text, style: AppTextStyle.generateWithTextStyle(textTheme.bodyLarge!), )), ), ); } } class AttributeSplitter extends StatelessWidget { const AttributeSplitter({super.key}); @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; return Container( decoration: BoxDecoration( color: colorScheme.surface, shape: BoxShape.circle, ), height: 4, width: 4, ); } }