增加上传进度(目前只对接了:答案图片上传,问题图片上传)

This commit is contained in:
kehaoyuan
2018-08-03 18:28:32 +08:00
parent de71ebad52
commit 8a10dac03d
8 changed files with 272 additions and 91 deletions

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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<ResponseBody>(compressFile, object : RetrofitCallback<ResponseBody>() {
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<String>, compressGif: Boolean, listener: OnUploadImageListListener) {
val postImageList = HashMap<String, String>()
Observable.create(ObservableOnSubscribe<Map<String, String>> {
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<ResponseBody>() {
override fun onSuccess(data: ResponseBody) {
val string = data.string()
if (!string.isNullOrEmpty()) {
val url = JSONObject(string).getString("url")
if (!url.isNullOrEmpty()) {
val map = HashMap<String, String>()
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<ResponseBody>() {
override fun onSuccess(data: ResponseBody) {
val string = data.string()
if (!string.isNullOrEmpty()) {
val url = JSONObject(string).getString("url")
if (!url.isNullOrEmpty()) {
val map = HashMap<String, String>()
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<String>, compressGif: Boolean): List<File>? {
return Luban.with(HaloApp.getInstance().application).load<Any>(imgs).filter { path ->
!(TextUtils.isEmpty(path) || (path.toLowerCase().endsWith(".gif") && !compressGif))
}.ignoreBy(200).get()
private fun compressImageList(imgs: List<String>, compressGif: Boolean): List<File> {
val compressList: MutableList<File> = 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 {

View File

@ -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();

View File

@ -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()

View File

@ -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()

View File

@ -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<T> extends RequestBody {
private RequestBody requestBody;
private RetrofitCallback<T> callback;
private BufferedSink bufferedSink;
private int DEFAULT_BUFFER_SIZE = 1024 * 8;
private File mFile;
public FileRequestBody(RequestBody requestBody, RetrofitCallback<T> callback) {
public FileRequestBody(File file, RetrofitCallback<T> 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);
}
}
}

View File

@ -15,8 +15,15 @@ public abstract class RetrofitCallback<T> implements Callback<T> {
}
}
public abstract void onSuccess(Call<T> call, Response<T> response);
public void onSuccess(Call<T> call, Response<T> response) {
}
@Override
public void onFailure(Call<T> call, Throwable t) {
}
//用于进度的回调
public abstract void onLoading(long total, long progress);
public abstract void onProgress(long total, long progress);
}