Files
assistant-android/app/src/main/java/com/gh/common/util/ImageUtils.kt

488 lines
19 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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