анимация circular reveal при смене темы

This commit is contained in:
klockky
2026-05-22 18:17:56 +00:00
parent 9badd63879
commit 939dabbd86
3 changed files with 249 additions and 54 deletions
+94 -2
View File
@@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
@@ -31,6 +33,7 @@ import 'frontend/debug/fps_overlay_layer.dart';
import 'frontend/screens/auth/login_screen.dart';
import 'frontend/screens/chats/chat_list_screen.dart';
import 'frontend/widgets/custom_notification.dart';
import 'frontend/widgets/theme_reveal.dart';
final api = Api();
final accountModule = AccountModule(api);
@@ -128,9 +131,15 @@ class KometApp extends StatefulWidget {
State<KometApp> createState() => KometAppState();
}
class KometAppState extends State<KometApp> with WidgetsBindingObserver {
class KometAppState extends State<KometApp>
with WidgetsBindingObserver, TickerProviderStateMixin {
static const _fallbackSeed = Color(0xFFC1C4FF);
final GlobalKey _captureBoundaryKey = GlobalKey();
OverlayEntry? _revealEntry;
AnimationController? _revealController;
ui.Image? _revealImage;
late Locale _locale;
late String _fontId;
bool _isLoggingOut = false;
@@ -239,6 +248,7 @@ class KometAppState extends State<KometApp> with WidgetsBindingObserver {
@override
void dispose() {
_finishReveal();
_sessionExpiredSub?.cancel();
_loginStatusSub?.cancel();
_vpnBypassSub?.cancel();
@@ -321,10 +331,89 @@ class KometAppState extends State<KometApp> with WidgetsBindingObserver {
await AppThemeModeConfig.save(mode);
}
void applyThemeModeWithReveal(AppThemeMode mode, Offset center) {
if (AppThemeModeConfig.current.value == mode) return;
_runThemeReveal(center, () => AppThemeModeConfig.save(mode));
}
Future<void> applyAmoled(bool value) async {
await AppAmoled.save(value);
}
void applyAmoledWithReveal(bool value, Offset center) {
if (AppAmoled.current.value == value) return;
_runThemeReveal(center, () => AppAmoled.save(value));
}
void _runThemeReveal(Offset center, Future<void> Function() apply) {
final overlay = KometApp.navigatorKey.currentState?.overlay;
final ctx = _captureBoundaryKey.currentContext;
if (overlay == null || ctx == null) {
apply();
return;
}
if (MediaQuery.disableAnimationsOf(ctx)) {
apply();
return;
}
final renderObject = ctx.findRenderObject();
if (renderObject is! RenderRepaintBoundary) {
apply();
return;
}
final ui.Image snapshot;
try {
final dpr = math.min(MediaQuery.of(ctx).devicePixelRatio, 2.0);
snapshot = renderObject.toImageSync(pixelRatio: dpr);
} catch (_) {
apply();
return;
}
_finishReveal();
final controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 650),
);
final entry = ThemeRevealOverlay.build(
snapshot: snapshot,
center: center,
animation: controller,
);
_revealController = controller;
_revealEntry = entry;
_revealImage = snapshot;
overlay.insert(entry);
apply();
WidgetsBinding.instance.endOfFrame.then((_) {
if (_revealController != controller) return;
controller.forward().then(
(_) {
if (_revealController != controller) return;
_finishReveal();
},
onError: (_) {},
);
});
}
void _finishReveal() {
_revealEntry?.remove();
_revealEntry = null;
_revealController?.dispose();
_revealController = null;
final img = _revealImage;
_revealImage = null;
if (img != null) {
WidgetsBinding.instance.addPostFrameCallback((_) => img.dispose());
}
}
Future<void> applyThemeSchedule(ThemeSchedule schedule) async {
await AppThemeSchedule.save(schedule);
}
@@ -553,7 +642,10 @@ class KometAppState extends State<KometApp> with WidgetsBindingObserver {
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
sChild!,
RepaintBoundary(
key: _captureBoundaryKey,
child: sChild!,
),
if (fpsOn) const FpsOverlayLayer(),
],
);