flutter_app_watermark 1.0.0
flutter_app_watermark: ^1.0.0 copied to clipboard
Flutter 水印组件:一行代码给整个 App 加水印,也能只给单个组件加;文案、角度、透明度、间距均可配置,零第三方依赖。
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_app_watermark/flutter_app_watermark.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';
import 'src/demo_state.dart';
import 'src/misc_pages.dart';
import 'src/mobile_shell.dart';
import 'src/sheets.dart';
import 'src/theme.dart';
import 'src/wide_shell.dart';
export 'l10n/app_localizations.dart' show AppLocalizations;
export 'src/demo_state.dart'
show
demo,
DemoState,
DemoLang,
WatermarkMode,
CoverageTest,
kLangs,
kZhHant,
resolveLocale;
/// 状态栏:白底黑字。
///
/// Android 15 起系统强制 edge-to-edge,`statusBarColor` 已经不起作用了——
/// 状态栏一律透明,露出的是 App 自己画的背景,再叠一层系统灰色蒙层,
/// 看起来就是「半透明的灰」。所以正确做法不是去涂状态栏,而是:
///
/// 1. `statusBarColor: transparent` + App 背景本来就是白的(`Scaffold` 的
/// `backgroundColor` 是 `AppColors.surface`,`SafeArea` 只做内缩不遮挡),
/// 白底就自然透出来了;
/// 2. `systemStatusBarContrastEnforced: false` 关掉那层灰色蒙层;
/// 3. 图标用深色:Android 看 `statusBarIconBrightness`,
/// iOS 看 `statusBarBrightness`(它描述的是**背景**亮度,浅色背景
/// → 系统自动用深色图标),两个都要给,否则另一个平台不生效。
///
/// 用 `edgeToEdge` 而不是 `SystemUiMode.manual`:后者会改变窗口尺寸,
/// 触发正文重新布局。
const SystemUiOverlayStyle _lightStatusBar = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemStatusBarContrastEnforced: false,
);
void main() {
// demo 在构造时要读设备语言,先确保 binding 已经就绪。
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(_lightStatusBar);
runApp(const DemoRoot());
}
/// 把 [WatermarkScope] 和 [DemoApp] 串起来。
///
/// 关键:`WatermarkScope` 包在 `MaterialApp` **外面**,
/// 这样任何路由、弹窗、BottomSheet 都盖不住水印。
///
/// 也正因为它在 `MaterialApp` 之上,继承不到 App 自己的 `Directionality`,
/// 所以切到阿拉伯语 / 乌尔都语时要显式把文字方向传给它。
///
/// 同时在这里监听系统语言变化:用户在手机设置里改了语言,
/// 只要他没在 App 内显式选过,界面就跟着变。
class DemoRoot extends StatefulWidget {
const DemoRoot({super.key});
@override
State<DemoRoot> createState() => _DemoRootState();
}
class _DemoRootState extends State<DemoRoot> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeLocales(List<Locale>? locales) {
super.didChangeLocales(locales);
demo.syncSystemLocale(locales ?? const <Locale>[]);
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: demo,
builder: (BuildContext context, Widget? child) => WatermarkScope(
controller: demo.watermark,
textDirection: demo.textDirection,
child: child!,
),
// main() 里设过一次全局样式;这里再包一层 AnnotatedRegion,
// 是为了推新路由 / 弹面板时样式不会被别的图层顶掉。
child: const AnnotatedRegion<SystemUiOverlayStyle>(
value: _lightStatusBar,
child: DemoApp(),
),
);
}
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: demo,
builder: (BuildContext context, Widget? child) => MaterialApp(
title: 'flutter_app_watermark',
debugShowCheckedModeBanner: false,
theme: buildTheme(),
locale: demo.locale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: const <LocalizationsDelegate<Object>>[
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: child,
),
child: const AppShell(),
);
}
}
/// 按窗口宽度在两套布局之间切换。
///
/// - `< AppBreakpoints.wide`:窄屏,底部标签栏 + 底部面板;
/// - `>= AppBreakpoints.wide`:宽屏,左侧导航 + 主区(够宽时设置常驻右栏)。
///
/// 两套布局共用同一份 [demo] 状态,所以旋转屏幕、拖动桌面窗口时
/// 当前页面、水印参数都不会丢。
class AppShell extends StatefulWidget {
const AppShell({super.key});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 语言变了就把水印演示文案换成对应语种(用户改过就不动他的输入)。
//
// 必须推到当前帧之后再改:didChangeDependencies 处于 build 阶段,
// 这里直接改状态会让上层的 ListenableBuilder 在构建过程中被标脏。
final AppLocalizations l = context.l10n;
WidgetsBinding.instance.addPostFrameCallback(
(_) => demo.syncLocalizedText(l),
);
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: demo,
builder: (BuildContext context, Widget? _) =>
AppBreakpoints.isWide(context)
? WideShell(
onSettings: () => _openSettings(context),
onLang: () => showLanguageSurface(context),
onCoverageTest: (CoverageTest kind) =>
runCoverageTest(context, kind),
)
: MobileShell(
onSettings: () => _openSettings(context),
onLang: () => showLanguageSurface(context),
),
);
}
Future<void> _openSettings(BuildContext context) async {
final CoverageTest? kind = await showSettingsSurface(context);
if (kind == null || !context.mounted) return;
// 面板里点了「覆盖测试」,等它收起来之后再叠上去。
await runCoverageTest(context, kind);
}
}