我记得在2017年写项目的时候图片压缩工具就是用的Luban,时隔多年后在近期的俩个项目中依旧发现了Luban的身影,所以觉得有必要好好记录一下老朋友
想学习一款新技术最好的方式就是去看 官方文档(Github - Luban) ,因为除了会介绍基本的使用之外,如果你有兴趣的话也可以学一下源码;而我记录Blog更多的是我的一个学习、反思的过程,希望也可以帮到你
- 基本了解
- 压缩效果
- 压缩算法
- 方法列表
- 基础使用
- 异步调用
- 同步调用
- 项目实战
基本了解
Android市场 相比 Ios市场 更具有开放性、广泛性;在国内也有着众多厂商,同时意味着也有更多的手机品牌,这就面临着每款手机的 拍照分辨率,屏幕显示分辨率
可能有所不同,故而就会出现一些小问题;
随着手机拍照分辨率
的提升,图片压缩就是一个很重要的问题;如果只是单纯对图片进行裁切,压缩可能很难满足诉求
(关于原始压缩方式和原理,等后续有时间补一篇),这是因为没有一个标准
,如果裁切过头图片太小,质量压缩过头则显示效果太差
。
个人见解:所谓的标准,可以说是策略,指的是不同场景下采用不同的方式
(如果不理解,可以看看 策略模式,开放下思维)
在一些项目实战场景,可以看出图片压缩的重要性
,例如~
-
加载大图
时浪费太多流量,同时还可能发生OOM
(现在) -
上传大图
时浪费太多流量,同时还可能出现上传失败的结果
压缩效果
于是这位前辈就想到App巨头“微信”会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法
。
同一张图片经过 Luban压缩
和 WeChat压缩
de 效果与对比
(压缩效果很好,所以沿用到了现在)
压缩算法
作者考虑到其他语言也想要实现Luban,所以描述了一遍
算法步骤
第三挡压缩(参考最新版微信压缩效果
)
作者在2018、2019年后就没再维护了,有俩种可能一种就是Luban已经很稳定了,另一种就是开启人生新篇章了吧
1.判断图片比例值
,是否处于以下区间内;
- [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
- [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
- [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
2.判断图片最长边是否过边界值
;
- [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
- [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
- [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
3.计算压缩图片实际边长值
,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)
4.计算压缩图片的实际文件大小
,以第2、3步结果为准,图片比例越大则文件越大。
size = (newW * newH) / (width * height) * m;
- [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
- [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
- [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值
5.判断第4步的size是否过小
- [1, 0.5625) 则最小 size 对应 60,60,100
- [0.5625, 0.5) 则最小 size 都为 100
- [0.5, 0) 则最小 size 都为 100
6.将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值
方法列表
方法 | 描述 |
load | 传入原图 |
filter | 设置开启压缩条件 |
ignoreBy | 不压缩的阈值,单位为K |
setFocusAlpha | 设置是否保留透明通道 |
setTargetDir | 缓存压缩图片路径 |
setCompressListener | 压缩回调接口 |
setRenameListener | 压缩前重命名接口 |
基础使用
以下使用方式在 Luban 都可以看到,是
作者提供的一些调用示例
,方便大家快速使用
build.gradle
implementation 'top.zibin:Luban:1.1.8'
异步调用
Luban内部采用IO线程进行图片压缩
,外部调用只需设置好结果监听即可:
Luban.with(this)
.load(photos)
.ignoreBy(100)
.setTargetDir(getPath())
.filter(new CompressionPredicate() {
@Override
public boolean apply(String path) {
return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
}
})
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch();
同步调用
同步方法请尽量避免在主线程调用以免阻塞主线程
,下面以rxJava
调用为例(应该也支持用协程写,只要将压缩操作放到子线程即可
)
Flowable.just(photos)
.observeOn(Schedulers.io())
.map(new Function<List<String>, List<File>>() {
@Override public List<File> apply(@NonNull List<String> list) throws Exception {
// 同步方法直接返回压缩后的文件
return Luban.with(MainActivity.this).load(list).get();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
项目实战
以下均为我在项目中使用Luban的一些伪代码,可供借鉴
ImageCompressor
package com.jsmedia.jsmanager.utils.imgUtils
import android.content.Context
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
import java.io.File
object ImageCompressor {
private const val IGNORE_SIZE = 1
fun compress(context: Context, imageFile: File, compressResult: CompressResult) {
Luban.with(context)
.load(imageFile)
.ignoreBy(IGNORE_SIZE)
.setCompressListener(object : OnCompressListener {
override fun onStart() {
}
override fun onSuccess(file: File) {
compressResult.onSuccess(file)
}
override fun onError(e: Throwable) {
compressResult.onError(e)
}
}).launch()
}
interface CompressResult {
fun onSuccess(file: File)
fun onError(e: Throwable)
}
}
调用方式
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
}
override fun onError(e: Throwable) {
}
})
项目 - 伪代码
/**
* 拍照
* */
fun onCamera(view: View?) {
ImagePicker.getInstance().startCamera(this, false, object : PickCallback() {
override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
ToastUtils.showShort(message)
}
override fun cropConfig(builder: ActivityBuilder) {
builder.setMultiTouchEnabled(true)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setCropShape(CropImageView.CropShape.OVAL)
.setRequestedSize(400, 400)
.setFixAspectRatio(true)
.setAspectRatio(1, 1)
}
override fun onPickImage(imageUri: Uri?) {
super.onPickImage(imageUri)
Log.e("tag", imageUri.toString())
var bitmap = BitmapFactory.decodeStream(imageUri?.let {
contentResolver.openInputStream(
it
)
});
var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
mViewModel.upload(
2,
"album",
file.absolutePath,
MMKV.defaultMMKV().decodeString("storeId").toString()
)
}
override fun onError(e: Throwable) {
}
})
}
override fun onCropImage(imageUri: Uri?) {
}
})
}
/**
* 相册
* */
fun onGallery(view: View?) {
ImagePicker.getInstance().startGallery(this, false, object : PickCallback() {
override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
ToastUtils.showShort(message)
}
override fun onPickImage(imageUri: Uri?) {
Log.e("tag", imageUri.toString())
var bitmap = BitmapFactory.decodeStream(imageUri?.let {
contentResolver.openInputStream(
it
)
});
var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
mViewModel.upload(
2,
"album",
file.absolutePath,
MMKV.defaultMMKV().decodeString("storeId").toString()
)
}
override fun onError(e: Throwable) {
}
})
}
})
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
ImagePicker.getInstance()
.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
ImagePicker.getInstance().onActivityResult(this, requestCode, resultCode, data)
}