Compare commits
9 Commits
feat/dsp-o
...
feat/GHZSC
| Author | SHA1 | Date | |
|---|---|---|---|
| f61e5472e1 | |||
| a4548a7cdb | |||
| 7171b6d14c | |||
| 878581b83f | |||
| 2ef8dcbdfd | |||
| 7b0fe13804 | |||
| 08d56959de | |||
| 239caac46a | |||
| 830e79dc57 |
@ -72,6 +72,7 @@ android_build:
|
||||
only:
|
||||
- dev
|
||||
- release
|
||||
- feat/GHZSCY-6872
|
||||
|
||||
# 代码检查
|
||||
sonarqube_analysis:
|
||||
@ -157,3 +158,4 @@ oss-upload&send-email:
|
||||
only:
|
||||
- dev
|
||||
- release
|
||||
- feat/GHZSCY-6872
|
||||
|
||||
@ -65,7 +65,7 @@ class NewInstalledGameFragmentAdapter(context: Context, private var mViewModel:
|
||||
if (drawable != null && (drawable.intrinsicWidth >= 300 || drawable.intrinsicHeight >= 300)) {
|
||||
drawable = drawable.toBitmap(200, 200).toDrawable(mContext.resources)
|
||||
}
|
||||
binding.gameIconView.getIconIv().hierarchy.setPlaceholderImage(drawable)
|
||||
// binding.gameIconView.getIconIv().hierarchy.setPlaceholderImage(drawable)
|
||||
binding.gameIconView.getIconDecoratorIv().visibility = View.GONE
|
||||
if (isSimulatorGame(gameEntity)) {
|
||||
binding.gameDes.text = String.format("V%s", gameEntity.getApk()[0].version)
|
||||
|
||||
@ -1026,7 +1026,6 @@ class ForumDetailFragment : BaseLazyTabFragment(), IScrollable {
|
||||
.setFadeDuration(500)
|
||||
.setRoundingParams(roundingParams)
|
||||
.setPressedStateOverlay(ColorDrawable(ContextCompat.getColor(requireContext(), com.gh.gamecenter.common.R.color.pressed_bg)))
|
||||
.setPlaceholderImage(com.gh.gamecenter.common.R.drawable.occupy2, ScalingUtils.ScaleType.CENTER)
|
||||
.setBackground(ColorDrawable(ContextCompat.getColor(requireContext(), com.gh.gamecenter.common.R.color.placeholder_bg)))
|
||||
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
|
||||
.build()
|
||||
|
||||
@ -153,11 +153,10 @@ class HomeGameItemViewHolder(val binding: HomeGameItemBinding) : BaseRecyclerVie
|
||||
binding.adLabelTv
|
||||
)
|
||||
MiniGameItemHelper.setMiniGameUsage(binding.gamePlayCount, game)
|
||||
val hierarchy = binding.gameImage.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(game.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
binding.gameImage.withFallbackPlaceholder(ColorDrawable(game.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
|
||||
DownloadItemUtils.setOnClickListener(
|
||||
|
||||
@ -118,11 +118,10 @@ class CustomHomeGameItemViewHolder(
|
||||
} else {
|
||||
ImageUtils.display(binding.gameImage, game.homeSetting.image)
|
||||
}
|
||||
val hierarchy = binding.gameImage.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(game.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
binding.gameImage.withFallbackPlaceholder(ColorDrawable(game.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
binding.autoVideoView.goneIf(game.showImage) {
|
||||
|
||||
@ -50,11 +50,10 @@ class CustomHomeHorizontalSlideVideoItemViewHolder(
|
||||
binding.autoVideoView.thumbImage.setTag(ImageUtils.TAG_TARGET_WIDTH, 280F.dip2px())
|
||||
binding.gameImage.goneIf(!gameEntity.showImage) {
|
||||
ImageUtils.display(binding.gameImage, gameEntity.homeSetting.image)
|
||||
val hierarchy = binding.gameImage.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(gameEntity.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
binding.gameImage.withFallbackPlaceholder(ColorDrawable(gameEntity.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
binding.autoVideoView.goneIf(gameEntity.showImage || gameEntity.topVideo == null) {
|
||||
|
||||
@ -10,7 +10,9 @@ import com.gh.gamecenter.common.base.BaseRecyclerViewHolder
|
||||
import com.gh.gamecenter.common.utils.ImageUtils
|
||||
import com.gh.gamecenter.common.utils.dip2px
|
||||
import com.gh.gamecenter.common.utils.goneIf
|
||||
import com.gh.gamecenter.common.utils.hexStringToIntColor
|
||||
import com.gh.gamecenter.common.utils.toColor
|
||||
import com.gh.gamecenter.common.utils.withFallbackPlaceholder
|
||||
import com.gh.gamecenter.core.utils.RandomUtils
|
||||
import com.gh.gamecenter.databinding.HomeSlideListItemCustomBinding
|
||||
import com.gh.gamecenter.entity.HomeSlide
|
||||
@ -61,11 +63,10 @@ class CustomHomeSlideListItemViewHolder(val binding: HomeSlideListItemCustomBind
|
||||
}
|
||||
)
|
||||
|
||||
val hierarchy = binding.slideBackground.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(Color.parseColor(homeSlide.placeholderColor)))
|
||||
binding.slideBackground.withFallbackPlaceholder(ColorDrawable(homeSlide.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
|
||||
val linkGame = homeSlide.linkGame ?: return
|
||||
|
||||
@ -12,6 +12,7 @@ import com.facebook.imagepipeline.image.ImageInfo
|
||||
import com.gh.gamecenter.R
|
||||
import com.gh.gamecenter.common.base.BaseRecyclerViewHolder
|
||||
import com.gh.gamecenter.common.utils.*
|
||||
import com.gh.gamecenter.common.utils.ShareUtils.ShareEntrance.game
|
||||
import com.gh.gamecenter.core.utils.DisplayUtils
|
||||
import com.gh.gamecenter.core.utils.RandomUtils
|
||||
import com.gh.gamecenter.databinding.HomeSubSlideListItemCustomBinding
|
||||
@ -77,11 +78,10 @@ class CustomHomeSubSlideListItemViewHolder(val binding: HomeSubSlideListItemCust
|
||||
}
|
||||
)
|
||||
|
||||
val hierarchy = binding.slideBackground.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(Color.parseColor(homeSlide.placeholderColor)))
|
||||
binding.slideBackground.withFallbackPlaceholder(ColorDrawable(homeSlide.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,11 +33,10 @@ class HomeHorizontalSlideVideoItemViewHolder(val binding: ItemHomeHorizontalSlid
|
||||
binding.autoVideoView.thumbImage.setTag(ImageUtils.TAG_TARGET_WIDTH, 280F.dip2px())
|
||||
binding.gameImage.goneIf(gameEntity.topVideo != null || gameEntity.homeSetting.image.isEmpty()) {
|
||||
ImageUtils.display(binding.gameImage, gameEntity.homeSetting.image)
|
||||
val hierarchy = binding.gameImage.hierarchy
|
||||
try {
|
||||
hierarchy.setPlaceholderImage(ColorDrawable(gameEntity.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
binding.gameImage.withFallbackPlaceholder(ColorDrawable(gameEntity.homeSetting.placeholderColor.hexStringToIntColor()))
|
||||
} catch (ignore: Throwable) {
|
||||
hierarchy.setPlaceholderImage(RandomUtils.getRandomPlaceholderColor())
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
binding.autoVideoView.goneIf(gameEntity.topVideo == null) {
|
||||
|
||||
@ -21,6 +21,7 @@ import com.gh.gamecenter.common.constant.Constants
|
||||
import com.gh.gamecenter.common.utils.ImageUtils
|
||||
import com.gh.gamecenter.common.utils.countDownTimer
|
||||
import com.gh.gamecenter.common.utils.dip2px
|
||||
import com.gh.gamecenter.common.utils.disableThumbHash
|
||||
import com.gh.gamecenter.common.utils.rxTimer
|
||||
import com.gh.gamecenter.common.view.DrawableView
|
||||
import com.gh.gamecenter.common.view.RadiusCardView
|
||||
@ -378,6 +379,7 @@ class AutomaticVideoView @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
}
|
||||
|
||||
fun updateThumb(url: String) {
|
||||
thumbImage.disableThumbHash()
|
||||
ImageUtils.display(thumbImage, url)
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +77,6 @@ abstract class BaseBottomTabFragment<T : ViewBinding> : ToolbarFragment() {
|
||||
} else {
|
||||
ImageUtils.picasso
|
||||
.load(Uri.parse(if (bottomTab.default) bottomTab.iconSelect else bottomTab.iconUnselect))
|
||||
.placeholder(com.gh.gamecenter.common.R.drawable.occupy)
|
||||
.into(tabIv)
|
||||
}
|
||||
root.isChecked = bottomTab.default
|
||||
|
||||
@ -330,7 +330,6 @@ class MainWrapperFragment : BaseBottomTabFragment<PieceBottomTabBinding>(), OnBa
|
||||
if (index < mBottomTabBindingList.size && bottomTab.iconSelector == 0) {
|
||||
ImageUtils.picasso
|
||||
.load(Uri.parse(if (position == index) bottomTab.iconSelect else bottomTab.iconUnselect))
|
||||
.placeholder(com.gh.gamecenter.common.R.drawable.occupy)
|
||||
.into(mBottomTabBindingList[index].tabIv)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
app:fadeDuration="500"
|
||||
app:placeholderImage="@drawable/occupy"
|
||||
app:placeholderImageScaleType="fitXY"
|
||||
app:roundedCornerRadius="8dp" />
|
||||
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:placeholderImage="@drawable/occupy"
|
||||
app:placeholderImageScaleType="fitXY"
|
||||
app:placeholderImage="@drawable/occupy"
|
||||
app:roundedCornerRadius="5dp"
|
||||
app:viewAspectRatio="2"
|
||||
tools:layout_width="match_parent"
|
||||
|
||||
@ -5,6 +5,7 @@ import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.text.TextUtils
|
||||
@ -36,7 +37,6 @@ import com.facebook.imagepipeline.request.Postprocessor
|
||||
import com.gh.gamecenter.common.HaloApp
|
||||
import com.gh.gamecenter.common.R
|
||||
import com.gh.gamecenter.common.callback.BiCallback
|
||||
import com.gh.gamecenter.common.constant.RouteConsts
|
||||
import com.gh.gamecenter.core.GHThreadFactory
|
||||
import com.gh.gamecenter.core.provider.IConfigProvider
|
||||
import com.gh.gamecenter.core.runOnUiThread
|
||||
@ -49,18 +49,24 @@ import com.squareup.picasso.Picasso
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import thumbhash.ThumbHashDrawable
|
||||
import java.io.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object ImageUtils {
|
||||
private const val TAG = "ImageUtils"
|
||||
|
||||
private const val THUMBHASH_PREFIX = "thumbhash"
|
||||
private const val THUMBHASH_REGEX = "(?<=thumbhash_)([^.]+)"
|
||||
|
||||
private const val PIC_MAX_FILE_SIZE: Long = 10 * 1024 * 1024
|
||||
|
||||
private val TINY_ANIMATED_ICON_SIZE by lazy { 30F.dip2px() }
|
||||
private val LARGE_ANIMATED_ICON_SIZE by lazy { 80F.dip2px() }
|
||||
private val STANDARD_ANIMATED_ICON_SIZE by lazy { 60F.dip2px() }
|
||||
|
||||
private val thumbHashRegex by lazy { Regex(THUMBHASH_REGEX) }
|
||||
|
||||
val TAG_TARGET_WIDTH by lazy { R.dimen.width_placeholder }
|
||||
val TAG_IS_GAME_ICON by lazy { R.dimen.game_icon_placeholder }
|
||||
|
||||
@ -213,6 +219,7 @@ object ImageUtils {
|
||||
) {
|
||||
if (simpleDraweeView == null) return
|
||||
val context = simpleDraweeView.context ?: return
|
||||
|
||||
simpleDraweeView.hierarchy = GenericDraweeHierarchyBuilder(resources)
|
||||
.setFadeDuration(500)
|
||||
.setPressedStateOverlay(
|
||||
@ -327,6 +334,12 @@ object ImageUtils {
|
||||
var lowResUrl = ""
|
||||
var highResUrl = ""
|
||||
|
||||
var thumbHash = if (view?.getTag(R.id.disable_thumbhash) == null) {
|
||||
getThumbHashIfValid(url)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
// 找同一图片地址已加载过的图片作为低质量预览图(字符串操作耗时不短,所以这里放到了独立的子线程来处理)
|
||||
// TODO 根据实际请求大小(w_width)来避免小图用大图作为低质量图片
|
||||
for (cachedImageUrl in imageUrlCacheSet) {
|
||||
@ -346,13 +359,13 @@ object ImageUtils {
|
||||
|
||||
highResUrl = getTunedImageUrl(view, url, width ?: 0, loadAsAnimatedImage, processor)
|
||||
runOnUiThread {
|
||||
loadImage(view, highResUrl, lowResUrl, processor, disableMemoryCache, controllerListener)
|
||||
loadImage(view, thumbHash, highResUrl, lowResUrl, processor, disableMemoryCache, controllerListener)
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
view ?: return@runOnUiThread
|
||||
highResUrl = getTunedImageUrl(view, url, width ?: view.width, true, processor)
|
||||
loadImage(view, highResUrl, lowResUrl, processor, disableMemoryCache, controllerListener)
|
||||
loadImage(view, thumbHash, highResUrl, lowResUrl, processor, disableMemoryCache, controllerListener)
|
||||
}
|
||||
}
|
||||
view?.tag = url
|
||||
@ -432,14 +445,15 @@ object ImageUtils {
|
||||
}
|
||||
|
||||
private fun loadImage(
|
||||
view: SimpleDraweeView?,
|
||||
view: SimpleDraweeView,
|
||||
thumbHash: String?,
|
||||
highResUrl: String,
|
||||
lowResUrl: String,
|
||||
processor: Postprocessor?,
|
||||
shouldNotSaveMemoryCache: Boolean,
|
||||
controllerListener: BaseControllerListener<ImageInfo>? = null,
|
||||
) {
|
||||
val lifecycleObserver = view?.getTag(R.id.lifecycle_observer) as? LifecycleObserver
|
||||
val lifecycleObserver = view.getTag(R.id.lifecycle_observer) as? LifecycleObserver
|
||||
val lifecycle = if (lifecycleObserver != null) {
|
||||
try {
|
||||
view.findFragment()?.viewLifecycleOwner?.lifecycle
|
||||
@ -447,11 +461,25 @@ object ImageUtils {
|
||||
null
|
||||
}
|
||||
} else null
|
||||
|
||||
val fallbackPlaceholder = view.getTag(R.id.fallback_placeholder)
|
||||
val fallbackPlaceholderScaleType = view.getTag(R.id.fallback_placeholder_scale_type) as? ScalingUtils.ScaleType
|
||||
|
||||
lifecycleObserver?.let {
|
||||
lifecycle?.removeObserver(it)
|
||||
view.setTag(R.id.lifecycle_observer, null)
|
||||
}
|
||||
|
||||
if (!thumbHash.isNullOrEmpty()) {
|
||||
view.hierarchy?.setPlaceholderImage(ThumbHashDrawable(thumbHash, view.width, view.height))
|
||||
} else if (fallbackPlaceholder != null) {
|
||||
if (fallbackPlaceholder is Int) {
|
||||
view.hierarchy?.setPlaceholderImage(fallbackPlaceholder, fallbackPlaceholderScaleType)
|
||||
} else if (fallbackPlaceholder is Drawable) {
|
||||
view.hierarchy?.setPlaceholderImage(fallbackPlaceholder)
|
||||
}
|
||||
}
|
||||
|
||||
val listener = getFinalControllerListener(view, controllerListener, lifecycle)
|
||||
val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(highResUrl))
|
||||
.apply {
|
||||
@ -467,7 +495,7 @@ object ImageUtils {
|
||||
.apply {
|
||||
if (lowResUrl.isNotEmpty()
|
||||
&& lowResUrl != highResUrl
|
||||
&& highResUrl != view?.getTag(R.string.highResImageTag)
|
||||
&& highResUrl != view.getTag(R.string.highResImageTag)
|
||||
) {
|
||||
lowResImageRequest =
|
||||
ImageRequestBuilder.newBuilderWithSource(Uri.parse(lowResUrl))
|
||||
@ -477,9 +505,9 @@ object ImageUtils {
|
||||
autoPlayAnimations = true
|
||||
}
|
||||
.build()
|
||||
view?.controller = controller
|
||||
view.controller = controller
|
||||
|
||||
view?.setTag(R.string.highResImageTag, highResUrl)
|
||||
view.setTag(R.string.highResImageTag, highResUrl)
|
||||
}
|
||||
|
||||
private fun getFinalControllerListener(
|
||||
@ -803,4 +831,31 @@ object ImageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThumbHashIfValid(url: String) : String {
|
||||
return if (url.contains(THUMBHASH_PREFIX)) {
|
||||
var hash = thumbHashRegex.find(url)?.groups?.get(1)?.value ?: ""
|
||||
hash.replace("-", "+").replace("_", "/")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun SimpleDraweeView.withFallbackPlaceholder(fitXy: Boolean) {
|
||||
if (fitXy) {
|
||||
setTag(R.id.fallback_placeholder, R.drawable.occupy)
|
||||
setTag(R.id.fallback_placeholder_scale_type, ScalingUtils.ScaleType.FIT_XY)
|
||||
} else {
|
||||
setTag(R.id.fallback_placeholder, R.drawable.occupy2)
|
||||
setTag(R.id.fallback_placeholder_scale_type, ScalingUtils.ScaleType.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
fun SimpleDraweeView.withFallbackPlaceholder(drawable: Drawable) {
|
||||
setTag(R.id.fallback_placeholder, drawable)
|
||||
}
|
||||
|
||||
fun SimpleDraweeView.disableThumbHash() {
|
||||
setTag(R.id.disable_thumbhash, true)
|
||||
}
|
||||
363
module_common/src/main/java/thumbhash/ThumbHash.kt
Normal file
363
module_common/src/main/java/thumbhash/ThumbHash.kt
Normal file
@ -0,0 +1,363 @@
|
||||
package thumbhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Base64
|
||||
import com.lightgame.utils.Utils
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
object ThumbHash {
|
||||
|
||||
fun getBitmapFromHash(hash: String, fullWidth: Int? = null, fullHeight: Int? = null): Bitmap {
|
||||
var bitmap: Bitmap? = null
|
||||
val elapsedTime = measureTimeMillis {
|
||||
val image = thumbHashToRGBA(Base64.decode(hash, Base64.DEFAULT))
|
||||
val bmp = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
|
||||
bmp.copyPixelsFromBuffer(ByteBuffer.wrap(image.rgba))
|
||||
|
||||
bitmap = bmp
|
||||
}
|
||||
|
||||
Utils.log("getBitmapFromHash fullWidth $fullWidth and fullHeight $fullHeight elapsed time: $elapsedTime ms")
|
||||
|
||||
if (fullHeight == 0 || fullWidth == 0) return bitmap!!
|
||||
|
||||
fullWidth ?: return bitmap!!
|
||||
fullHeight ?: return bitmap!!
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap!!, fullWidth, fullHeight, true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
|
||||
*
|
||||
* @param w The width of the input image. Must be ≤100px.
|
||||
* @param h The height of the input image. Must be ≤100px.
|
||||
* @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements.
|
||||
* @return The ThumbHash as a byte array.
|
||||
*/
|
||||
fun rgbaToThumbHash(w: Int, h: Int, rgba: ByteArray): ByteArray {
|
||||
// Encoding an image larger than 100x100 is slow with no benefit
|
||||
require(!(w > 100 || h > 100)) { w.toString() + "x" + h + " doesn't fit in 100x100" }
|
||||
|
||||
// Determine the average color
|
||||
var avg_r = 0f
|
||||
var avg_g = 0f
|
||||
var avg_b = 0f
|
||||
var avg_a = 0f
|
||||
run {
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < w * h) {
|
||||
val alpha = (rgba[j + 3].toInt() and 255) / 255.0f
|
||||
avg_r += alpha / 255.0f * (rgba[j].toInt() and 255)
|
||||
avg_g += alpha / 255.0f * (rgba[j + 1].toInt() and 255)
|
||||
avg_b += alpha / 255.0f * (rgba[j + 2].toInt() and 255)
|
||||
avg_a += alpha
|
||||
i++
|
||||
j += 4
|
||||
}
|
||||
}
|
||||
if (avg_a > 0) {
|
||||
avg_r /= avg_a
|
||||
avg_g /= avg_a
|
||||
avg_b /= avg_a
|
||||
}
|
||||
val hasAlpha = avg_a < w * h
|
||||
val l_limit = if (hasAlpha) 5 else 7 // Use fewer luminance bits if there's alpha
|
||||
val lx = Math.max(1, Math.round((l_limit * w).toFloat() / Math.max(w, h).toFloat()))
|
||||
val ly = Math.max(1, Math.round((l_limit * h).toFloat() / Math.max(w, h).toFloat()))
|
||||
val l = FloatArray(w * h) // luminance
|
||||
val p = FloatArray(w * h) // yellow - blue
|
||||
val q = FloatArray(w * h) // red - green
|
||||
val a = FloatArray(w * h) // alpha
|
||||
|
||||
// Convert the image from RGBA to LPQA (composite atop the average color)
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < w * h) {
|
||||
val alpha = (rgba[j + 3].toInt() and 255) / 255.0f
|
||||
val r = avg_r * (1.0f - alpha) + alpha / 255.0f * (rgba[j].toInt() and 255)
|
||||
val g = avg_g * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 1].toInt() and 255)
|
||||
val b = avg_b * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 2].toInt() and 255)
|
||||
l[i] = (r + g + b) / 3.0f
|
||||
p[i] = (r + g) / 2.0f - b
|
||||
q[i] = r - g
|
||||
a[i] = alpha
|
||||
i++
|
||||
j += 4
|
||||
}
|
||||
|
||||
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
|
||||
val l_channel = Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l)
|
||||
val p_channel = Channel(3, 3).encode(w, h, p)
|
||||
val q_channel = Channel(3, 3).encode(w, h, q)
|
||||
val a_channel = if (hasAlpha) Channel(5, 5).encode(w, h, a) else null
|
||||
|
||||
// Write the constants
|
||||
val isLandscape = w > h
|
||||
val header24 = (Math.round(63.0f * l_channel.dc)
|
||||
or (Math.round(31.5f + 31.5f * p_channel.dc) shl 6)
|
||||
or (Math.round(31.5f + 31.5f * q_channel.dc) shl 12)
|
||||
or (Math.round(31.0f * l_channel.scale) shl 18)
|
||||
or if (hasAlpha) 1 shl 23 else 0)
|
||||
val header16 = ((if (isLandscape) ly else lx)
|
||||
or (Math.round(63.0f * p_channel.scale) shl 3)
|
||||
or (Math.round(63.0f * q_channel.scale) shl 9)
|
||||
or if (isLandscape) 1 shl 15 else 0)
|
||||
val ac_start = if (hasAlpha) 6 else 5
|
||||
val ac_count = (l_channel.ac.size + p_channel.ac.size + q_channel.ac.size
|
||||
+ if (hasAlpha) a_channel!!.ac.size else 0)
|
||||
val hash = ByteArray(ac_start + (ac_count + 1) / 2)
|
||||
hash[0] = header24.toByte()
|
||||
hash[1] = (header24 shr 8).toByte()
|
||||
hash[2] = (header24 shr 16).toByte()
|
||||
hash[3] = header16.toByte()
|
||||
hash[4] = (header16 shr 8).toByte()
|
||||
if (hasAlpha) hash[5] = (Math.round(15.0f * a_channel!!.dc)
|
||||
or (Math.round(15.0f * a_channel.scale) shl 4)).toByte()
|
||||
|
||||
// Write the varying factors
|
||||
var ac_index = 0
|
||||
ac_index = l_channel.writeTo(hash, ac_start, ac_index)
|
||||
ac_index = p_channel.writeTo(hash, ac_start, ac_index)
|
||||
ac_index = q_channel.writeTo(hash, ac_start, ac_index)
|
||||
if (hasAlpha) a_channel!!.writeTo(hash, ac_start, ac_index)
|
||||
return hash
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
|
||||
*
|
||||
* @param hash The bytes of the ThumbHash.
|
||||
* @return The width, height, and pixels of the rendered placeholder image.
|
||||
*/
|
||||
fun thumbHashToRGBA(hash: ByteArray): Image {
|
||||
// Read the constants
|
||||
val header24 = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
|
||||
val header16 = hash[3].toInt() and 255 or (hash[4].toInt() and 255 shl 8)
|
||||
val l_dc = (header24 and 63).toFloat() / 63.0f
|
||||
val p_dc = (header24 shr 6 and 63).toFloat() / 31.5f - 1.0f
|
||||
val q_dc = (header24 shr 12 and 63).toFloat() / 31.5f - 1.0f
|
||||
val l_scale = (header24 shr 18 and 31).toFloat() / 31.0f
|
||||
val hasAlpha = header24 shr 23 != 0
|
||||
val p_scale = (header16 shr 3 and 63).toFloat() / 63.0f
|
||||
val q_scale = (header16 shr 9 and 63).toFloat() / 63.0f
|
||||
val isLandscape = header16 shr 15 != 0
|
||||
val lx = Math.max(3, if (isLandscape) if (hasAlpha) 5 else 7 else header16 and 7)
|
||||
val ly = Math.max(3, if (isLandscape) header16 and 7 else if (hasAlpha) 5 else 7)
|
||||
val a_dc = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
|
||||
val a_scale = (hash[5].toInt() shr 4 and 15).toFloat() / 15.0f
|
||||
|
||||
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
||||
val ac_start = if (hasAlpha) 6 else 5
|
||||
var ac_index = 0
|
||||
val l_channel = Channel(lx, ly)
|
||||
val p_channel = Channel(3, 3)
|
||||
val q_channel = Channel(3, 3)
|
||||
var a_channel: Channel? = null
|
||||
ac_index = l_channel.decode(hash, ac_start, ac_index, l_scale)
|
||||
ac_index = p_channel.decode(hash, ac_start, ac_index, p_scale * 1.25f)
|
||||
ac_index = q_channel.decode(hash, ac_start, ac_index, q_scale * 1.25f)
|
||||
if (hasAlpha) {
|
||||
a_channel = Channel(5, 5)
|
||||
a_channel.decode(hash, ac_start, ac_index, a_scale)
|
||||
}
|
||||
val l_ac = l_channel.ac
|
||||
val p_ac = p_channel.ac
|
||||
val q_ac = q_channel.ac
|
||||
val a_ac = if (hasAlpha) a_channel!!.ac else null
|
||||
|
||||
// Decode using the DCT into RGB
|
||||
val ratio = thumbHashToApproximateAspectRatio(hash)
|
||||
val w = Math.round(if (ratio > 1.0f) 32.0f else 32.0f * ratio)
|
||||
val h = Math.round(if (ratio > 1.0f) 32.0f / ratio else 32.0f)
|
||||
val rgba = ByteArray(w * h * 4)
|
||||
val cx_stop = Math.max(lx, if (hasAlpha) 5 else 3)
|
||||
val cy_stop = Math.max(ly, if (hasAlpha) 5 else 3)
|
||||
val fx = FloatArray(cx_stop)
|
||||
val fy = FloatArray(cy_stop)
|
||||
var y = 0
|
||||
var i = 0
|
||||
while (y < h) {
|
||||
var x = 0
|
||||
while (x < w) {
|
||||
var l = l_dc
|
||||
var p = p_dc
|
||||
var q = q_dc
|
||||
var a = a_dc
|
||||
|
||||
// Precompute the coefficients
|
||||
for (cx in 0 until cx_stop) fx[cx] = Math.cos(Math.PI / w * (x + 0.5f) * cx).toFloat()
|
||||
for (cy in 0 until cy_stop) fy[cy] = Math.cos(Math.PI / h * (y + 0.5f) * cy).toFloat()
|
||||
|
||||
// Decode L
|
||||
run {
|
||||
var cy = 0
|
||||
var j = 0
|
||||
while (cy < ly) {
|
||||
val fy2 = fy[cy] * 2.0f
|
||||
var cx = if (cy > 0) 0 else 1
|
||||
while (cx * ly < lx * (ly - cy)) {
|
||||
l += l_ac[j] * fx[cx] * fy2
|
||||
cx++
|
||||
j++
|
||||
}
|
||||
cy++
|
||||
}
|
||||
}
|
||||
|
||||
// Decode P and Q
|
||||
var cy = 0
|
||||
var j = 0
|
||||
while (cy < 3) {
|
||||
val fy2 = fy[cy] * 2.0f
|
||||
var cx = if (cy > 0) 0 else 1
|
||||
while (cx < 3 - cy) {
|
||||
val f = fx[cx] * fy2
|
||||
p += p_ac[j] * f
|
||||
q += q_ac[j] * f
|
||||
cx++
|
||||
j++
|
||||
}
|
||||
cy++
|
||||
}
|
||||
|
||||
// Decode A
|
||||
if (hasAlpha) {
|
||||
var cy = 0
|
||||
var j = 0
|
||||
while (cy < 5) {
|
||||
val fy2 = fy[cy] * 2.0f
|
||||
var cx = if (cy > 0) 0 else 1
|
||||
while (cx < 5 - cy) {
|
||||
a += a_ac!![j] * fx[cx] * fy2
|
||||
cx++
|
||||
j++
|
||||
}
|
||||
cy++
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to RGB
|
||||
val b = l - 2.0f / 3.0f * p
|
||||
val r = (3.0f * l - b + q) / 2.0f
|
||||
val g = r - q
|
||||
rgba[i] = Math.max(0, Math.round(255.0f * Math.min(1f, r))).toByte()
|
||||
rgba[i + 1] = Math.max(0, Math.round(255.0f * Math.min(1f, g))).toByte()
|
||||
rgba[i + 2] = Math.max(0, Math.round(255.0f * Math.min(1f, b))).toByte()
|
||||
rgba[i + 3] = Math.max(0, Math.round(255.0f * Math.min(1f, a))).toByte()
|
||||
x++
|
||||
i += 4
|
||||
}
|
||||
y++
|
||||
}
|
||||
return Image(w, h, rgba)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the average color from a ThumbHash. RGB is not be premultiplied by A.
|
||||
*
|
||||
* @param hash The bytes of the ThumbHash.
|
||||
* @return The RGBA values for the average color. Each value ranges from 0 to 1.
|
||||
*/
|
||||
fun thumbHashToAverageRGBA(hash: ByteArray): RGBA {
|
||||
val header = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
|
||||
val l = (header and 63).toFloat() / 63.0f
|
||||
val p = (header shr 6 and 63).toFloat() / 31.5f - 1.0f
|
||||
val q = (header shr 12 and 63).toFloat() / 31.5f - 1.0f
|
||||
val hasAlpha = header shr 23 != 0
|
||||
val a = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
|
||||
val b = l - 2.0f / 3.0f * p
|
||||
val r = (3.0f * l - b + q) / 2.0f
|
||||
val g = r - q
|
||||
return RGBA(
|
||||
Math.max(0f, Math.min(1f, r)),
|
||||
Math.max(0f, Math.min(1f, g)),
|
||||
Math.max(0f, Math.min(1f, b)),
|
||||
a
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the approximate aspect ratio of the original image.
|
||||
*
|
||||
* @param hash The bytes of the ThumbHash.
|
||||
* @return The approximate aspect ratio (i.e. width / height).
|
||||
*/
|
||||
fun thumbHashToApproximateAspectRatio(hash: ByteArray): Float {
|
||||
val header = hash[3]
|
||||
val hasAlpha = hash[2].toInt() and 0x80 != 0
|
||||
val isLandscape = hash[4].toInt() and 0x80 != 0
|
||||
val lx = if (isLandscape) if (hasAlpha) 5 else 7 else header.toInt() and 7
|
||||
val ly = if (isLandscape) header.toInt() and 7 else if (hasAlpha) 5 else 7
|
||||
return lx.toFloat() / ly.toFloat()
|
||||
}
|
||||
|
||||
data class Image(var width: Int, var height: Int, var rgba: ByteArray)
|
||||
data class RGBA(var r: Float, var g: Float, var b: Float, var a: Float)
|
||||
private class Channel internal constructor(var nx: Int, var ny: Int) {
|
||||
var dc = 0f
|
||||
var ac: FloatArray
|
||||
var scale = 0f
|
||||
|
||||
init {
|
||||
var n = 0
|
||||
for (cy in 0 until ny) {
|
||||
var cx = if (cy > 0) 0 else 1
|
||||
while (cx * ny < nx * (ny - cy)) {
|
||||
n++
|
||||
cx++
|
||||
}
|
||||
}
|
||||
ac = FloatArray(n)
|
||||
}
|
||||
|
||||
fun encode(w: Int, h: Int, channel: FloatArray): Channel {
|
||||
var n = 0
|
||||
val fx = FloatArray(w)
|
||||
for (cy in 0 until ny) {
|
||||
var cx = 0
|
||||
while (cx * ny < nx * (ny - cy)) {
|
||||
var f = 0f
|
||||
for (x in 0 until w) fx[x] = Math.cos(Math.PI / w * cx * (x + 0.5f)).toFloat()
|
||||
for (y in 0 until h) {
|
||||
val fy = Math.cos(Math.PI / h * cy * (y + 0.5f)).toFloat()
|
||||
for (x in 0 until w) f += channel[x + y * w] * fx[x] * fy
|
||||
}
|
||||
f /= (w * h).toFloat()
|
||||
if (cx > 0 || cy > 0) {
|
||||
ac[n++] = f
|
||||
scale = Math.max(scale, Math.abs(f))
|
||||
} else {
|
||||
dc = f
|
||||
}
|
||||
cx++
|
||||
}
|
||||
}
|
||||
if (scale > 0) for (i in ac.indices) ac[i] = 0.5f + 0.5f / scale * ac[i]
|
||||
return this
|
||||
}
|
||||
|
||||
fun decode(hash: ByteArray, start: Int, index: Int, scale: Float): Int {
|
||||
var index = index
|
||||
for (i in ac.indices) {
|
||||
val data = hash[start + (index shr 1)].toInt() shr (index and 1 shl 2)
|
||||
ac[i] = ((data and 15).toFloat() / 7.5f - 1.0f) * scale
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
fun writeTo(hash: ByteArray, start: Int, index: Int): Int {
|
||||
var index = index
|
||||
for (v in ac) {
|
||||
hash[start + (index shr 1)] =
|
||||
(hash[start + (index shr 1)].toInt() or (Math.round(15.0f * v) shl (index and 1 shl 2))).toByte()
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
76
module_common/src/main/java/thumbhash/ThumbHashDrawable.kt
Normal file
76
module_common/src/main/java/thumbhash/ThumbHashDrawable.kt
Normal file
@ -0,0 +1,76 @@
|
||||
package thumbhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.withTranslation
|
||||
|
||||
class ThumbHashDrawable(
|
||||
private val hash: String,
|
||||
private val targetWidth: Int = MIN_SIZE, // Too large a resolution will affect performance
|
||||
private val targetHeight: Int = MIN_SIZE, // Too large a resolution will affect performance
|
||||
) : Drawable() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private var bitmap: Bitmap? = null
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return bitmap?.height ?: targetHeight
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return bitmap?.width ?: targetWidth
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
updateThumbHash(bounds)
|
||||
}
|
||||
|
||||
private fun calculateSize(boundSize: Int, targetSize: Int): Int {
|
||||
return if (targetSize > MIN_SIZE) targetSize else boundSize
|
||||
}
|
||||
|
||||
private fun updateThumbHash(bounds: Rect) {
|
||||
val width = calculateSize(bounds.width(), targetWidth)
|
||||
val height = calculateSize(bounds.height(), targetHeight)
|
||||
val bitmap = this.bitmap
|
||||
if (bitmap != null) {
|
||||
if (width == bitmap.width && height == bitmap.height) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.bitmap = ThumbHash.getBitmapFromHash(hash, width, height)
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val bitmap = this.bitmap ?: return
|
||||
val rect = bounds
|
||||
|
||||
canvas.withTranslation(rect.left.toFloat(), rect.top.toFloat()) {
|
||||
drawBitmap(bitmap, 0F, 0F, paint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.OPAQUE
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val MIN_SIZE = 0
|
||||
}
|
||||
}
|
||||
@ -2,4 +2,7 @@
|
||||
<resources>
|
||||
<item name="root_container" type="id" />
|
||||
<item name="lifecycle_observer" type="id" />
|
||||
<item name="fallback_placeholder" type="id" />
|
||||
<item name="disable_thumbhash" type="id" />
|
||||
<item name="fallback_placeholder_scale_type" type="id" />
|
||||
</resources>
|
||||
@ -168,10 +168,7 @@ class GameIconView : FrameLayout {
|
||||
|
||||
setTag(ImageUtils.TAG_IS_GAME_ICON, true)
|
||||
|
||||
mGameIconIv?.hierarchy?.setPlaceholderImage(
|
||||
ContextCompat.getDrawable(context, com.gh.gamecenter.common.R.drawable.occupy),
|
||||
ScalingUtils.ScaleType.FIT_XY
|
||||
)
|
||||
mGameIconIv?.withFallbackPlaceholder(fitXy = true)
|
||||
mGameIconIv?.aspectRatio = 1.0F
|
||||
|
||||
mGameIconIv?.display(icon)
|
||||
|
||||
@ -777,7 +777,6 @@ class ComposeBindPhoneActivity : ComposeBaseActivity() {
|
||||
SimpleDraweeView(it).apply {
|
||||
val builder = GenericDraweeHierarchyBuilder(it.resources)
|
||||
hierarchy = builder.setFadeDuration(500)
|
||||
.setPlaceholderImage(com.gh.gamecenter.common.R.drawable.occupy)
|
||||
.setPlaceholderImageScaleType(ScalingUtils.ScaleType.FIT_XY)
|
||||
.setRoundingParams(RoundingParams.asCircle().apply {
|
||||
borderColor = com.gh.gamecenter.common.R.color.ui_divider.toColor(it)
|
||||
|
||||
Reference in New Issue
Block a user