From fdbce75afdab5b7fc3fa056726d9e39ac67c2553 Mon Sep 17 00:00:00 2001 From: niuhuan Date: Sat, 6 Nov 2021 19:25:44 +0800 Subject: [PATCH] resize large image --- .../kotlin/niuhuan/pikapi/MainActivity.kt | 36 ++++++++++++++ lib/basic/Common.dart | 31 +++++++----- lib/basic/Method.dart | 6 +++ lib/basic/config/ConvertToPNG.dart | 48 +++++++++++++++++++ lib/screens/InitScreen.dart | 2 + lib/screens/SettingsScreen.dart | 2 + lib/screens/components/Images.dart | 26 +++++++--- 7 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 lib/basic/config/ConvertToPNG.dart diff --git a/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt index 02b5222..aad640d 100644 --- a/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt +++ b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt @@ -3,11 +3,13 @@ package niuhuan.pikapi import android.content.ContentValues import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Matrix import android.os.Build import android.os.Environment import android.os.Handler import android.os.Looper import android.provider.MediaStore +import android.util.DisplayMetrics import android.util.Log import android.view.Display import android.view.KeyEvent @@ -23,6 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.sync.Mutex import mobile.Mobile +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -100,6 +103,7 @@ class MainActivity : FlutterActivity() { // 获取可以迁移数据地址 "androidGetExtendDirs" -> androidGetExtendDirs() "androidSecureFlag" -> androidSecureFlag(call.argument("flag")!!) + "convertToPNG" -> convertToPNG(call.argument("path")!!) else -> { notImplementedToken } @@ -350,4 +354,36 @@ class MainActivity : FlutterActivity() { } } + private fun convertToPNG(path: String): ByteArray { + BitmapFactory.decodeFile(path)?.let { bitmap -> + val maxWidth = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> windowManager.currentWindowMetrics.bounds.width() + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> { + val displayMetrics = DisplayMetrics() + windowManager.defaultDisplay.getRealMetrics(displayMetrics) + displayMetrics.widthPixels + } + else -> throw Exception("not support") + } + if (bitmap.width > maxWidth) { + val newHeight = maxWidth * bitmap.height / bitmap.width + val newImage = Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true) + return compressBitMap(newImage) + } + return compressBitMap(bitmap) + } + throw Exception("error pic") + } + + private fun compressBitMap(bitmap: Bitmap): ByteArray { + val bos = ByteArrayOutputStream() + bos.use { bos -> + Log.d("BITMAP", bitmap.width.toString()) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos) + } + return bos.toByteArray() + } + + } diff --git a/lib/basic/Common.dart b/lib/basic/Common.dart index e64a461..d1c9cb1 100644 --- a/lib/basic/Common.dart +++ b/lib/basic/Common.dart @@ -95,23 +95,30 @@ List filteredList(List list, bool Function(T) filter) { /// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容 Future chooseListDialog( - BuildContext context, - String title, - List items, -) async { + BuildContext context, String title, List items, + {String? tips}) async { + List widgets = []; + if (tips != null) { + widgets.add( + Container( + padding: EdgeInsets.fromLTRB(15, 5, 15, 15), + child: Text(tips), + ) + ); + } + widgets.addAll(items.map((e) => SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(e); + }, + child: Text('$e'), + ))); + return showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: Text(title), - children: items - .map((e) => SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(e); - }, - child: Text('$e'), - )) - .toList(), + children: widgets, ); }, ); diff --git a/lib/basic/Method.dart b/lib/basic/Method.dart index 1b2df04..d891d0b 100644 --- a/lib/basic/Method.dart +++ b/lib/basic/Method.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:pikapi/basic/Entities.dart'; @@ -607,4 +608,9 @@ class Method { "comicId": comicId, }); } + + /// 转化为PNG + Future convertToPNG(String path) async { + return await _channel.invokeMethod("convertToPNG", {"path": path}); + } } diff --git a/lib/basic/config/ConvertToPNG.dart b/lib/basic/config/ConvertToPNG.dart new file mode 100644 index 0000000..b65fec1 --- /dev/null +++ b/lib/basic/config/ConvertToPNG.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import '../Common.dart'; +import '../Method.dart'; + +const _propertyName = "convertToPNG"; +var _convertToPNG = false; + +Future initConvertToPNG() async { + if (Platform.isAndroid) { + _convertToPNG = + (await method.loadProperty(_propertyName, "false")) == "true"; + } +} + +bool convertToPNG() { + return _convertToPNG; +} + +Future _chooseConvertToPNGSetting(BuildContext context) async { + String? result = await chooseListDialog(context, "超大图片缩放", ["是", "否"], + tips: "会增加耗电\n可以解决部分漫画崩溃的问题"); + if (result != null) { + var target = result == "是"; + await method.saveProperty(_propertyName, "$target"); + _convertToPNG = target; + } +} + +Widget convertToPNGSetting() { + if (Platform.isAndroid) { + return StatefulBuilder( + builder: (BuildContext context, void Function(void Function()) setState) { + return ListTile( + title: Text("读取到超大图片时进行缩放(防止崩溃)"), + subtitle: Text(_convertToPNG ? "是" : "否"), + onTap: () async { + await _chooseConvertToPNGSetting(context); + setState(() {}); + }, + ); + }, + ); + } + return Container(); +} diff --git a/lib/screens/InitScreen.dart b/lib/screens/InitScreen.dart index 95ddfdc..9d5eadf 100644 --- a/lib/screens/InitScreen.dart +++ b/lib/screens/InitScreen.dart @@ -6,6 +6,7 @@ import 'package:pikapi/basic/config/AutoClean.dart'; import 'package:pikapi/basic/config/AutoFullScreen.dart'; import 'package:pikapi/basic/config/ChooserRoot.dart'; import 'package:pikapi/basic/config/ContentFailedReloadAction.dart'; +import 'package:pikapi/basic/config/ConvertToPNG.dart'; import 'package:pikapi/basic/config/DownloadAndExportPath.dart'; import 'package:pikapi/basic/config/DownloadThreadCount.dart'; import 'package:pikapi/basic/config/FullScreenAction.dart'; @@ -67,6 +68,7 @@ class _InitScreenState extends State { await initDownloadAndExportPath(); await initAndroidSecureFlag(); await initDownloadThreadCount(); + await initConvertToPNG(); // 登录, 如果token失效重新登录, 网络不好的时候可能需要1分钟 if (await method.preLogin()) { // 如果token或username+password有效则直接进入登录好的界面 diff --git a/lib/screens/SettingsScreen.dart b/lib/screens/SettingsScreen.dart index ece9807..b43ce01 100644 --- a/lib/screens/SettingsScreen.dart +++ b/lib/screens/SettingsScreen.dart @@ -9,6 +9,7 @@ import 'package:pikapi/basic/config/AutoClean.dart'; import 'package:pikapi/basic/config/AutoFullScreen.dart'; import 'package:pikapi/basic/config/ChooserRoot.dart'; import 'package:pikapi/basic/config/ContentFailedReloadAction.dart'; +import 'package:pikapi/basic/config/ConvertToPNG.dart'; import 'package:pikapi/basic/config/DownloadAndExportPath.dart'; import 'package:pikapi/basic/config/DownloadThreadCount.dart'; import 'package:pikapi/basic/config/FullScreenAction.dart'; @@ -37,6 +38,7 @@ class SettingsScreen extends StatelessWidget { NetworkSetting(), Divider(), qualitySetting(), + convertToPNGSetting(), readerTypeSetting(), readerDirectionSetting(), autoFullScreenSetting(), diff --git a/lib/screens/components/Images.dart b/lib/screens/components/Images.dart index dd5559a..7545391 100644 --- a/lib/screens/components/Images.dart +++ b/lib/screens/components/Images.dart @@ -1,20 +1,31 @@ +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pikapi/basic/Method.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:pikapi/basic/config/ConvertToPNG.dart'; import 'dart:io'; import 'dart:ui' as ui show Codec; +Future _loadImageFile(String path) { + if (convertToPNG()) { + return method.convertToPNG(path); + } + return File(path).readAsBytes(); +} + // 从本地加载图片 -class ResourceFileImageProvider extends ImageProvider { +class ResourceFileImageProvider + extends ImageProvider { final String path; final double scale; ResourceFileImageProvider(this.path, {this.scale = 1.0}); @override - ImageStreamCompleter load(ResourceFileImageProvider key, DecoderCallback decode) { + ImageStreamCompleter load( + ResourceFileImageProvider key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: key.scale, @@ -22,14 +33,15 @@ class ResourceFileImageProvider extends ImageProvider } @override - Future obtainKey(ImageConfiguration configuration) { + Future obtainKey( + ImageConfiguration configuration) { return SynchronousFuture(this); } Future _loadAsync(ResourceFileImageProvider key) async { assert(key == this); return PaintingBinding.instance! - .instantiateImageCodec(await File(path).readAsBytes()); + .instantiateImageCodec(await _loadImageFile(path)); } @override @@ -96,7 +108,8 @@ class ResourceDownloadFileImageProvider } // 从远端加载图片 暂时未使用 (现在都是先获取路径然后再通过file显示) -class ResourceRemoteImageProvider extends ImageProvider { +class ResourceRemoteImageProvider + extends ImageProvider { final String fileServer; final String path; final double scale; @@ -113,7 +126,8 @@ class ResourceRemoteImageProvider extends ImageProvider obtainKey(ImageConfiguration configuration) { + Future obtainKey( + ImageConfiguration configuration) { return SynchronousFuture(this); }