pref: 优化图片保存

This commit is contained in:
HuanCheng65 2023-02-19 15:37:10 +08:00
parent 254c844ed2
commit 90cebdb90d
No known key found for this signature in database
GPG Key ID: E9031EF91A805148
4 changed files with 705 additions and 679 deletions

View File

@ -30,6 +30,7 @@ import com.huanchengfly.tieba.post.models.PhotoViewBean
import com.huanchengfly.tieba.post.ui.common.theme.utils.ThemeUtils
import com.huanchengfly.tieba.post.utils.AnimUtil
import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.download
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -95,7 +96,7 @@ class PhotoViewActivity : BaseActivity(), OnChangeBottomBarVisibilityListener,
}
picBeans.addAll(data.picList)
picBeans.forEach {
it.img.original?.let { it1 -> imgInfoBeans.add(it1) }
it.img.original.let { it1 -> imgInfoBeans.add(it1) }
}
lastIndex = picBeans.first().overAllIndex.toInt()
for (photoViewBean in photoViewBeans) {
@ -277,7 +278,7 @@ class PhotoViewActivity : BaseActivity(), OnChangeBottomBarVisibilityListener,
this,
mAdapter.getBean(mViewPager.currentItem).originUrl,
true
) { uri: Uri? ->
) { uri: Uri ->
val intent = Intent(Intent.ACTION_SEND)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.putExtra(Intent.EXTRA_STREAM, uri)

View File

@ -51,6 +51,7 @@ import com.huanchengfly.tieba.post.toastShort
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.download
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt

View File

@ -1,677 +0,0 @@
package com.huanchengfly.tieba.post.utils;
import static com.huanchengfly.tieba.post.utils.FileUtil.FILE_FOLDER;
import static com.huanchengfly.tieba.post.utils.FileUtil.changeFileExtension;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.webkit.URLUtil;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.FileProvider;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.huanchengfly.tieba.post.App;
import com.huanchengfly.tieba.post.ExtensionsKt;
import com.huanchengfly.tieba.post.R;
import com.huanchengfly.tieba.post.activities.PhotoViewActivity;
import com.huanchengfly.tieba.post.components.transformations.RadiusTransformation;
import com.huanchengfly.tieba.post.models.PhotoViewBean;
import com.zhihu.matisse.MimeType;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ImageUtil {
/**
* 智能省流
*/
public static final int SETTINGS_SMART_ORIGIN = 0;
/**
* 智能无图
*/
public static final int SETTINGS_SMART_LOAD = 1;
/**
* 始终高质量
*/
public static final int SETTINGS_ALL_ORIGIN = 2;
/**
* 始终无图
*/
public static final int SETTINGS_ALL_NO = 3;
public static final int LOAD_TYPE_SMALL_PIC = 0;
public static final int LOAD_TYPE_AVATAR = 1;
public static final int LOAD_TYPE_NO_RADIUS = 2;
public static final int LOAD_TYPE_ALWAYS_ROUND = 3;
public static final String TAG = "ImageUtil";
private static boolean isGifFile(File file) {
if (file == null) return false;
try {
return isGifFile(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return false;
}
//判断是否为GIF文件
private static boolean isGifFile(InputStream inputStream) {
if (inputStream == null) return false;
byte[] bytes = new byte[4];
try {
inputStream.read(bytes);
inputStream.close();
String str = new String(bytes);
return str.equalsIgnoreCase("GIF8");
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
public static File compressImage(Bitmap bitmap, File output, int maxSize) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);//质量压缩方法这里100表示不压缩把压缩后的数据存放到baos中
while (baos.toByteArray().length / 1024 > maxSize && quality > 0) { //循环判断如果压缩后图片是否大于20kb,大于继续压缩 友盟缩略图要求不大于18kb
baos.reset();//重置baos即清空baos
quality -= 5;//每次都减少5
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);//这里压缩options%把压缩后的数据存放到baos中
}
try {
FileOutputStream fos = new FileOutputStream(output);
try {
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return output;
}
public static File bitmapToFile(Bitmap bitmap, File output) {
return bitmapToFile(bitmap, output, Bitmap.CompressFormat.JPEG);
}
public static File bitmapToFile(Bitmap bitmap, File output, Bitmap.CompressFormat format) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(format, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(output);
try {
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return output;
}
public static File compressImage(Bitmap bitmap, File output) {
return compressImage(bitmap, output, 100);
}
public static Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}
public static boolean copyFile(FileInputStream src, FileOutputStream dest) {
boolean result = false;
if ((src == null) || (dest == null)) {
return result;
}
FileChannel srcChannel = null;
FileChannel dstChannel = null;
try {
srcChannel = src.getChannel();
dstChannel = dest.getChannel();
srcChannel.transferTo(0, srcChannel.size(), dstChannel);
result = true;
} catch (IOException e) {
e.printStackTrace();
return result;
}
try {
srcChannel.close();
dstChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
public static boolean copyFile(File src, File dest) {
boolean result = false;
if ((src == null) || (dest == null)) {
return result;
}
if (dest.exists()) {
dest.delete();
}
try {
dest.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
FileChannel srcChannel = null;
FileChannel dstChannel = null;
try {
srcChannel = new FileInputStream(src).getChannel();
dstChannel = new FileOutputStream(dest).getChannel();
srcChannel.transferTo(0, srcChannel.size(), dstChannel);
result = true;
} catch (IOException e) {
e.printStackTrace();
return result;
}
try {
srcChannel.close();
dstChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private static void changeBrightness(ImageView imageView, int brightness) {
ColorMatrix cMatrix = new ColorMatrix();
cMatrix.set(new float[]{1, 0, 0, 0, brightness, 0, 1, 0, 0, brightness, // 改变亮度
0, 0, 1, 0, brightness, 0, 0, 0, 1, 0});
imageView.setColorFilter(new ColorMatrixColorFilter(cMatrix));
}
@SuppressLint("StaticFieldLeak")
public static void download(Context context, String url) {
download(context, url, false, null);
}
private static void downloadForShare(Context context, String url, @NonNull ShareTaskCallback taskCallback) {
new DownloadAsyncTask(context, url, file -> {
File pictureFolder = new File(context.getCacheDir(), ".shareTemp");
if (pictureFolder.exists() || pictureFolder.mkdirs()) {
String fileName = "share_" + System.currentTimeMillis();
File destFile = new File(pictureFolder, fileName);
if (!destFile.exists()) {
copyFile(file, destFile);
}
Uri shareUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
shareUri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".share.FileProvider",
destFile
);
} else {
shareUri = Uri.fromFile(destFile);
}
taskCallback.onGetUri(shareUri);
}
}).execute();
}
@SuppressLint("StaticFieldLeak")
public static void download(Context context, String url, boolean forShare, @Nullable ShareTaskCallback taskCallback) {
if (forShare) {
if (taskCallback != null) downloadForShare(context, url, taskCallback);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
downloadAboveQ(context, url);
return;
}
PermissionUtils.INSTANCE.askPermission(
context,
new PermissionUtils.PermissionData(
Arrays.asList(PermissionUtils.READ_EXTERNAL_STORAGE, PermissionUtils.WRITE_EXTERNAL_STORAGE),
context.getString(R.string.tip_permission_storage)
),
R.string.toast_no_permission_save_photo,
() -> {
downloadBelowQ(context, url);
return null;
});
}
private static void downloadAboveQ(Context context, String url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return;
}
new DownloadAsyncTask(context, url, file -> {
String mimeType = MimeType.JPEG.toString();
String fileName = URLUtil.guessFileName(url, null, MimeType.JPEG.toString());
if (isGifFile(file)) {
fileName = changeFileExtension(fileName, ".gif");
mimeType = MimeType.GIF.toString();
}
Log.i(TAG, "download: fileName = " + fileName);
String relativePath = Environment.DIRECTORY_PICTURES + File.separator + FILE_FOLDER;
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.RELATIVE_PATH, relativePath);
values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
values.put(MediaStore.Images.Media.DESCRIPTION, fileName);
Uri uri = null;
ContentResolver cr = context.getContentResolver();
try {
uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri == null) {
return;
}
ParcelFileDescriptor descriptor = cr.openFileDescriptor(uri, "w");
FileOutputStream outputStream = new FileOutputStream(descriptor.getFileDescriptor());
FileInputStream inputStream = new FileInputStream(file);
copyFile(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
cr.delete(uri, null, null);
}
return;
}
Toast.makeText(context, context.getString(R.string.toast_photo_saved, relativePath), Toast.LENGTH_SHORT).show();
}).execute();
}
private static void downloadBelowQ(Context context, String url) {
new DownloadAsyncTask(context, url, file -> {
File pictureFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsoluteFile();
File appDir;
appDir = new File(pictureFolder, FILE_FOLDER);
if (appDir.exists() || appDir.mkdirs()) {
String fileName = URLUtil.guessFileName(url, null, MimeType.JPEG.toString());
if (isGifFile(file)) {
fileName = changeFileExtension(fileName, ".gif");
}
File destFile = new File(appDir, fileName);
if (destFile.exists()) {
return;
}
copyFile(file, destFile);
checkGifFile(destFile);
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(new File(destFile.getPath()))));
Toast.makeText(context, context.getString(R.string.toast_photo_saved, destFile.getPath()), Toast.LENGTH_SHORT).show();
}
}).execute();
}
private static void checkGifFile(File file) {
if (isGifFile(file)) {
File gifFile = new File(file.getParentFile(), FileUtil.changeFileExtension(file.getName(), ".gif"));
if (gifFile.exists()) {
file.delete();
} else {
file.renameTo(gifFile);
}
}
}
@NonNull
public static String getPicId(String picUrl) {
String fileName = URLUtil.guessFileName(picUrl, null, MimeType.JPEG.toString());
return fileName.replace(".jpg", "");
}
public static void initImageView(@NonNull ImageView view, List<PhotoViewBean> photoViewBeans, int position, String forumName, String forumId, String threadId, boolean seeLz, String objType) {
view.setOnClickListener(v -> {
Object tag = view.getTag(R.id.image_load_tag);
if (tag != null) {
boolean loaded = (boolean) tag;
if (loaded) {
PhotoViewActivity.launch(v.getContext(), photoViewBeans.toArray(new PhotoViewBean[0]), position, forumName, forumId, threadId, seeLz, objType);
} else {
load(view, LOAD_TYPE_SMALL_PIC, photoViewBeans.get(position).getUrl(), true);
}
}
});
view.setOnLongClickListener(v -> {
PopupMenu popupMenu = PopupUtil.create(view);
popupMenu.getMenuInflater().inflate(R.menu.menu_image_long_click, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.menu_save_image) {
download(view.getContext(), photoViewBeans.get(position).getOriginUrl());
return true;
}
return false;
});
popupMenu.show();
return true;
});
}
public static void initImageView(ImageView view, List<PhotoViewBean> photoViewBeans, int position) {
view.setOnClickListener(v -> {
Object tag = view.getTag(R.id.image_load_tag);
if (tag != null) {
boolean loaded = (boolean) tag;
if (loaded) {
PhotoViewActivity.launch(v.getContext(), photoViewBeans, position);
} else {
load(view, LOAD_TYPE_SMALL_PIC, photoViewBeans.get(position).getUrl(), true);
}
}
});
view.setOnLongClickListener(v -> {
PopupMenu popupMenu = PopupUtil.create(view);
popupMenu.getMenuInflater().inflate(R.menu.menu_image_long_click, popupMenu.getMenu());
popupMenu.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.menu_save_image) {
download(view.getContext(), photoViewBeans.get(position).getOriginUrl());
return true;
}
return false;
});
popupMenu.show();
return true;
});
}
public static void initImageView(ImageView view, PhotoViewBean photoViewBean) {
List<PhotoViewBean> photoViewBeans = new ArrayList<>();
photoViewBeans.add(photoViewBean);
initImageView(view, photoViewBeans, 0);
}
public static String getNonNullString(String... strings) {
for (String url : strings) {
if (!TextUtils.isEmpty(url)) {
return url;
}
}
return null;
}
public static int getRadiusPx(Context context) {
return DisplayUtil.dp2px(context, getRadiusDp(context));
}
public static int getRadiusDp(Context context) {
return AppPreferencesUtilsKt.getAppPreferences(context).getRadius();
}
public static void load(ImageView imageView, @LoadType int type, String url) {
load(imageView, type, url, false);
}
public static void clear(ImageView imageView) {
Glide.with(imageView).clear(imageView);
}
@NonNull
public static Drawable getPlaceHolder(Context context, int radius) {
GradientDrawable drawable = new GradientDrawable();
int color = ThemeUtil.isNightMode() ?
context.getResources().getColor(R.color.color_place_holder_night) :
context.getResources().getColor(R.color.color_place_holder);
drawable.setColor(color);
drawable.setCornerRadius(DisplayUtil.dp2px(context, radius));
return drawable;
}
@SuppressLint("CheckResult")
public static void load(
ImageView imageView,
@LoadType int type,
String url,
boolean skipNetworkCheck,
boolean noTransition
) {
if (!Util.canLoadGlide(imageView.getContext())) {
return;
}
int radius = getRadiusDp(imageView.getContext());
RequestBuilder<Drawable> requestBuilder;
if (skipNetworkCheck ||
type == LOAD_TYPE_AVATAR ||
getImageLoadSettings() == SETTINGS_ALL_ORIGIN ||
getImageLoadSettings() == SETTINGS_SMART_ORIGIN ||
(getImageLoadSettings() == SETTINGS_SMART_LOAD && NetworkUtil.isWifiConnected(imageView.getContext()))) {
imageView.setTag(R.id.image_load_tag, true);
requestBuilder = Glide.with(imageView).load(url);
} else {
imageView.setTag(R.id.image_load_tag, false);
requestBuilder = Glide.with(imageView).load(getPlaceHolder(imageView.getContext(), type == LOAD_TYPE_SMALL_PIC ? radius : 0));
}
if (ThemeUtil.isNightMode()) {
changeBrightness(imageView, -35);
} else {
imageView.clearColorFilter();
}
switch (type) {
case LOAD_TYPE_SMALL_PIC:
requestBuilder.apply(RequestOptions.bitmapTransform(new RadiusTransformation(radius))
.placeholder(getPlaceHolder(imageView.getContext(), radius))
.skipMemoryCache(true));
break;
case LOAD_TYPE_AVATAR:
requestBuilder.apply(RequestOptions.bitmapTransform(new RadiusTransformation(6))
.placeholder(getPlaceHolder(imageView.getContext(), 6))
.skipMemoryCache(true));
break;
case LOAD_TYPE_NO_RADIUS:
requestBuilder.apply(new RequestOptions()
.placeholder(getPlaceHolder(imageView.getContext(), 0))
.skipMemoryCache(true));
break;
case LOAD_TYPE_ALWAYS_ROUND:
requestBuilder.apply(new RequestOptions()
.circleCrop()
.placeholder(getPlaceHolder(imageView.getContext(), ExtensionsKt.dpToPx(100)))
.skipMemoryCache(true));
break;
}
if (!noTransition) {
requestBuilder.transition(DrawableTransitionOptions.withCrossFade());
}
requestBuilder.into(imageView);
}
public static void load(
ImageView imageView,
@LoadType int type,
String url,
boolean skipNetworkCheck
) {
load(imageView, type, url, skipNetworkCheck, false);
}
/**
* 获取要加载的图片 Url
*
* @param isSmallPic 加载的是否为缩略图
* @param originUrl 原图 Url
* @param smallPicUrls 缩略图 Url按照画质从好到差排序
* @return 要加载的图片 Url
*/
public static String getUrl(Context context, boolean isSmallPic, @NonNull String originUrl, @NonNull String... smallPicUrls) {
List<String> urls = Arrays.asList(smallPicUrls);
if (isSmallPic) {
if (needReverse(context)) {
Collections.reverse(urls);
}
for (String url : urls) {
if (!TextUtils.isEmpty(url)) {
return url;
}
}
}
return originUrl;
}
private static boolean needReverse(Context context) {
if (getImageLoadSettings() == SETTINGS_SMART_ORIGIN && NetworkUtil.isWifiConnected(context)) {
return false;
}
return getImageLoadSettings() != SETTINGS_ALL_ORIGIN;
}
@ImageLoadSettings
private static int getImageLoadSettings() {
return Integer.parseInt(AppPreferencesUtilsKt.getAppPreferences(App.getINSTANCE()).getImageLoadType());
}
public static String imageToBase64(InputStream is) {
if (is == null) {
return null;
}
byte[] data = null;
String result = null;
try {
//创建一个字符流大小的数组
data = new byte[is.available()];
//写入数组
is.read(data);
//用默认的编码格式进行编码
result = Base64.encodeToString(data, Base64.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
public static String imageToBase64(File file) {
if (file == null) {
return null;
}
String result = null;
try {
InputStream is = new FileInputStream(file);
result = imageToBase64(is);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
public interface ShareTaskCallback {
void onGetUri(Uri uri);
}
@IntDef({LOAD_TYPE_SMALL_PIC, LOAD_TYPE_AVATAR, LOAD_TYPE_NO_RADIUS, LOAD_TYPE_ALWAYS_ROUND})
@Retention(RetentionPolicy.SOURCE)
public @interface LoadType {
}
@IntDef({SETTINGS_SMART_ORIGIN, SETTINGS_SMART_LOAD, SETTINGS_ALL_ORIGIN, SETTINGS_ALL_NO})
public @interface ImageLoadSettings {
}
public static class DownloadAsyncTask extends AsyncTask<Void, Integer, File> {
private final WeakReference<Context> contextWeakReference;
private final TaskCallback callback;
private final String url;
public DownloadAsyncTask(Context context, String url, TaskCallback callback) {
this.contextWeakReference = new WeakReference<>(context);
this.url = url;
this.callback = callback;
}
public TaskCallback getCallback() {
return callback;
}
public String getUrl() {
return url;
}
private Context getContext() {
return contextWeakReference.get();
}
@Override
protected File doInBackground(Void... voids) {
File file = null;
try {
FutureTarget<File> future = Glide.with(getContext())
.asFile()
.load(getUrl())
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
file = future.get();
return file;
} catch (Exception e) {
e.printStackTrace();
}
return file;
}
@Override
protected void onPostExecute(File file) {
super.onPostExecute(file);
getCallback().onPostExecute(file);
}
public interface TaskCallback {
void onPostExecute(File file);
}
}
}

View File

@ -0,0 +1,701 @@
package com.huanchengfly.tieba.post.utils
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Canvas
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Base64
import android.view.MenuItem
import android.view.View
import android.webkit.URLUtil
import android.widget.ImageView
import android.widget.Toast
import androidx.annotation.IntDef
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.github.panpf.sketch.request.DownloadRequest
import com.github.panpf.sketch.request.DownloadResult
import com.huanchengfly.tieba.post.App.Companion.INSTANCE
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.PhotoViewActivity.Companion.launch
import com.huanchengfly.tieba.post.components.transformations.RadiusTransformation
import com.huanchengfly.tieba.post.dpToPx
import com.huanchengfly.tieba.post.models.PhotoViewBean
import com.huanchengfly.tieba.post.utils.PermissionUtils.PermissionData
import com.huanchengfly.tieba.post.utils.PermissionUtils.askPermission
import com.huanchengfly.tieba.post.utils.ThemeUtil.isNightMode
import com.zhihu.matisse.MimeType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.channels.FileChannel
object ImageUtil {
/**
* 智能省流
*/
const val SETTINGS_SMART_ORIGIN = 0
/**
* 智能无图
*/
const val SETTINGS_SMART_LOAD = 1
/**
* 始终高质量
*/
const val SETTINGS_ALL_ORIGIN = 2
/**
* 始终无图
*/
const val SETTINGS_ALL_NO = 3
const val LOAD_TYPE_SMALL_PIC = 0
const val LOAD_TYPE_AVATAR = 1
const val LOAD_TYPE_NO_RADIUS = 2
const val LOAD_TYPE_ALWAYS_ROUND = 3
const val TAG = "ImageUtil"
private fun isGifFile(file: File?): Boolean {
if (file == null) return false
try {
return FileInputStream(file).use { isGifFile(it) }
} catch (e: FileNotFoundException) {
e.printStackTrace()
}
return false
}
//判断是否为GIF文件
private fun isGifFile(inputStream: InputStream?): Boolean {
if (inputStream == null) return false
val bytes = ByteArray(4)
try {
inputStream.mark(0)
inputStream.read(bytes)
inputStream.reset()
val str = String(bytes)
return str.equals("GIF8", ignoreCase = true)
} catch (e: IOException) {
e.printStackTrace()
}
return false
}
@JvmOverloads
fun compressImage(bitmap: Bitmap, output: File, maxSize: Int = 100): File {
val baos = ByteArrayOutputStream()
var quality = 100
bitmap.compress(CompressFormat.JPEG, quality, baos) //质量压缩方法这里100表示不压缩把压缩后的数据存放到baos中
while (baos.toByteArray().size / 1024 > maxSize && quality > 0) { //循环判断如果压缩后图片是否大于20kb,大于继续压缩 友盟缩略图要求不大于18kb
baos.reset() //重置baos即清空baos
quality -= 5 //每次都减少5
bitmap.compress(CompressFormat.JPEG, quality, baos) //这里压缩options%把压缩后的数据存放到baos中
}
try {
val fos = FileOutputStream(output)
try {
fos.write(baos.toByteArray())
fos.flush()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
}
return output
}
@JvmOverloads
fun bitmapToFile(
bitmap: Bitmap,
output: File,
format: CompressFormat? = CompressFormat.JPEG
): File {
val baos = ByteArrayOutputStream()
bitmap.compress(format, 100, baos)
try {
val fos = FileOutputStream(output)
try {
fos.write(baos.toByteArray())
fos.flush()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
}
return output
}
fun drawableToBitmap(drawable: Drawable): Bitmap {
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
if (drawable.opacity != PixelFormat.OPAQUE) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(canvas)
return bitmap
}
fun copyFile(src: FileInputStream?, dest: FileOutputStream?): Boolean {
if (src == null || dest == null) {
return false
}
val srcChannel: FileChannel?
val dstChannel: FileChannel?
try {
srcChannel = src.channel
dstChannel = dest.channel
srcChannel.transferTo(0, srcChannel.size(), dstChannel)
} catch (e: IOException) {
e.printStackTrace()
return false
}
try {
srcChannel.close()
dstChannel.close()
} catch (e: IOException) {
e.printStackTrace()
}
return true
}
fun copyFile(src: File?, dest: File?): Boolean {
if (src == null || dest == null) {
return false
}
if (dest.exists()) {
dest.delete()
}
try {
dest.createNewFile()
} catch (e: IOException) {
e.printStackTrace()
}
val srcChannel: FileChannel?
val dstChannel: FileChannel?
try {
srcChannel = FileInputStream(src).channel
dstChannel = FileOutputStream(dest).channel
srcChannel.transferTo(0, srcChannel.size(), dstChannel)
} catch (e: IOException) {
e.printStackTrace()
return false
}
try {
srcChannel.close()
dstChannel.close()
} catch (e: IOException) {
e.printStackTrace()
}
return true
}
private fun changeBrightness(imageView: ImageView, brightness: Int) {
val cMatrix = ColorMatrix()
cMatrix.set(
floatArrayOf(
1f, 0f, 0f, 0f, brightness.toFloat(), 0f, 1f, 0f, 0f, brightness.toFloat(), // 改变亮度
0f, 0f, 1f, 0f, brightness.toFloat(), 0f, 0f, 0f, 1f, 0f
)
)
imageView.colorFilter = ColorMatrixColorFilter(cMatrix)
}
@SuppressLint("StaticFieldLeak")
fun download(context: Context, url: String?) {
download(context, url, false, null)
}
private fun downloadForShare(context: Context, url: String?, taskCallback: ShareTaskCallback) {
if (url == null) return
CoroutineScope(Dispatchers.IO).launch {
val downloadResult = DownloadRequest(context, url).execute()
if (downloadResult is DownloadResult.Success) {
val inputStream = downloadResult.data.data.newInputStream()
val pictureFolder = File(context.cacheDir, ".shareTemp")
if (pictureFolder.exists() || pictureFolder.mkdirs()) {
val fileName = "share_" + System.currentTimeMillis()
val destFile = File(pictureFolder, fileName)
if (!destFile.exists()) {
withContext(Dispatchers.IO) {
destFile.createNewFile()
}
}
inputStream.use { input ->
if (destFile.canWrite()) {
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
val shareUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
context,
context.packageName + ".share.FileProvider",
destFile
)
} else {
Uri.fromFile(destFile)
}
withContext(Dispatchers.Main) {
taskCallback.onGetUri(shareUri)
}
}
}
}
}
@SuppressLint("StaticFieldLeak")
fun download(
context: Context,
url: String?,
forShare: Boolean,
taskCallback: ShareTaskCallback?
) {
if (forShare) {
if (taskCallback != null) downloadForShare(context, url, taskCallback)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
downloadAboveQ(context, url)
return
}
askPermission(
context,
PermissionData(
listOf(
PermissionUtils.READ_EXTERNAL_STORAGE,
PermissionUtils.WRITE_EXTERNAL_STORAGE
),
context.getString(R.string.tip_permission_storage)
),
R.string.toast_no_permission_save_photo
) {
downloadBelowQ(context, url)
}
}
private fun downloadAboveQ(context: Context, url: String?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
}
CoroutineScope(Dispatchers.IO).launch {
val downloadResult = DownloadRequest(context, url).execute()
if (downloadResult is DownloadResult.Success) {
var mimeType = MimeType.JPEG.toString()
var fileName = URLUtil.guessFileName(url, null, mimeType)
downloadResult.data.data.newInputStream().use { inputStream ->
if (isGifFile(inputStream)) {
mimeType = MimeType.GIF.toString()
fileName = FileUtil.changeFileExtension(fileName, ".gif")
}
}
downloadResult.data.data.newInputStream().use { inputStream ->
val relativePath =
Environment.DIRECTORY_PICTURES + File.separator + FileUtil.FILE_FOLDER
val values = ContentValues().apply {
put(MediaStore.Images.Media.RELATIVE_PATH, relativePath)
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
put(MediaStore.Images.Media.DESCRIPTION, fileName)
}
val cr = context.contentResolver
val uri: Uri = runCatching {
cr.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
}.getOrNull() ?: return@launch
try {
cr.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.toast_photo_saved, relativePath),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
e.printStackTrace()
cr.delete(uri, null, null)
}
}
}
}
}
private fun downloadBelowQ(context: Context, url: String?) {
CoroutineScope(Dispatchers.IO).launch {
val downloadResult = DownloadRequest(context, url).execute()
if (downloadResult is DownloadResult.Success) {
var fileName = URLUtil.guessFileName(url, null, MimeType.JPEG.toString())
downloadResult.data.data.newInputStream().use { inputStream ->
if (isGifFile(inputStream)) {
fileName = FileUtil.changeFileExtension(fileName, ".gif")
}
}
downloadResult.data.data.newInputStream().use { inputStream ->
val pictureFolder =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val appDir = File(pictureFolder, FileUtil.FILE_FOLDER)
val dirExists =
withContext(Dispatchers.IO) { appDir.exists() || appDir.mkdirs() }
if (dirExists) {
val destFile = File(appDir, fileName)
if (!destFile.exists()) {
withContext(Dispatchers.IO) {
destFile.createNewFile()
}
}
destFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
context.sendBroadcast(
Intent(
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(File(destFile.path))
)
)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.toast_photo_saved, destFile.path),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
}
private fun checkGifFile(file: File) {
if (isGifFile(file)) {
val gifFile = File(file.parentFile, FileUtil.changeFileExtension(file.name, ".gif"))
if (gifFile.exists()) {
file.delete()
} else {
file.renameTo(gifFile)
}
}
}
fun getPicId(picUrl: String?): String {
val fileName = URLUtil.guessFileName(picUrl, null, MimeType.JPEG.toString())
return fileName.replace(".jpg", "")
}
@JvmStatic
fun initImageView(
view: ImageView,
photoViewBeans: List<PhotoViewBean?>,
position: Int,
forumName: String?,
forumId: String?,
threadId: String?,
seeLz: Boolean,
objType: String?
) {
view.setOnClickListener { v: View ->
val tag = view.getTag(R.id.image_load_tag)
if (tag != null) {
val loaded = tag as Boolean
if (loaded) {
launch(
v.context,
photoViewBeans.toTypedArray(),
position,
forumName,
forumId,
threadId,
seeLz,
objType
)
} else {
load(view, LOAD_TYPE_SMALL_PIC, photoViewBeans[position]!!.url, true)
}
}
}
view.setOnLongClickListener {
val popupMenu = PopupUtil.create(view)
popupMenu.menuInflater.inflate(R.menu.menu_image_long_click, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
if (item.itemId == R.id.menu_save_image) {
download(view.context, photoViewBeans[position]!!.originUrl)
return@setOnMenuItemClickListener true
}
false
}
popupMenu.show()
true
}
}
@JvmStatic
fun initImageView(view: ImageView, photoViewBeans: List<PhotoViewBean>, position: Int) {
view.setOnClickListener { v: View ->
val tag = view.getTag(R.id.image_load_tag)
if (tag != null) {
val loaded = tag as Boolean
if (loaded) {
launch(v.context, photoViewBeans, position)
} else {
load(view, LOAD_TYPE_SMALL_PIC, photoViewBeans[position].url, true)
}
}
}
view.setOnLongClickListener {
val popupMenu = PopupUtil.create(view)
popupMenu.menuInflater.inflate(R.menu.menu_image_long_click, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
if (item.itemId == R.id.menu_save_image) {
download(view.context, photoViewBeans[position].originUrl)
return@setOnMenuItemClickListener true
}
false
}
popupMenu.show()
true
}
}
fun initImageView(view: ImageView, photoViewBean: PhotoViewBean) {
val photoViewBeans: MutableList<PhotoViewBean> = ArrayList()
photoViewBeans.add(photoViewBean)
initImageView(view, photoViewBeans, 0)
}
@JvmStatic
fun getNonNullString(vararg strings: String?): String? {
for (url in strings) {
if (!TextUtils.isEmpty(url)) {
return url
}
}
return null
}
@JvmStatic
fun getRadiusPx(context: Context): Int {
return DisplayUtil.dp2px(context, getRadiusDp(context).toFloat())
}
private fun getRadiusDp(context: Context): Int {
return context.appPreferences.radius
}
fun clear(imageView: ImageView?) {
Glide.with(imageView!!).clear(imageView)
}
fun getPlaceHolder(context: Context, radius: Int): Drawable {
val drawable = GradientDrawable()
val colorResId =
if (isNightMode()) R.color.color_place_holder_night else R.color.color_place_holder
val color = ContextCompat.getColor(context, colorResId)
drawable.setColor(color)
drawable.cornerRadius =
DisplayUtil.dp2px(context, radius.toFloat()).toFloat()
return drawable
}
@SuppressLint("CheckResult")
fun load(
imageView: ImageView,
@LoadType type: Int,
url: String?,
skipNetworkCheck: Boolean,
noTransition: Boolean
) {
if (!Util.canLoadGlide(imageView.context)) {
return
}
val radius = getRadiusDp(imageView.context)
val requestBuilder =
if (skipNetworkCheck ||
type == LOAD_TYPE_AVATAR ||
imageLoadSettings == SETTINGS_ALL_ORIGIN ||
imageLoadSettings == SETTINGS_SMART_ORIGIN ||
(imageLoadSettings == SETTINGS_SMART_LOAD && NetworkUtil.isWifiConnected(imageView.context))
) {
imageView.setTag(R.id.image_load_tag, true)
Glide.with(imageView).load(url)
} else {
imageView.setTag(R.id.image_load_tag, false)
Glide.with(imageView).load(
getPlaceHolder(
imageView.context,
if (type == LOAD_TYPE_SMALL_PIC) radius else 0
)
)
}
if (isNightMode()) {
changeBrightness(imageView, -35)
} else {
imageView.clearColorFilter()
}
when (type) {
LOAD_TYPE_SMALL_PIC -> requestBuilder.apply(
RequestOptions.bitmapTransform(RadiusTransformation(radius))
.placeholder(getPlaceHolder(imageView.context, radius))
.skipMemoryCache(true)
)
LOAD_TYPE_AVATAR -> requestBuilder.apply(
RequestOptions.bitmapTransform(RadiusTransformation(6))
.placeholder(getPlaceHolder(imageView.context, 6))
.skipMemoryCache(true)
)
LOAD_TYPE_NO_RADIUS -> requestBuilder.apply(
RequestOptions()
.placeholder(getPlaceHolder(imageView.context, 0))
.skipMemoryCache(true)
)
LOAD_TYPE_ALWAYS_ROUND -> requestBuilder.apply(
RequestOptions()
.circleCrop()
.placeholder(getPlaceHolder(imageView.context, 100.dpToPx()))
.skipMemoryCache(true)
)
}
if (!noTransition) {
requestBuilder.transition(DrawableTransitionOptions.withCrossFade())
}
requestBuilder.into(imageView)
}
@JvmStatic
@JvmOverloads
fun load(
imageView: ImageView,
@LoadType type: Int,
url: String?,
skipNetworkCheck: Boolean = false
) {
load(imageView, type, url, skipNetworkCheck, false)
}
/**
* 获取要加载的图片 Url
*
* @param isSmallPic 加载的是否为缩略图
* @param originUrl 原图 Url
* @param smallPicUrls 缩略图 Url按照画质从好到差排序
* @return 要加载的图片 Url
*/
@JvmStatic
fun getUrl(
context: Context,
isSmallPic: Boolean,
originUrl: String,
vararg smallPicUrls: String?
): String {
val urls = mutableListOf(*smallPicUrls)
if (isSmallPic) {
if (needReverse(context)) {
urls.reverse()
}
for (url in urls) {
if (!url.isNullOrEmpty()) {
return url
}
}
}
return originUrl
}
private fun needReverse(context: Context): Boolean {
return if (imageLoadSettings == SETTINGS_SMART_ORIGIN &&
NetworkUtil.isWifiConnected(context)
) false
else imageLoadSettings != SETTINGS_ALL_ORIGIN
}
@get:ImageLoadSettings
private val imageLoadSettings: Int
get() = INSTANCE.appPreferences.imageLoadType!!.toInt()
fun imageToBase64(inputStream: InputStream?): String? {
if (inputStream == null) {
return null
}
return runCatching {
inputStream.use {
Base64.encodeToString(inputStream.readBytes(), Base64.DEFAULT)
}
}.getOrNull()
}
fun imageToBase64(file: File?): String? {
if (file == null) {
return null
}
var result: String? = null
try {
val `is`: InputStream = FileInputStream(file)
result = imageToBase64(`is`)
} catch (e: IOException) {
e.printStackTrace()
}
return result
}
interface ShareTaskCallback {
fun onGetUri(uri: Uri)
}
@IntDef(LOAD_TYPE_SMALL_PIC, LOAD_TYPE_AVATAR, LOAD_TYPE_NO_RADIUS, LOAD_TYPE_ALWAYS_ROUND)
@Retention(AnnotationRetention.SOURCE)
annotation class LoadType
@IntDef(SETTINGS_SMART_ORIGIN, SETTINGS_SMART_LOAD, SETTINGS_ALL_ORIGIN, SETTINGS_ALL_NO)
annotation class ImageLoadSettings
}
fun ImageUtil.download(
context: Context,
url: String?,
forShare: Boolean,
taskCallback: (Uri) -> Unit
) {
download(context, url, forShare, object : ImageUtil.ShareTaskCallback {
override fun onGetUri(uri: Uri) {
taskCallback(uri)
}
})
}