-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathAsyncDrawable.kt
More file actions
145 lines (121 loc) · 3.8 KB
/
Copy pathAsyncDrawable.kt
File metadata and controls
145 lines (121 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package com.swmansion.enriched.common
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.ImageDecoder
import android.graphics.PixelFormat
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toDrawable
import com.swmansion.enriched.R
import java.net.URL
import java.nio.ByteBuffer
import java.util.concurrent.Executors
class AsyncDrawable(
private val url: String,
private var placeholderTintColor: Int,
) : Drawable() {
private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable()
private val mainHandler = Handler(Looper.getMainLooper())
private val executor = Executors.newSingleThreadExecutor()
var isLoaded = false
var isShowingPlaceholder = false
private set
init {
internalDrawable.bounds = bounds
load()
}
private fun load() {
executor.execute {
try {
isLoaded = false
val inputStream = URL(url).openStream()
val bytes = inputStream.readBytes()
val d = prepareDrawable(bytes)
// Switch to Main Thread to update UI
mainHandler.post {
if (d != null) {
d.bounds = bounds
internalDrawable = d
} else {
loadPlaceholderImage()
}
}
} catch (e: Exception) {
Log.e("AsyncDrawable", "Failed to load: $url", e)
mainHandler.post {
loadPlaceholderImage()
}
} finally {
isLoaded = true
onLoaded?.invoke()
}
}
}
private fun prepareDrawable(bytes: ByteArray): Drawable? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val buffer = ByteBuffer.wrap(bytes)
val source = ImageDecoder.createSource(buffer)
val drawable =
ImageDecoder.decodeDrawable(source) { decoder, _, _ ->
decoder.setTargetSize(bounds.width(), bounds.height())
}
if (drawable is AnimatedImageDrawable) {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.repeatCount = AnimatedImageDrawable.REPEAT_INFINITE
drawable.start()
}
return drawable
} catch (e: Exception) {
Log.w("AsyncDrawable", "ImageDecoder failed, falling back to Bitmap", e)
}
}
// Fallback to bitmap if ImageDecoder fails
return try {
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
bitmap?.toDrawable(Resources.getSystem())
} catch (_: Exception) {
null
}
}
private fun loadPlaceholderImage() {
val drawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
DrawableCompat.setTint(drawable, placeholderTintColor)
isShowingPlaceholder = true
internalDrawable = drawable
}
fun applyPlaceholderTint(color: Int) {
placeholderTintColor = color
if (!isShowingPlaceholder) return
DrawableCompat.setTint(internalDrawable, color)
}
override fun draw(canvas: Canvas) {
internalDrawable.draw(canvas)
}
override fun setAlpha(alpha: Int) {
internalDrawable.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
internalDrawable.colorFilter = colorFilter
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
override fun setBounds(
left: Int,
top: Int,
right: Int,
bottom: Int,
) {
super.setBounds(left, top, right, bottom)
internalDrawable.setBounds(left, top, right, bottom)
}
var onLoaded: (() -> Unit)? = null
}