flutter_app_watermark 1.0.0 copy "flutter_app_watermark: ^1.0.0" to clipboard
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);
  }
}
1
likes
0
points
214
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter 水印组件:一行代码给整个 App 加水印,也能只给单个组件加;文案、角度、透明度、间距均可配置,零第三方依赖。

Repository (GitHub)
View/report issues

Topics

#watermark #security #overlay #widget

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_app_watermark