首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

基于flutter的开源壁纸软件

  • 25-04-22 20:22
  • 3094
  • 8534
juejin.cn

基于flutter的开源壁纸软件

功能描述

  1. 一款基于flutter的开源壁纸软件,所有接口都是大佬开源的,感谢各位大佬;
  2. 支持设置下载壁纸;
  3. 支持简单收藏功能(本地收藏);
  4. 支持设置壁纸功能;
  5. 支持获取本地壁纸来设置壁纸;
  6. 支持扫描本地相册来设置壁纸;
  7. 支持获取本地视频设置壁纸(只支持mp4格式)。

没有登录/注册功能,只能简单的做一个本地存储,功能和其它壁纸软件也大差不差。不过我最喜欢的还是可以设置本地壁纸和本地视频壁纸的功能。

1、项目预览图

  • 截取了部分图,其它功能有在以前的文章介绍过,懒得在叙述了,有的页面很简单也懒得截了。
微信图片_20250307142647.jpg 微信图片_20250307142642.jpg 微信图片_20250307142638.jpg 微信图片_20250307142633.jpg 微信图片_20250307142612.jpg 微信图片_20250307142607.jpg 微信图片_20250307142603.jpg 微信图片_20250307142558.jpg 微信图片_20250307142553.jpg 微信图片_20250307142533.jpg

2、插件使用(推荐)

  1. extended_image 图片展示预览插件,该插件支持图片放大缩小等各种功能,并且能够缓存图片至临时文件夹,下次加载时能够直接读取缓存中的图片,无需额外的网络开销。这里只用了基础放大缩小、缓存功能,其它功能都没用,详细用法见官网,非常强大的图片管理插件。
  2. flutter_cache_manager 缓存管理插件,允许用户手动清理缓存。
  3. image_picker 适用于 iOS 和 Android 的 Flutter 插件,用于从图像库中选择图像,并使用相机拍摄新照片。
  4. photo_manager 无需 UI 集成,即可在 Android、iOS、macOS 和 OpenHarmony 上获取资产(图片/视频/音频)。
  5. flutter_wallpaper_manager 用于在您的 Android 设备上设置壁纸。它支持主屏幕、锁屏和两种屏幕模式。(已弃用,设置超大尺寸图片时容易卡死,软件容易闪退、容易设置失败等问题。)
  6. async_wallpaper 一个 flutter 包,其中包含一些函数的集合,用于在 Android 设备上异步设置壁纸。使用此插件,您还可以本地设置视频动态壁纸 (.mp4)。
  7. wallpaper_manager_plus 一个插件,用于设置 HomeScreen、LockScreen 和 Both Screen 的壁纸,即使对于大图像也没有延迟。(使用改插件设置壁纸,支持超大尺寸的图片)。

wallpaper_manager_plus 基本用法:

dart
代码解读
复制代码
// 设置网络图片 String url = ''; // Image url // 结合flutter_cache_manager从缓存中读取图片,这里官网文档用的是String,实际是File File cachedimage = await DefaultCacheManager().getSingleFile(url); //image file int location = WallpaperManagerPlus.HOME_SCREEN; //Choose screen type // 第一个参数必须是File类型 WallpaperManagerPlus().setWallpaper(cachedimage, location); // Wrap with try catch for error management. // 同理,设置系统图片时也需要File类型参数 imagefile = /0/images/image.png, location = WallpaperManagerPlus.HOME_SCREEN //Choose screen type WallpaperManagerPlus().setWallpaper(imagefile, location);

基础用法都不做介绍了,主要介绍一些可能遇到的问题:

  • 下载图片、设置壁纸、获取缓存大小的逻辑都在下方;
  • 下载和设置壁纸都直接从缓存中查看是否存在,存在则直接从缓存中拿,减少不必要的网络开销;
  • var file = await DefaultCacheManager().getSingleFile(url); 我用这种方法获取图片缓存,在图片加载成功后,第一次设置壁纸并没有从缓存中直接拿到图片数据,间接导致我使用了两个壁纸设置插件。
  • 使用 var cacheData = await getNetworkImageData(imagePath, useCache: true); 这个方法获取图片缓存,图片加载成功后,第一次设置壁纸就能有效的从缓存中拿到图片数据,在把图片存到临时文件夹中,返回自定义的路径即可,但是这种方法使用 async_wallpaper设置壁纸时在模拟器上没问题,真机上面就失败了,暂时不知道原因,因此用了两个壁纸设置插件。

/lib/tools/down_image.dart

dart
代码解读
复制代码
import 'dart:io'; import 'dart:typed_data'; import 'package:bot_toast/bot_toast.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:dio/dio.dart'; class DownImage { /// 获取应用总缓存大小 (单位: MB) static Future<double> getTotalCacheSize() async { double totalSize = 0; final tempDir = await getTemporaryDirectory(); totalSize += await _getFolderSize(tempDir); return totalSize; } /// 清理所有缓存 static Future<void> clearAllCache() async { // 1. 清理内存中的图片缓存 imageCache.clear(); imageCache.clearLiveImages(); // 2. 清理磁盘中的图片缓存 await DefaultCacheManager().emptyCache(); await clearExtendedImageCache(); // 3. 清理临时目录 final tempDir = await getTemporaryDirectory(); await _deleteFolder(tempDir); // 4. 清理其他缓存(按需添加) // await SharedPreferences.getInstance().then((prefs) => prefs.clear()); // await Hive.deleteFromDisk(); } /// 清理 extended_image 特殊缓存 static Future<void> clearExtendedImageCache() async { try { final directory = Directory( '${(await getTemporaryDirectory()).path}/extended_image_cache'); if (directory.existsSync()) { await directory.delete(recursive: true); } } catch (e) { debugPrint('清理extended_image缓存失败: $e'); } } /// 计算文件夹大小 static Future<double> _getFolderSize(Directory dir) async { if (!dir.existsSync()) return 0; int totalBytes = 0; final files = dir.listSync(recursive: true); await Future.forEach(files, (file) async { if (file is File) { totalBytes += await file.length(); } }); return totalBytes / (1024 * 1024); } /// 删除文件夹 static Future<void> _deleteFolder(Directory dir) async { if (dir.existsSync()) { // ignore: body_might_complete_normally_catch_error await dir.delete(recursive: true).catchError((e) { debugPrint('删除文件夹失败: $e'); }); } } /// 下载网络图片(先读缓存资源,缓存没有再重新获取资源) static Future<String> downloadNetworkImage(String imagePath) async { var status = await Permission.storage.status; if (!status.isGranted) { //未授予 Permission.storage.request(); } const String prefix = 'App-Save'; // 获取缓存图片 var cacheData = await getNetworkImageData(imagePath, useCache: true); // 获取当前时间戳 int timestamp = DateTime.now().millisecondsSinceEpoch; // 将时间戳转换为可读的日期格式 String dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp).toString(); // 拼接名字 String saveName = '$prefix-$dateTime'; var loadingBot = BotToast.showCustomLoading( backgroundColor: const Color.fromARGB(100, 4, 4, 4), toastBuilder: (cancel) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 10), Text( '下载中,请稍后···', style: TextStyle(color: Colors.white, fontSize: 13), ), ], ); }); // 下载逻辑 late dynamic result; // 如果缓存图片不为空 if (cacheData != null) { result = await ImageGallerySaverPlus.saveImage( cacheData, quality: 100, name: saveName, ); } else { var response = await Dio() .get(imagePath, options: Options(responseType: ResponseType.bytes)); result = await ImageGallerySaverPlus.saveImage( Uint8List.fromList(response.data), quality: 100, ); } if (result["isSuccess"]) { loadingBot(); BotToast.showText(text: '下载成功'); return result["filePath"]; } else { loadingBot(); BotToast.showText(text: '下载失败', contentColor: Colors.red); return 'error'; } } Future<String?> setWallpaper(String url) async { try { final dir = await getTemporaryDirectory(); final filename = url.split('/').last; final path = '${dir.path}/$filename'; // 先读取缓存中的图片 存在缓存则直接返回 final cacheData = await getNetworkImageData(url, useCache: true); if (cacheData != null) { await File(path).writeAsBytes(cacheData); return path; } else { final response = await Dio().download(url, path); if (response.statusCode == 200) { return path; } return null; } } catch (e) { print('下载失败: $e'); return null; } } }
  • 图片缓存基本上就是核心问题了,其它都没啥复杂的,各个插件官网教程都很详细,注意有的插件可能需要引入对应的权限即可。

还有一个问题,获取相册中所有图片时,会存在性能问题:

  • id == 'isAll' || name == 'Recent' 就是最大的相册,我是获取全部图片,就直接获取最大的相册了,没有做细分。
  • final authState = await PhotoManager.requestPermissionExtend();这个方法获取到的是所有相册。
  • _loadImages()中 final assets = await album.getAssetListRange(start: start, end: end);可以获取对应相册中所有图像(图片和视频)的信息,只需要图片,在下方做了判断。

刚开始,我是通过album.getAssetListRange(start: start, end: end);方法一次性获取全部图片,图片多就会存在性能问题和等待时间过长问题,丢个ai优化后就是下面这种分页加载的,用这种方式我自己测试是没啥大问题了,可能是我图片不够多的原因,如果图片过多,依然存在性能问题时建议结合滚动事件,滚动到底部时在获取数据,因为提供了单独选择图片的页面,不存在性能问题,懒得弄滚动事件了就沿用下面这种方式了。

dart
代码解读
复制代码
Future<void> _requestPermissionAndLoadAlbums() async { // 请求相册权限 final authState = await PhotoManager.requestPermissionExtend(); if (authState.isAuth) { // 获取所有相册 final albumList = await PhotoManager.getAssetPathList(); for (var i = 0; i < albumList.length; i++) { if (albumList[i].id == 'isAll' || albumList[i].name == 'Recent') { setState(() { album = albumList[i]; }); break; } } } _loadImages(); } // 加载所有图片 Future<void> _loadImages() async { // 清空之前的图片列表 setState(() { imageList.clear(); currentPage = 0; // 重置页码 }); // 分页加载图片 while (true) { int start = currentPage * pageSize; int end = start + pageSize; // 获取当前页的图片 final assets = await album.getAssetListRange(start: start, end: end); // 如果没有更多图片,退出循环 if (assets.isEmpty) { break; } // 处理图片并添加到列表 for (var asset in assets) { if (asset.type == AssetType.image) { final file = await asset.file; // final compressedFile = await _compressImage(file?.path ?? ''); setState(() { imageList.add(file!.path); }); } } currentPage++; } }

3、问题记录

3.1、使用 photo_manager 组件报错

报错内容如下:

logs
代码解读
复制代码
[ ] FAILURE: Build failed with an exception. [ +1 ms] * What went wrong: [ ] Execution failed for task ':photo_manager:compileDebugKotlin'. [ ] > Error while evaluating property 'compilerOptions.jvmTarget' of task ':photo_manager:compileDebugKotlin'. [ ] > Failed to calculate the value of property 'jvmTarget'. [ ] > Unknown Kotlin JVM target: 21 [ ] * Try: [ ] > Run with --debug option to get more log output. [ ] > Run with --scan to get full insights. [ ] > Get more help at https://help.gradle.org. [ +1 ms] * Exception is: [ ] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':photo_manager:compileDebugKotlin'. [ ] at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:38) [ +36 ms] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77) [ ] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55) [ +1 ms] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) [ +1 ms] at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) [ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
  • 解决办法:https://github.com/fluttercandies/flutter_photo_manager/issues/1198
  • 翻了一下评论,不止 photo_manager 插件会报错,其它插件也可能报这个错。
image.png

到这里就能正常运行项目了,但是又出现了一个新的爆红警告(不影响项目启动):

image.png
  • 大概就是版本不兼容的意思,按照他的提示在 android\app\build.gradle 文件中修改 ndk版本即可。

image.png

3.2、使用 async_wallpaper 插件,打包时报错。

微信截图_20250307092852.png

解决 Flutter 使用 async_wallpaper 插件设置视频壁纸时 Gradle 构建错误的方法

当您在 Flutter 项目中使用 async_wallpaper 插件设置视频壁纸时,可能会遇到以下 Gradle 构建错误:

复制

sql
代码解读
复制代码
ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in C:\Users\n03158\Desktop\new_wallpaper\build\app\outputs\mapping\release\missing_rules.txt.

这是因为在构建过程中,R8 检测到某些类缺失。以下是解决此问题的步骤:

检查 missing_rules.txt 文件:

  • 位置:build\app\outputs\mapping\release,上面的报错截图不全,只能看到部分位置信息,完整报错能看到完整的路径。

  • 在 Gradle 构建输出中,您会看到一个路径指向 missing_rules.txt 文件。该文件包含解决此问题所需的 ProGuard 规则。将其内容复制到您的 proguard-rules.pro 文件中。

  • 在 Flutter 项目中添加 ProGuard 规则以解决 R8 缩减代码时的类缺失问题,具体步骤如下:

步骤 1:找到 proguard-rules.pro 文件

  • 该文件通常位于 android/app/proguard-rules.pro 路径下(我的项目下面没有这个文件,是我手动创建的)。

步骤 2:打开文件并添加规则

  • 根据 missing_rules.txt 文件中的提示,将相应的规则添加到 proguard-rules.pro 文件中。
  • 把 missing_rules.txt 文件中的内容 copy 到 proguard-rules.pro 文件中。

步骤 3:保存并重新构建项目

  • 保存更改后,重新运行 flutter build apk 或 flutter build appbundle 命令以重新构建项目。

  • 通过上述步骤,您可以有效地解决 R8 在代码缩减过程中遇到的类缺失问题。

3.3、下拉刷新失效

imgs.isEmpty && !isLoading 逻辑是没有在加载数据中,并且数据为空时显示 Empty() 空组件,有数据时则展示图片。现在的问题是在 Empty() 状态时,下拉刷新不会触发。

dart
代码解读
复制代码
@override Widget build(BuildContext context) { super.build(context); return RefreshIndicator( onRefresh: () { setState(() { imgs.clear(); results.clear(); }); getData(); return Future.delayed( Duration(milliseconds: OptionsBase().refreshTime)); }, child: imgs.isEmpty && !isLoading ? SizedBox( height: MediaQuery.of(context).size.height * 0.8, child: Empty(), ) : CustomScrollView( controller: scrollController, slivers: [ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: OptionsBase().imageColumns(context), childAspectRatio: widget.sort == 'pc' ? 1.5 : 0.7, ), delegate: SliverChildBuilderDelegate( (context, index) => buildItem(context, index), childCount: imgs.length, ), ), if (isLoading) SliverToBoxAdapter( child: Center( child: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).size.height * 0.4), child: CircularProgressIndicator(), ), ), ), ], ), ); }

Empty()组件代码:一个简单的图标和文字描述。

dart
代码解读
复制代码
import 'package:flutter/material.dart'; class Empty extends StatelessWidget { final double width; const Empty({super.key, this.width = 80}); @override Widget build(BuildContext context) { return SizedBox( // 屏幕高度 height: MediaQuery.of(context).size.height - 100, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.upcoming_outlined, size: width, color: Colors.grey[400], ), Text( '暂无数据', style: TextStyle(color: Colors.grey[400], fontSize: 15), ) ], ), ), ); } }

丢给ai,看看ai的分析:

image.png 很显然Empty()就是一个普通的静态小组件,并不具备滚动能力,因此无法触发下拉刷新事件。根本原因还是对组件的属性不熟导致的,丢给ai瞬间就能发现问题了。知道问题就好解决了,直接给Empty()包裹在可滚动的组件中即可。

修改后的代码:

dart
代码解读
复制代码
@override Widget build(BuildContext context) { super.build(context); return RefreshIndicator( onRefresh: () { setState(() { imgs.clear(); results.clear(); }); getData(); return Future.delayed( Duration(milliseconds: OptionsBase().refreshTime)); }, child: imgs.isEmpty && !isLoading ? SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), child: Empty(), ) : CustomScrollView( controller: scrollController, slivers: [ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: OptionsBase().imageColumns(context), childAspectRatio: widget.sort == 'pc' ? 1.5 : 0.7, ), delegate: SliverChildBuilderDelegate( (context, index) => buildItem(context, index), childCount: imgs.length, ), ), if (isLoading) SliverToBoxAdapter( child: Center( child: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).size.height * 0.4), child: CircularProgressIndicator(), ), ), ), ], ), ); }

修改点说明:

  • SingleChildScrollView 和 AlwaysScrollableScrollPhysics() :

    • 使用 SingleChildScrollView 包裹 Empty(),并设置 physics: AlwaysScrollableScrollPhysics(),确保即使内容不足一屏也可以滚动。
    • 这样可以让 RefreshIndicator 始终检测到滚动行为,从而支持下拉刷新。

4、地址

软件体验链接:pan.baidu.com/s/15WyWisXf… 提取码:i975

项目地址: gitee.com/zsnoin-can/…

注:本文转载自juejin.cn的Zsnoin能的文章"https://juejin.cn/post/7478870801157324852"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top