隐私模糊
一款跨平台应用程序,用于模糊图像中的敏感数据,面向 iOS 和 Android 设备。主要使用 Flutter SDK 的 Dart 语言编写。
该应用现已在 Google Play 商店和 App Store 上架!
|
该项目旨在为用户提供一个免费、简洁、简单的图像数据处理解决方案。
无应用内购买。无广告。无水印。无烦恼。永久免费,因为隐私不应收费。免费,因为我们关心!
代码风格
我们对该项目的代码风格有一些限制。
您可以在 CONTRIBUTING 文件中找到所有要求。
功能
- 人脸检测
- 模糊/像素化效果
- 精细/粗糙颗粒效果
- 圆形/方形区域
- 导出到相机胶卷
Flutter SDK 设置
该应用运行在 Flutter SDK 2.2 和 Dart 2.13 上。
在处理项目之前,请确保运行 flutter upgrade。
Windows
点击此处
Windows 安装指南
macOS
点击此处
macOS 安装指南
构建
该应用面向手机和平板电脑上的 iOS 和 Android。
桌面和 Web 平台可能会导致问题,目前未计划支持。
建议运行
flutter clean
flutter pub get
一旦出现构建问题。
iOS 构建
您需要一台 macOS 机器才能运行 Flutter iOS 应用程序。
请同时确保您的机器上已正确安装了 CocoaPods 和 Xcode 的版本。
有关部署信息,请访问:iOS 部署指南
Android 构建
构建 Android 版本无需其他要求。
有关部署信息,请访问:Android 部署指南
结构
lib/--+--main.dart (entry point)
|
+--resources/--- images, fonts, strings, etc...
|
+--------src/--+----- app.dart (some inital code)
+---router.dart (navigation handling)
+-------di.dart (dependency injection)
|
|
+--screens/--screen_name/
| +--helpers/-- (garbage place, like many events and states)
| +--widgets/-- (internal widgets for this screen)
| |
| +--repo.dart
| +--bloc.dart
| +--view.dart
|
|
+--widgets/-- (common widgets for application)
|
|
+----utils/-- (some utils for application if necessary)
架构
为了构建一个可维护、可扩展和可测试的项目,该应用采用了架构模式和文件结构。
为了可读性,我们将文件大小限制在 300 行。
导航
导航使用 Flutter Navigation v1 保持相对简单。router.dart 文件包含 2 个类中所有的路由和导航逻辑。
- ScreenNavigator:实现并提供启用路由的方法
- AppRouter:声明所有路由及其配置
为了能够单独测试每个路由,AppRouter 具有多个构造函数。每个路由也必须定义一个构造函数。
该构造函数会覆盖实际的初始路由,并在指定位置启动。
每个路由接收依赖注入实例和 AppRouter 实例本身。可选参数作为第三个参数。
依赖注入
依赖注入类用于按正确顺序注入应用程序的所有可测试部分。
它持有不同 bloc 和 repository 的 provider。该类的实例必须传递给视图,以便它们可以在 Provider/Consumer 结构中访问所有依赖项。
BLoC
在 Dart 中,使用 Streams 和 Provider 模式非常有用。因此,我们选择了流行的 BLoC 模式。
为每个屏幕实现一个 bloc 是有意义的,但前提是它需要持有和管理数据。通常,bloc 从依赖注入中接收相应的 repository。
BLoC 类文件位于屏幕视图的同一目录中。
每个视图都使用 events 来触发 bloc 中的 state 更新,而视图又会重新消费这些更新。
事件和状态位于屏幕文件夹下的 helpers 目录中。
为了能够决定发出哪个状态,bloc 会消费事件并根据事件处理决策。
每个事件后始终产生一些输出很重要,否则 UI 不会被更新并且会卡住。
Stream<SampleState> mapEventToState(SampleEvent event) async* {
if (event is EventOne) {
yield* handleStateOneChange(event);
} else if (event is EventTwo) {
yield* handleStateTwoChange(event);
} ...
}
bloc 作为屏幕的逻辑组件,将所有逻辑与 UI 分开。因此,bloc 也不应处理任何 UI 部分。
视图
每个视图代表用作导航路由中屏幕的小部件。它应该只定义 UI 并消费/使用状态。
主要而言,最好将屏幕实现为 StatelessWidget,并且只在状态更新后重新渲染真正需要重新渲染的部分。
Bloc-Screen 被 MultiBlocProvider 包裹,后者从依赖注入中获取相应的 bloc,并通过 context 使其可用于嵌套的 BlocConsumer。
return MultiBlocProvider(
providers: [_di.getSampleBloc()],
child: BlocConsumer<SampleBloc, SampleState>(...)
对于简单的 UI 数据更改,例如显示 Toast 消息,我们使用了 bloc consumer 的 buildWhen 条件来防止整个重新渲染。只有 MessageBar 会通过监听器绘制在屏幕上方。
BlocConsumer<SampleBloc, SampleState>(
listenWhen: (_, curState) => (curState is SimpleState),
buildWhen: (_, curState) => !(curState is SimpleState),
listener: (_, state) {
showMessageOnSimpleState(...)
}
...
视图的大部分内容都分成了单独的方法。这提高了可读性和模块化。一旦文件大小超过 300 行或小部件变得非常大,就有必要将它们移动到另一个文件。
小部件
小部件存在于通用和屏幕作用域级别。通用小部件位于 src/widgets 中,屏幕小部件位于 src/screen_name/widgets 中。
通用级别的小部件不应实现屏幕特定的配置,而应通过最小化设置使其可重用。
屏幕作用域中的小部件要么仅在此处使用,要么其用法非常具体于屏幕。
有状态小部件
持有状态的小部件的最佳实践是也内部管理它。例如
class WidgetState extends State<WidgetWithState> {
late double _initialValue;
@override
void didUpdateWidget(covariant DeferredSlider oldWidget) {
_initialValue = widget._initialValue;
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
_initialValue = widget._initialValue;
super.initState();
}
... Widget build ...
void onChanged(doubleValue) {
setState(() {
_initialValue = doubleValue;
});
this.widget.onChanged(doubleValue);
}
}
自适应小部件
该应用面向 iOS 和 Android,但在这两个平台上需要看起来不同。
这就是为什么我们将特定于平台(iOS/Android)的代码拆分到 src/widgets/adaptive_widgets_builder.dart 中。
我们注意到,Flutter 正在开始构建小部件的自适应版本。
当他们完成这些工作后,我们可以移除并用 Flutter 小部件的自适应版本替换此文件中的“构建器”。
国际化
该应用已翻译成
- :us: 英语(默认语言)
- :de: 德语
翻译文件位于 lib/resources/i18n 目录中。
要添加新语言,您需要执行以下操作:
- 将语言代码添加到支持的语言中
var delegate = await LocalizationDelegate.create(
...
supportedLocales: [..., '<language_code>']);
- 在
lib/resources/i18n/<lang_code>.json中为语言代码添加 json 文件 - 从现有的 .json 文件复制结构
- 一旦添加了新文本,就需要生成新的键。运行:
flutter pub run build_runner build --delete-conflicting-outputs
主题
主题根据不同平台拆分,此外,Android 还有主题模式。
theme_provider.dart
- iosTheme(提供主题模式拆分的 iOS ThemeData)
- light(提供浅色 Android ThemeData)
- dark(提供深色 Android ThemeData)
支持深色和浅色模式,具体取决于系统偏好设置。
// AppBuilder widget
themeMode: ThemeMode.system,
资产/资源
所有资产/资源都可以在 lib/resouces 中找到。
图片、图标和其他通用资源应放置在此处。
图标
目前,该应用使用自定义图标字体,其中包含用于工具栏选择中图像编辑工具的 4 个图标。
字体可以在此处生成:fluttericon.com。
仅包含项目中使用的图标!
此外,Flutter 还提供了默认图标,可以从 CupertinoIcons 或 Icons 类使用。
为了从一个地方处理图标,项目中有 icons_provider 类。
这是定义图标名称的地方,它可以有额外的逻辑来区分平台。
图片
图片可以添加到此处。为了处理不同屏幕分辨率的图像尺寸,可以将图像以 x2 和 x3 的尺寸添加到相关的屏幕。
在 Dart 端,像这样加载它就足够了:lib/resources/images/<image_name>。Dart 会处理分辨率之间的拆分。
目前,该应用仅使用 App Logo,它在启动/闪屏和主屏幕上至关重要。还建议限制静态图像文件的数量,因为它们会大大增加项目的大小。
字符串
此处包含应用程序中使用的所有文本字符串。计划在项目的后期阶段添加 i18n 和 i10n。
此处的所有字符串都需要移至翻译 .aab 文件。
ImageFilter 库描述
此库是单例模式。它在工作时使用大量内存,因此创建该类的多个实例是个非常糟糕的主意。
该类可以加载和过滤图像。
支持的过滤器
- 像素化
- 线性模糊(非高斯,太慢)
类特性
- 提交前的更改预览
- 撤销特定区域的更改(橡皮擦工具)
- 处理方形和圆形区域
- 一次处理多个区域
- 将所有已处理的像素替换为选定的过滤器
如何使用此类
import 'dart:ui' as img_tools;
final file = File(filename);
final imageFilter = ImageAppFilter();
var completer = Completer<img_tools.Image>();
img_tools.decodeImageFromList(file.readAsBytesSync(), (result) {
completer.complete(result);
});
var image = await completer.future;
/// VERY IMPORTANT TO USE AWAIT HERE!!!
var filteredImage = await imageFilter.setImage(_blocState.image);
imageFilter.transactionStart();
imageFilter.setMatrix(MatrixAppPixelate(20));
imageFilter.apply2CircleArea(200, 200, 150);
/// for preview before saving (only changed area of image will be updated)
filteredImage = await imageFilter.getImage();
imageFilter.setMatrix(MatrixAppBlur(20));
imageFilter.apply2CircleArea(400, 400, 250);
/// save to cached image, after that you can not cancel changes
/// only if you load image again.
imageFilter.transactionCommit();
/// get saved image after transaction completed (complete merged image without changed part)
filteredImage = await imageFilter.getImage();
方法
- static void setMaxProcessedWidth(int) - 默认值为 1000,为了模糊速度优化,我们需要知道最大处理区域宽度。
- Future<ImageFilterResult> setImage(dart:ui.Image) - 设置要过滤的图像。
- Future<ImageFilterResult> getImage() - 如果事务处于活动状态,则仅返回已更改区域且不包含背景图像。如果事务已关闭,则返回完整的更新图像。
- void transactionStart() - 为更改开启事务
- void transactionCancel() - 重置更改并关闭事务
- void transactionCommit() - 应用事务中的更改并关闭事务
- void setFilter(ImageAppMatrix newMatrix) - 设置当前过滤器
- void apply2SquareArea(int x1, int y1, double radius) - 将选定的过滤器应用于以 x1, y1 为中心、半径为 radius 的方形区域。
- void apply2CircleArea(int x1, int y1, double radius) - 将选定的过滤器应用于以 x1, y1 为中心、半径为 radius 的圆形区域。
- void cancelCurrent() - 取消当前事务中的所有更改
- void cancelSquare(int, int, int, int) - 取消方形区域内的更改
- void cancelCircle(int, int, int) - 取消圆形区域内的更改
过滤器
- MatrixAppPixelate(int blockSize)
- MatrixAppBlur(int blockSize) - 线性模糊
测试
在项目根目录或测试文件夹中运行 flutter test 命令。所有测试文件必须以 _test.dart 后缀结尾。
依赖项
只添加空安全依赖项!
该应用程序运行在健全的空安全模式下。因此,所有添加的依赖项都需要遵循此限制。
- flutter_styled_toast: 用于警报
- image_size_getter: 获取图像尺寸以决定是否应旋转图像
- flutter_exif_rotation: 用于图像加载时的初始旋转
- url_launcher: 用于在主屏幕打开链接
- image_picker: 用于访问图像库
- image: 用于图像文件处理
- image_gallery_saver: 用于将图像保存到库
- permission_handler: 用于访问操作系统权限状态
- path_provider: 用于读取操作系统图像路径
- bloc: 架构模式类的库
- flutter_bloc: Dart bloc 库的 Flutter 附加功能
- mockito: 用于测试中的模拟
- bloc_test: 用于测试 bloc
- flutter_translate: 为应用添加 i18n
- flutter_translate_gen: 用于生成用于静态使用翻译的变量
