From 8a10dac03dfb1eb2cdf0dfe118dd0f04525ff7bd Mon Sep 17 00:00:00 2001 From: kehaoyuan Date: Fri, 3 Aug 2018 18:28:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8A=E4=BC=A0=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6(=E7=9B=AE=E5=89=8D=E5=8F=AA=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E4=BA=86:=E7=AD=94=E6=A1=88=E5=9B=BE=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0,=E9=97=AE=E9=A2=98=E5=9B=BE=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/fragment/WaitingDialogFragment.java | 8 +- .../com/gh/common/util/CompressImageUtils.kt | 132 ++++++++++++++++++ .../com/gh/common/util/UploadImageUtils.kt | 84 +++++------ .../qa/answer/edit/AnswerEditFragment.java | 11 ++ .../qa/questions/edit/QuestionEditActivity.kt | 28 ++-- .../questions/edit/QuestionEditViewModel.kt | 7 + .../gamecenter/retrofit/FileRequestBody.java | 82 ++++++----- .../gamecenter/retrofit/RetrofitCallback.java | 11 +- 8 files changed, 272 insertions(+), 91 deletions(-) create mode 100644 app/src/main/java/com/gh/common/util/CompressImageUtils.kt diff --git a/app/src/main/java/com/gh/base/fragment/WaitingDialogFragment.java b/app/src/main/java/com/gh/base/fragment/WaitingDialogFragment.java index c50e1ef8c9..afffab1808 100644 --- a/app/src/main/java/com/gh/base/fragment/WaitingDialogFragment.java +++ b/app/src/main/java/com/gh/base/fragment/WaitingDialogFragment.java @@ -21,6 +21,8 @@ public class WaitingDialogFragment extends BaseDialogFragment { private OnDialogBackListener mBackListener; + private TextView message; + public static WaitingDialogFragment newInstance(String message) { Bundle args = new Bundle(); args.putString(KEY_MSG, message); @@ -43,7 +45,7 @@ public class WaitingDialogFragment extends BaseDialogFragment { @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.set_wait_dialog, null); - final TextView message = (TextView) view.findViewById(R.id.set_wait_message); + message = (TextView) view.findViewById(R.id.set_wait_message); message.setText(getArguments().getString(KEY_MSG)); return view; } @@ -62,6 +64,10 @@ public class WaitingDialogFragment extends BaseDialogFragment { this.mBackListener = backListener; } + public void uploadWaitingHint(String hint) { + if (message != null) message.setText(hint); + } + @Override public boolean onBack() { if (mBackListener != null) { diff --git a/app/src/main/java/com/gh/common/util/CompressImageUtils.kt b/app/src/main/java/com/gh/common/util/CompressImageUtils.kt new file mode 100644 index 0000000000..dfb2eea574 --- /dev/null +++ b/app/src/main/java/com/gh/common/util/CompressImageUtils.kt @@ -0,0 +1,132 @@ +package com.gh.common.util + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import com.halo.assistant.HaloApp +import java.io.File +import java.io.FileOutputStream + +/** + * Created by khy on 02/08/18. + * 图片压缩工具类 + * 资料参考:https://github.com/zetbaitsu/Compressor + * 压缩算法:http://gitlab.ghzhushou.com/pm/halo-app-issues/issues/298 + */ +object CompressImageUtils { + + private const val compressLimit = 1280 + private const val defaultQuality = 90 + + /** + * 压缩图片并保存到目标文件 + * 该压缩方法是同步执行 请勿在主线程执行 + * 返回源文件的三种情况:小于特定值,图片类型为GIF,压缩失败 + */ + @Throws(Exception::class) + fun compressImageAndSaveToFile(imageFile: File, compressGif: Boolean): File { + // 小于300K直接返回原图 + if (imageFile.length() < 50 * 1024) { + return imageFile + } + + val cacheDir = getImageCacheDir() + val parentFile = cacheDir.parentFile + if (!parentFile.exists()) parentFile.mkdirs() + var fileOutputStream: FileOutputStream? = null + + try { + // 确定图片类型 + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(imageFile.absolutePath, options) + val formatType = if (options.outMimeType.contains("png")) { + Bitmap.CompressFormat.PNG + } else if (options.outMimeType.contains("gif") && !compressGif) { // gif直接返回原图 + return imageFile + } else { + Bitmap.CompressFormat.WEBP + } + + fileOutputStream = FileOutputStream(cacheDir) + // write the compressed bitmap at the destination specified by destinationPath. + decodeSampledBitmapFromFile(imageFile).compress(formatType, defaultQuality, fileOutputStream) + return cacheDir + } catch (e: Exception) { + e.printStackTrace() + if (cacheDir.exists()) { + cacheDir.delete() + } + } finally { + if (fileOutputStream != null) { + fileOutputStream.flush() + fileOutputStream.close() + } + } + return imageFile + } + + private fun getImageCacheDir(): File { +// return File(Environment.getExternalStorageDirectory().absolutePath + "/Pictures/test/" + System.currentTimeMillis() + ".jpg") + // 统一用jpg保存应该没有影响吧 + return File(HaloApp.getInstance().application.cacheDir.absolutePath + File.separator + System.currentTimeMillis() + ".jpg") + } + + // 根据图片获取压缩后的位图 + @Throws(Exception::class) + private fun decodeSampledBitmapFromFile(imageFile: File): Bitmap { + // First decode with inJustDecodeBounds=true to check dimensions + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(imageFile.absolutePath, options) + + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + /** + * 压缩类型:用于矩阵缩放 + * 0: 短边等比压缩至1280 H:长边 W:短边 + * 1: 取长边等比压缩至1280 H:短边 W: 长边 + */ + var compressType = -1 + + val longSide = Math.max(height, width) //最长边 + val shortSide = Math.min(height, width) //最短边 + val scale = longSide.toFloat() / shortSide // 长短边比例 + + if (longSide > compressLimit && shortSide > compressLimit) { + if (scale > 2) { + inSampleSize = if (shortSide / compressLimit == 0) 1 else shortSide / compressLimit + compressType = 0 + } else { + inSampleSize = if (longSide / compressLimit == 0) 1 else longSide / compressLimit + compressType = 1 + + } + } else if (longSide > compressLimit && shortSide < compressLimit) { + if (scale <= 2) { + inSampleSize = if (longSide / compressLimit == 0) 1 else longSide / compressLimit + compressType = 1 + } + } + + // Calculate inSampleSize + options.inSampleSize = inSampleSize + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false + + var scaledBitmap = BitmapFactory.decodeFile(imageFile.absolutePath, options) + + val matrix = Matrix() // 精确缩放 + if (compressType != -1) { + val targetMatrixScale = if (compressType == 0) compressLimit.toFloat() / scaledBitmap.width + else compressLimit.toFloat() / scaledBitmap.height + matrix.setScale(targetMatrixScale, targetMatrixScale) + scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.width, scaledBitmap.height, matrix, true) + } + return scaledBitmap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/common/util/UploadImageUtils.kt b/app/src/main/java/com/gh/common/util/UploadImageUtils.kt index 164b71231b..719c2158b0 100644 --- a/app/src/main/java/com/gh/common/util/UploadImageUtils.kt +++ b/app/src/main/java/com/gh/common/util/UploadImageUtils.kt @@ -1,7 +1,8 @@ package com.gh.common.util -import android.text.TextUtils import com.gh.gamecenter.retrofit.BiResponse +import com.gh.gamecenter.retrofit.FileRequestBody +import com.gh.gamecenter.retrofit.RetrofitCallback import com.gh.gamecenter.retrofit.RetrofitManager import com.halo.assistant.HaloApp import io.reactivex.Observable @@ -16,9 +17,9 @@ import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.ResponseBody import org.json.JSONObject -import top.zibin.luban.Luban import java.io.File import java.util.* +import kotlin.collections.ArrayList import kotlin.collections.HashMap object UploadImageUtils { @@ -28,7 +29,7 @@ object UploadImageUtils { suggestion } - private val boundary = UUID.randomUUID().toString().replace("-".toRegex(), "").substring(0, 16) + val boundary = UUID.randomUUID().toString().replace("-".toRegex(), "").substring(0, 16) // 社区图片上传 后续统一其他 fun compressAndUploadImage(type: UploadType, imgPath: String, compressGif: Boolean, listener: OnUploadImageListener): Disposable { @@ -36,11 +37,14 @@ object UploadImageUtils { return Single.just(imgPath) .subscribeOn(Schedulers.computation()) .flatMap { - var compressPath = compressImage(it, compressGif) // compress - if (compressPath.isNullOrEmpty()) compressPath = it - val file = File(compressPath) - val requestFile = RequestBody.create(MediaType.parse("multipart/form-data;boundary=$boundary"), file) - val part = MultipartBody.Part.createFormData("Filedata", file.name, requestFile) + val compressFile = compressImage(it, compressGif) // compress + val requestBody = FileRequestBody(compressFile, object : RetrofitCallback() { + override fun onProgress(total: Long, progress: Long) { + listener.onProgress(total, progress) + } + }) + val part = MultipartBody.Part.createFormData("Filedata", compressFile.name, requestBody) + return@flatMap RetrofitManager.getInstance(HaloApp.getInstance().application).uploadApi.uploadImage(part, type.name) } .subscribeOn(Schedulers.io()) @@ -65,40 +69,34 @@ object UploadImageUtils { } fun compressAndUploadImageList(type: UploadType, imgs: List, compressGif: Boolean, listener: OnUploadImageListListener) { - val postImageList = HashMap() Observable.create(ObservableOnSubscribe> { val compressList = compressImageList(imgs, compressGif) - - if (compressList != null) { - for (img in compressList) { - val requestFile = RequestBody.create(MediaType.parse("multipart/form-data;boundary=$boundary"), img) - val part = MultipartBody.Part.createFormData("Filedata", img.name, requestFile) - RetrofitManager.getInstance(HaloApp.getInstance().application) - .uploadApi.uploadImage(part, type.name) - .subscribe(object : BiResponse() { - override fun onSuccess(data: ResponseBody) { - val string = data.string() - if (!string.isNullOrEmpty()) { - val url = JSONObject(string).getString("url") - if (!url.isNullOrEmpty()) { - val map = HashMap() - map[img.path] = url - it.onNext(map) - return - } + for (img in compressList) { + val requestFile = RequestBody.create(MediaType.parse("multipart/form-data;boundary=$boundary"), img) + val part = MultipartBody.Part.createFormData("Filedata", img.name, requestFile) + RetrofitManager.getInstance(HaloApp.getInstance().application) + .uploadApi.uploadImage(part, type.name) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + val string = data.string() + if (!string.isNullOrEmpty()) { + val url = JSONObject(string).getString("url") + if (!url.isNullOrEmpty()) { + val map = HashMap() + map[img.path] = url + it.onNext(map) + return } - onFailure(IllegalAccessException("HeHe")) } + onFailure(IllegalAccessException("HeHe")) + } - override fun onFailure(exception: Exception) { - it.onError(exception) - } - }) - } - - + override fun onFailure(exception: Exception) { + it.onError(exception) + } + }) } it.onComplete() }) @@ -126,21 +124,23 @@ object UploadImageUtils { } // 同步调用->避免在主线程调用以免阻塞主线程 - private fun compressImageList(imgs: List, compressGif: Boolean): List? { - return Luban.with(HaloApp.getInstance().application).load(imgs).filter { path -> - !(TextUtils.isEmpty(path) || (path.toLowerCase().endsWith(".gif") && !compressGif)) - }.ignoreBy(200).get() + private fun compressImageList(imgs: List, compressGif: Boolean): List { + val compressList: MutableList = ArrayList() + for (img in imgs) { + compressList.add(CompressImageUtils.compressImageAndSaveToFile(File(img), compressGif)) + } + return compressList } // 同步调用->避免在主线程调用以免阻塞主线程 - private fun compressImage(imgPath: String, compressGif: Boolean): String? { - if (TextUtils.isEmpty(imgPath) || (imgPath.toLowerCase().endsWith(".gif") && !compressGif)) return imgPath - return Luban.with(HaloApp.getInstance().application).ignoreBy(200).get(imgPath).path + private fun compressImage(imgPath: String, compressGif: Boolean): File { + return CompressImageUtils.compressImageAndSaveToFile(File(imgPath), compressGif) } interface OnUploadImageListener { fun onSuccess(imageUrl: String) fun onError(e: Throwable?) + fun onProgress(total: Long, progress: Long) } interface OnUploadImageListListener { diff --git a/app/src/main/java/com/gh/gamecenter/qa/answer/edit/AnswerEditFragment.java b/app/src/main/java/com/gh/gamecenter/qa/answer/edit/AnswerEditFragment.java index 70c86953d5..cea79d7c56 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/answer/edit/AnswerEditFragment.java +++ b/app/src/main/java/com/gh/gamecenter/qa/answer/edit/AnswerEditFragment.java @@ -43,6 +43,7 @@ import org.json.JSONObject; import java.io.IOException; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import butterknife.BindView; @@ -221,6 +222,16 @@ public class AnswerEditFragment extends NormalFragment { mUploadImageDisposable = UploadImageUtils.INSTANCE.compressAndUploadImage(UploadImageUtils.UploadType.community , picturePath, false, new UploadImageUtils.OnUploadImageListener() { + @Override + public void onProgress(long total, long progress) { + float percent = 100 * (progress / (float) total); + if (percent >= 100) percent = 99.9F; + String format = String.format(Locale.CHINA, "%.1f", percent); + if (postDialog != null) { + postDialog.uploadWaitingHint("图片上传中" + format + "%"); + } + } + @Override public void onSuccess(@NotNull String imageUrl) { if (postDialog != null) postDialog.dismissAllowingStateLoss(); diff --git a/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditActivity.kt b/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditActivity.kt index 1e8f285355..7de1fc3136 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditActivity.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditActivity.kt @@ -128,18 +128,22 @@ class QuestionEditActivity : BaseActivity() { // Process dialog mViewModel?.processDialog?.observe(this, Observer { it -> if (it?.isShow!!) { - mProcessingDialog = WaitingDialogFragment.newInstance(it.msg, false) - mProcessingDialog?.show(supportFragmentManager, null, { - if (mViewModel?.uploadImageSubscription != null && !mViewModel?.uploadImageSubscription!!.isDisposed) { - mUpdateImageCancelDialog = DialogUtils.showAlertDialog(this, "提示" - , "图片正在上传中,确定取消吗?" - , "确定", "取消", { - mViewModel?.uploadImageSubscription!!.dispose() - mUpdateImageCancelDialog?.dismiss() - mProcessingDialog?.dismiss() - }, null) - } - }) + if (mProcessingDialog != null && mProcessingDialog?.isVisible!!) { + mProcessingDialog?.uploadWaitingHint(it.msg) + } else { + mProcessingDialog = WaitingDialogFragment.newInstance(it.msg, false) + mProcessingDialog?.show(supportFragmentManager, null, { + if (mViewModel?.uploadImageSubscription != null && !mViewModel?.uploadImageSubscription!!.isDisposed) { + mUpdateImageCancelDialog = DialogUtils.showAlertDialog(this, "提示" + , "图片正在上传中,确定取消吗?" + , "确定", "取消", { + mViewModel?.uploadImageSubscription!!.dispose() + mUpdateImageCancelDialog?.dismiss() + mProcessingDialog?.dismiss() + }, null) + } + }) + } } else { mUpdateImageCancelDialog?.dismiss() mProcessingDialog?.dismiss() diff --git a/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditViewModel.kt b/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditViewModel.kt index d5d58829d3..224251e67d 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/questions/edit/QuestionEditViewModel.kt @@ -186,6 +186,13 @@ class QuestionEditViewModel(application: Application) : AndroidViewModel(applica processDialog.postValue(WaitingDialogFragment.WaitingDialogData("上传中...", true)) uploadImageSubscription = UploadImageUtils.compressAndUploadImage(UploadImageUtils.UploadType.community , picPath, true, object : UploadImageUtils.OnUploadImageListener { + override fun onProgress(total: Long, progress: Long) { + var percent = 100 * (progress / total.toFloat()) + if (percent >= 100) percent = 99.9F + val format = String.format("%.1f", percent) + processDialog.postValue(WaitingDialogFragment.WaitingDialogData("图片上传中$format%", true)) + } + override fun onSuccess(imageUrl: String) { processDialog.postValue(WaitingDialogFragment.WaitingDialogData("上传中...", false)) val list = picList.value ?: ArrayList() diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/FileRequestBody.java b/app/src/main/java/com/gh/gamecenter/retrofit/FileRequestBody.java index 17fdea0fdf..0cf2021c8a 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/FileRequestBody.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/FileRequestBody.java @@ -1,65 +1,79 @@ package com.gh.gamecenter.retrofit; +import android.os.Handler; +import android.os.Looper; + +import com.gh.common.util.UploadImageUtils; + +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; -import okio.Buffer; import okio.BufferedSink; -import okio.ForwardingSink; -import okio.Okio; -import okio.Sink; public class FileRequestBody extends RequestBody { - private RequestBody requestBody; - private RetrofitCallback callback; - private BufferedSink bufferedSink; + private int DEFAULT_BUFFER_SIZE = 1024 * 8; + + private File mFile; - public FileRequestBody(RequestBody requestBody, RetrofitCallback callback) { + public FileRequestBody(File file, RetrofitCallback callback) { super(); - this.requestBody = requestBody; + this.mFile = file; this.callback = callback; } + @Override + public long contentLength() throws IOException { + return mFile.length(); + } @Override public MediaType contentType() { - return requestBody.contentType(); + return MediaType.parse("multipart/form-data;boundary=" + UploadImageUtils.INSTANCE.getBoundary()); } + // 注意 addNetworkInterceptor ->HttpLoggingInterceptor 会调用两遍 @Override public void writeTo(BufferedSink sink) throws IOException { - bufferedSink = Okio.buffer(sink(sink)); + long fileLength = mFile.length(); + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + FileInputStream in = new FileInputStream(mFile); + long uploaded = 0; - //写入 - requestBody.writeTo(bufferedSink); - //必须调用flush,否则最后一部分数据可能不会被写入 - bufferedSink.flush(); + try { + int read; + Handler handler = new Handler(Looper.getMainLooper()); + while ((read = in.read(buffer)) != -1) { + uploaded += read; + sink.write(buffer, 0, read); + // update progress on UI thread + handler.post(new ProgressUpdater(uploaded, fileLength)); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + in.close(); + } } - private Sink sink(Sink sink) { - return new ForwardingSink(sink) { - //当前写入字节数 - long bytesWritten = 0L; - //总字节长度,避免多次调用contentLength()方法 - long contentLength = 0L; + private class ProgressUpdater implements Runnable { + private long mUploaded; + private long mTotal; - @Override - public void write(Buffer source, long byteCount) throws IOException { - super.write(source, byteCount); - if (contentLength == 0) { - //获得contentLength的值,后续不再调用 - contentLength = contentLength(); - } - //增加当前写入的字节数 - bytesWritten += byteCount; - //回调 - callback.onLoading(contentLength, bytesWritten); - } - }; + public ProgressUpdater(long uploaded, long total) { + mUploaded = uploaded; + mTotal = total; + } + + @Override + public void run() { + callback.onProgress(mTotal, mUploaded); + } } } diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitCallback.java b/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitCallback.java index 3c40c45bc7..e25bc925f6 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitCallback.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitCallback.java @@ -15,8 +15,15 @@ public abstract class RetrofitCallback implements Callback { } } - public abstract void onSuccess(Call call, Response response); + public void onSuccess(Call call, Response response) { + + } + + @Override + public void onFailure(Call call, Throwable t) { + + } //用于进度的回调 - public abstract void onLoading(long total, long progress); + public abstract void onProgress(long total, long progress); } \ No newline at end of file