488 lines
19 KiB
Kotlin
488 lines
19 KiB
Kotlin
package com.gh.common.util
|
||
|
||
import android.annotation.SuppressLint
|
||
import android.content.Context
|
||
import android.content.res.Resources
|
||
import android.graphics.Bitmap
|
||
import android.graphics.drawable.Animatable
|
||
import android.graphics.drawable.ColorDrawable
|
||
import android.net.Uri
|
||
import android.os.Build
|
||
import androidx.annotation.DrawableRes
|
||
import androidx.core.content.ContextCompat
|
||
import com.facebook.common.executors.CallerThreadExecutor
|
||
import com.facebook.common.references.CloseableReference
|
||
import com.facebook.datasource.DataSource
|
||
import com.facebook.drawee.backends.pipeline.Fresco
|
||
import com.facebook.drawee.controller.BaseControllerListener
|
||
import com.facebook.drawee.controller.ControllerListener
|
||
import com.facebook.drawee.drawable.ScalingUtils
|
||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||
import com.facebook.drawee.view.SimpleDraweeView
|
||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
|
||
import com.facebook.imagepipeline.image.CloseableImage
|
||
import com.facebook.imagepipeline.image.ImageInfo
|
||
import com.facebook.imagepipeline.request.ImageRequest
|
||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||
import com.facebook.imagepipeline.request.Postprocessor
|
||
import com.gh.common.constant.Config
|
||
import com.gh.common.structure.FixedSizeLinkedHashSet
|
||
import com.gh.gamecenter.R
|
||
import com.halo.assistant.HaloApp
|
||
import com.squareup.picasso.Picasso
|
||
import io.reactivex.Single
|
||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||
import io.reactivex.schedulers.Schedulers
|
||
import java.io.ByteArrayOutputStream
|
||
|
||
object ImageUtils {
|
||
|
||
private const val PIC_MAX_FILE_SIZE: Long = 10 * 1024 * 1024
|
||
|
||
private val TINY_GIF_SIZE = 30F.dip2px()
|
||
private val LARGE_GIF_SIZE = 80F.dip2px()
|
||
private val STANDARD_GIF_SIZE = 60F.dip2px()
|
||
|
||
const val TARGET_WIDTH = R.dimen.width_placeholder
|
||
|
||
private val mImageUrlCacheSet by lazy { FixedSizeLinkedHashSet<String>(maxSize = 200) }
|
||
|
||
@JvmStatic
|
||
fun getUploadFileMaxSize(): Long {
|
||
val uploadLimitSize = Config.getSettings()?.image?.uploadLimitSize
|
||
if (uploadLimitSize != null) {
|
||
return uploadLimitSize
|
||
}
|
||
return PIC_MAX_FILE_SIZE
|
||
}
|
||
|
||
@JvmStatic
|
||
fun getDefaultGifRule(): String? {
|
||
val gifConfig = Config.getSettings()?.image?.oss?.gif
|
||
if (gifConfig != null) {
|
||
return gifConfig
|
||
}
|
||
return ""
|
||
}
|
||
|
||
@JvmStatic
|
||
fun getWatermarkWidthGifRule(width: Int?): String? {
|
||
val gifConfig = Config.getSettings()?.image?.oss?.gitThumb
|
||
val gifWaterMark = Config.getSettings()?.image?.oss?.gifWaterMark
|
||
if (gifConfig != null && gifWaterMark != null) {
|
||
return "$gifConfig,w_$width$gifWaterMark"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
|
||
@JvmStatic
|
||
fun getLimitWidthRule(width: Int?): String? {
|
||
val jpegConfig = Config.getSettings()?.image?.oss?.jpeg
|
||
if (jpegConfig != null) {
|
||
return "$jpegConfig,w_$width"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
@JvmStatic
|
||
fun addLimitWidth(imageUrl: String?, width: Int?): String? {
|
||
val jpegConfig = Config.getSettings()?.image?.oss?.jpeg
|
||
val webpConfig = Config.getSettings()?.image?.oss?.webp
|
||
?: Config.getSettings()?.image?.oss?.gif
|
||
if (jpegConfig != null) {
|
||
return if (width == 0 || width == null) {
|
||
"$imageUrl$webpConfig"
|
||
} else {
|
||
"$imageUrl$jpegConfig,w_$width"
|
||
}
|
||
}
|
||
return imageUrl
|
||
}
|
||
|
||
@JvmStatic
|
||
fun addLimitHeight(imageUrl: String, height: Int): String {
|
||
val jpegConfig = Config.getSettings()?.image?.oss?.jpeg
|
||
if (jpegConfig != null) {
|
||
return "$imageUrl$jpegConfig,h_$height"
|
||
}
|
||
return imageUrl
|
||
}
|
||
|
||
@JvmStatic
|
||
fun addLimitWidthAndHeight(imageUrl: String, width: Int, height: Int): String {
|
||
val jpegConfig = Config.getSettings()?.image?.oss?.jpeg
|
||
if (jpegConfig != null) {
|
||
return "$imageUrl$jpegConfig,w_$width,h_$height"
|
||
}
|
||
return imageUrl
|
||
}
|
||
|
||
@JvmStatic
|
||
fun getGitStaticImage(imageUrl: String): String {
|
||
val gifThumb = Config.getSettings()?.image?.oss?.gitThumb
|
||
if (gifThumb != null) {
|
||
return "$imageUrl$gifThumb"
|
||
}
|
||
return imageUrl
|
||
}
|
||
|
||
|
||
@JvmStatic
|
||
fun addLimitWidthAndLoad(draweeView: SimpleDraweeView?, imageUrl: String, width: Int) {
|
||
val newUrl = addLimitWidth(imageUrl, width)
|
||
draweeView?.setImageURI(newUrl)
|
||
}
|
||
|
||
@JvmStatic
|
||
fun addLimitWidthAndLoad(draweeView: SimpleDraweeView?, imageUrl: String?, width: Int?, onLoadListener: OnImageloadListener?) {
|
||
val newUrl = getTransformLimitUrl(imageUrl, width, draweeView?.context)
|
||
val listener = object : BaseControllerListener<ImageInfo>() {
|
||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||
onLoadListener?.onLoadFinal(imageInfo)
|
||
}
|
||
}
|
||
|
||
draweeView?.controller = Fresco.newDraweeControllerBuilder()
|
||
.setUri(newUrl)
|
||
.setControllerListener(listener)
|
||
.build()
|
||
}
|
||
|
||
|
||
fun display(simpleDraweeView: SimpleDraweeView?, url: String?, width: Int?, listener: BaseControllerListener<ImageInfo>) {
|
||
simpleDraweeView?.controller = Fresco.newDraweeControllerBuilder()
|
||
.setUri(getTransformLimitUrl(url, width, simpleDraweeView?.context))
|
||
.setControllerListener(listener)
|
||
.build()
|
||
}
|
||
|
||
// 自适应图片宽高
|
||
@JvmStatic
|
||
fun display(simpleDraweeView: SimpleDraweeView?, url: String?, width: Int) {
|
||
val listener = object : BaseControllerListener<ImageInfo>() {
|
||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||
if (imageInfo == null) {
|
||
return
|
||
}
|
||
val layoutParams = simpleDraweeView?.layoutParams
|
||
val scale = imageInfo.height.toFloat() / imageInfo.width.toFloat()
|
||
layoutParams?.height = (width * scale).toInt()
|
||
simpleDraweeView?.layoutParams = layoutParams
|
||
}
|
||
}
|
||
simpleDraweeView?.controller = Fresco.newDraweeControllerBuilder()
|
||
.setControllerListener(listener)
|
||
.setUri(getTransformLimitUrl(url, width, simpleDraweeView?.context))
|
||
.build()
|
||
}
|
||
|
||
// 自适应图片宽高
|
||
@JvmStatic
|
||
fun displayScale(simpleDraweeView: SimpleDraweeView?, url: String?, height: Int) {
|
||
val listener = object : BaseControllerListener<ImageInfo>() {
|
||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||
|
||
if (imageInfo == null) {
|
||
return
|
||
}
|
||
val layoutParams = simpleDraweeView?.layoutParams
|
||
val scale = imageInfo.width.toFloat() / imageInfo.height.toFloat()
|
||
layoutParams?.width = (height * scale).toInt()
|
||
simpleDraweeView?.layoutParams = layoutParams
|
||
}
|
||
}
|
||
simpleDraweeView?.controller = Fresco.newDraweeControllerBuilder()
|
||
.setUri(url)
|
||
.setControllerListener(listener)
|
||
.build()
|
||
}
|
||
|
||
// 设置缩放类型,设置按压状态下的叠加图
|
||
@JvmStatic
|
||
fun display(resources: Resources?, simpleDraweeView: SimpleDraweeView?, width: Int,
|
||
scaleType: ScalingUtils.ScaleType?, url: String?) {
|
||
if (simpleDraweeView == null) return
|
||
val context = simpleDraweeView.context ?: return
|
||
simpleDraweeView.hierarchy = GenericDraweeHierarchyBuilder(resources)
|
||
.setFadeDuration(500)
|
||
.setPressedStateOverlay(ColorDrawable(ContextCompat.getColor(context, R.color.pressed_bg)))
|
||
.setPlaceholderImage(R.drawable.occupy2, ScalingUtils.ScaleType.CENTER)
|
||
.setBackground(ColorDrawable(ContextCompat.getColor(context, R.color.placeholder_bg)))
|
||
.setActualImageScaleType(scaleType)
|
||
.build()
|
||
simpleDraweeView.setImageURI(getTransformLimitUrl(url, width, context))
|
||
}
|
||
|
||
// 设置占位符
|
||
@JvmStatic
|
||
fun display(resources: Resources?, simpleDraweeView: SimpleDraweeView?, url: String?, placeholderImage: Int) {
|
||
if (simpleDraweeView == null) return
|
||
val context = simpleDraweeView.context ?: return
|
||
simpleDraweeView.hierarchy = GenericDraweeHierarchyBuilder(resources)
|
||
.setFadeDuration(500)
|
||
.setPressedStateOverlay(ColorDrawable(ContextCompat.getColor(context, R.color.pressed_bg)))
|
||
.setBackground(ColorDrawable(ContextCompat.getColor(context, R.color.placeholder_bg)))
|
||
.setPlaceholderImage(placeholderImage)
|
||
.build()
|
||
display(simpleDraweeView, url)
|
||
}
|
||
|
||
// 图片下载监听和设置低高分辨率图片
|
||
fun display(simpleDraweeView: SimpleDraweeView?, url: String?, lowUrl: String?,
|
||
listener: ControllerListener<in ImageInfo>) {
|
||
simpleDraweeView?.controller = Fresco.newDraweeControllerBuilder()
|
||
.setImageRequest(ImageRequest.fromUri(url))
|
||
.setControllerListener(listener)
|
||
.setLowResImageRequest(ImageRequest.fromUri(lowUrl)) // 低分辨率图片
|
||
.build()
|
||
}
|
||
|
||
/**
|
||
* 获取bitmap (使用 fresco 获取 gif 的 bitmap 会为空,https://github.com/facebook/fresco/issues/241)
|
||
* 所以当 url 以 .gif 结尾时换用 picasso 获取
|
||
* 优先使用 fresco 是因为可能已经有了缓存
|
||
*/
|
||
@SuppressLint("CheckResult")
|
||
@JvmStatic
|
||
fun getBitmap(url: String, callback: BiCallback<Bitmap, Boolean>) {
|
||
if (url.endsWith(".gif")) {
|
||
getBitmapWithPicasso(url, callback)
|
||
} else {
|
||
getBitmapWithFresco(url, callback)
|
||
}
|
||
}
|
||
|
||
private fun getBitmapWithFresco(url: String, callback: BiCallback<Bitmap, Boolean>) {
|
||
val imageRequest = ImageRequestBuilder
|
||
.newBuilderWithSource(Uri.parse(url))
|
||
.build()
|
||
Fresco.getImagePipeline()
|
||
.fetchDecodedImage(imageRequest, HaloApp.getInstance().application)
|
||
.subscribe(object : BaseBitmapDataSubscriber() {
|
||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
|
||
callback.onSecond(true)
|
||
}
|
||
|
||
override fun onNewResultImpl(bitmap: Bitmap?) {
|
||
if (bitmap != null) {
|
||
callback.onFirst(bitmap)
|
||
} else {
|
||
callback.onSecond(true)
|
||
}
|
||
}
|
||
}, CallerThreadExecutor.getInstance())
|
||
}
|
||
|
||
@SuppressLint("CheckResult")
|
||
private fun getBitmapWithPicasso(url: String, callback: BiCallback<Bitmap, Boolean>) {
|
||
Single.just(url)
|
||
.map { Picasso.with(HaloApp.getInstance().application).load(url).priority(Picasso.Priority.HIGH).get() }
|
||
.subscribeOn(Schedulers.io())
|
||
.observeOn(AndroidSchedulers.mainThread())
|
||
.subscribe({
|
||
callback.onFirst(it)
|
||
}, {
|
||
callback.onSecond(true)
|
||
it.printStackTrace()
|
||
})
|
||
}
|
||
|
||
@JvmStatic
|
||
fun display(view: SimpleDraweeView?, url: String?) {
|
||
display(view, url, true)
|
||
}
|
||
|
||
/**
|
||
* 规则 width>0 Wifi/4G:x2 traffic:x1
|
||
* 统一加载 webp 忽略掉第二种方案
|
||
* 第一种方案:通过LayoutParams获取 可以快速(无延迟)获取宽高,但是无法获取wrap_content和match_parent的View
|
||
* ~~第二种方案(备用方案):有延迟,View的宽高需要在Measure过程后才能确定,能够在这里获取到正确的宽高~~
|
||
* @param isAutoPlayGif 是否禁止播放动图
|
||
*/
|
||
@JvmStatic
|
||
fun display(view: SimpleDraweeView?,
|
||
url: String?,
|
||
isAutoPlayGif: Boolean = true,
|
||
processor: Postprocessor? = null) {
|
||
if (url == null) return
|
||
|
||
// 部分自适应宽高图片需要一个 TARGET_WIDTH 来避免加载过小图片
|
||
val width = (view?.getTag(TARGET_WIDTH) as? Int) ?: view?.layoutParams?.width
|
||
val height = view?.layoutParams?.height
|
||
|
||
var lowResUrl = ""
|
||
var highResUrl = ""
|
||
|
||
// 找同一图片地址已加载过的图片作为低质量预览图
|
||
// TODO 根据实际请求大小(w_width)来避免小图用大图作为低质量图片
|
||
for (cachedImageUrl in mImageUrlCacheSet) {
|
||
if (url.isNotEmpty() && cachedImageUrl.contains(url)) {
|
||
lowResUrl = cachedImageUrl
|
||
break
|
||
}
|
||
}
|
||
|
||
val loadImageClosure: (autoPlay: Boolean, highResUrl: String, lowResUrl: String) -> Unit = { autoPlay, hUrl, lUrl ->
|
||
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(hUrl))
|
||
.setPostprocessor(processor)
|
||
.build()
|
||
val controller = Fresco.newDraweeControllerBuilder()
|
||
.setImageRequest(imageRequest)
|
||
.apply {
|
||
if (lUrl.isNotEmpty()
|
||
&& lUrl != hUrl
|
||
&& hUrl != view?.getTag(R.string.highResImageTag)) {
|
||
lowResImageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(lUrl))
|
||
.setPostprocessor(processor)
|
||
.build()
|
||
}
|
||
autoPlayAnimations = autoPlay
|
||
}
|
||
.build()
|
||
view?.controller = controller
|
||
|
||
view?.setTag(R.string.highResImageTag, highResUrl)
|
||
}
|
||
|
||
// 低于 2G 运行内存的不加载 gif
|
||
val shouldLoadAsGif = url.endsWith(".gif")
|
||
&& isAutoPlayGif
|
||
&& (HaloApp.getInstance().deviceRamSize == 0L || HaloApp.getInstance().deviceRamSize > 2500)
|
||
&& view?.getTag(R.id.tag_show_gif) != false
|
||
|
||
if (shouldLoadAsGif && view?.tag == url) return
|
||
|
||
if (!shouldLoadAsGif) {
|
||
highResUrl = getTransformLimitUrl(url, width ?: 0, view?.context) ?: ""
|
||
loadImageClosure(shouldLoadAsGif, highResUrl, lowResUrl)
|
||
} else {
|
||
if (width != null && width > 0) {
|
||
highResUrl = resizeGif(url, view!!.width, height ?: 0)
|
||
loadImageClosure(shouldLoadAsGif, highResUrl, lowResUrl)
|
||
} else {
|
||
view?.post {
|
||
highResUrl = resizeGif(url, view.width, height ?: 0)
|
||
loadImageClosure(shouldLoadAsGif, highResUrl, lowResUrl)
|
||
}
|
||
}
|
||
}
|
||
view?.tag = url
|
||
}
|
||
|
||
// Wifi/4G:x2 traffic:x1
|
||
@JvmStatic
|
||
fun getTransformLimitUrl(url: String?, width: Int?, context: Context?): String? {
|
||
val transformUrl: String?
|
||
if (width != null && width > 0) {
|
||
// 加载大小是实际 ImageView 大小两倍的图片真的有意义吗?
|
||
val transformUrlX2 = addLimitWidth(url, width * 2)
|
||
val transformUrlX1 = addLimitWidth(url, width)
|
||
// 当网络为 WIFI 或 4G, 且系统版本大于 5.0 && 手机内存大于 4G 才用超高清图片 (x2)
|
||
transformUrl = if (NetworkUtils.isWifiOr4GConnected(context)
|
||
&& Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
|
||
&& HaloApp.getInstance().deviceRamSize > 4500) {
|
||
transformUrlX2
|
||
} else {
|
||
// 检查X2大图是否被缓存
|
||
if (Fresco.getImagePipeline().isInBitmapMemoryCache(Uri.parse(transformUrlX2)) ||
|
||
Fresco.getImagePipeline().isInDiskCacheSync(Uri.parse(transformUrlX2))) {
|
||
transformUrlX2
|
||
} else {
|
||
transformUrlX1
|
||
}
|
||
}
|
||
} else {
|
||
transformUrl = addLimitWidth(url, null)
|
||
}
|
||
addCachedUrl(transformUrl ?: "")
|
||
return transformUrl
|
||
}
|
||
|
||
// 规则 width>0 Wifi/4G:x2 traffic:x2
|
||
@JvmStatic
|
||
fun displayIcon(view: SimpleDraweeView?, url: String?) {
|
||
val width = view?.layoutParams?.width
|
||
if (width != null && width > 0) {
|
||
view.setImageURI(addLimitWidth(url, width * 2))
|
||
} else {
|
||
view?.setImageURI(addLimitWidth(url, view.width * 2))
|
||
}
|
||
}
|
||
|
||
private fun addCachedUrl(url: String) {
|
||
if (url.startsWith("http")) {
|
||
mImageUrlCacheSet.add(url)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取已缓存的图片地址,不区分质量,暂时只供查看大图页面使用
|
||
*/
|
||
fun getCachedUrl(url: String): String {
|
||
for (decoratedUrl in mImageUrlCacheSet) {
|
||
if (decoratedUrl.contains(url)) {
|
||
return decoratedUrl
|
||
}
|
||
}
|
||
return url
|
||
}
|
||
|
||
@JvmStatic
|
||
fun bmpToByteArray(bmp: Bitmap, needRecycle: Boolean): ByteArray {
|
||
val output = ByteArrayOutputStream()
|
||
bmp.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||
if (needRecycle) {
|
||
bmp.recycle()
|
||
}
|
||
|
||
val result = output.toByteArray()
|
||
try {
|
||
output.close()
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
|
||
@JvmStatic
|
||
fun display(draweeView: SimpleDraweeView, @DrawableRes res: Int?) {
|
||
draweeView.setImageURI("res:///" + res)
|
||
}
|
||
|
||
//预加载图片
|
||
@JvmStatic
|
||
fun prefetchToDiskCache(url: String) {
|
||
val imagePipeline = Fresco.getImagePipeline()
|
||
val imageRequest = ImageRequest.fromUri(url)
|
||
imagePipeline.prefetchToDiskCache(imageRequest, HaloApp.getInstance().application)
|
||
}
|
||
|
||
private fun resizeGif(url: String, width: Int, height: Int): String {
|
||
val idealSize = getIdealGifSize(width, height)
|
||
return "$url?x-oss-process=image/resize,h_$idealSize,w_$idealSize".also { addCachedUrl(it) }
|
||
}
|
||
|
||
private fun getIdealGifSize(width: Int, height: Int): String {
|
||
return if (width > LARGE_GIF_SIZE || height > LARGE_GIF_SIZE) {
|
||
"256"
|
||
} else if (width >= STANDARD_GIF_SIZE || height >= STANDARD_GIF_SIZE) {
|
||
"192"
|
||
} else if (width > TINY_GIF_SIZE || height > TINY_GIF_SIZE) {
|
||
"128"
|
||
} else {
|
||
"64"
|
||
}
|
||
}
|
||
|
||
interface OnImageloadListener {
|
||
fun onLoadFinal(imageInfo: ImageInfo?)
|
||
}
|
||
|
||
fun getVideoSnapshot(videoUrl: String, progress: Long): String {
|
||
return "$videoUrl?x-oss-process=video/snapshot,t_$progress,f_jpg,w_0,h_0"
|
||
}
|
||
}
|