Skip to content

Instantly share code, notes, and snippets.

@Miha-x64
Last active April 26, 2021 18:48
Show Gist options
  • Save Miha-x64/c3b0cff4f38690455d85d5c415024ebe to your computer and use it in GitHub Desktop.
Save Miha-x64/c3b0cff4f38690455d85d5c415024ebe to your computer and use it in GitHub Desktop.
A TextView with Picasso targets for compound drawables
package net.aquadc.commonandroid.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.TransitionDrawable
import android.view.View
import android.widget.TextView
import com.squareup.picasso.Picasso
import java.lang.Exception
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.math.min
/**
* A TextView subclass which has a Picasso Target for each compound drawable.
*
* The original idea of implementing Target in a TextView was suggested
* by Jake Wharton here: https://github.com/square/picasso/issues/308#issuecomment-28499104
*
* This gist permalink: https://gist.github.com/Miha-x64/c3b0cff4f38690455d85d5c415024ebe
*/
@SuppressLint("AppCompatCustomView")
open class PicassoTextView(context: Context) : TextView(context) {
private companion object {
private val emptyDrawables: Drawable.ConstantState = ColorDrawable().constantState!!
}
abstract inner class DrawableTarget internal constructor() : com.squareup.picasso.Target {
final override fun onPrepareLoad(placeHolderDrawable: Drawable?): Unit =
setDrawable(placeHolderDrawable)
final override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?): Unit =
setDrawable(errorDrawable)
final override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom): Unit {
val animate = from != Picasso.LoadedFrom.MEMORY
val drawable =
if (animate)
TransitionDrawable(arrayOf(emptyDrawables.newDrawable(), BitmapDrawable(resources, bitmap)))
.also { it.startTransition(fadeDuration) }
else
BitmapDrawable(resources, bitmap)
setDrawable(drawable)
}
/** Drawable fade duration in millis. */
var fadeDuration: Int = 300
set(value) {
check(value >= 0)
field = value
}
/** Shows you current drawable. */
@JvmName("get") operator fun invoke(): Drawable? = getDrawable()
/** Allows you to change current drawable. */
@JvmName("set") operator fun invoke(new: Drawable?): Unit = setDrawable(new)
protected abstract fun getDrawable(): Drawable?
protected abstract fun setDrawable(drawable: Drawable?)
}
inner class CompoundDrawable internal constructor(private val index: Int) : DrawableTarget() {
/** Compound drawable width hint, when {
* <0 -> limit width to abs(width)
* 0 -> use intrinsic width
* >0 -> force width (but save aspect if height is also forced)
* } */
var widthHint: Int = 0
set(new) {
if (field != new) {
field = new
setDrawable(compoundDrawableAt(index))
}
}
/** Compound drawable height hint, when {
* <0 -> limit height to abs(height)
* 0 -> use intrinsic height
* >0 -> force height (but save aspect if width is also forced)
* } */
var heightHint: Int = 0
set(new) {
if (field != new) {
field = new
setDrawable(compoundDrawableAt(index))
}
}
override fun getDrawable(): Drawable? =
compoundDrawableAt(index)?.takeIf { it.constantState != emptyDrawables }
override fun setDrawable(drawable: Drawable?) {
val drawable = drawable ?: emptyDrawables.newDrawable()
val imgWidth = drawable.intrinsicWidth
val imgHeight = drawable.intrinsicHeight
var width = widthHint
var height = heightHint
// if dimensions are limited, force smaller
if (width < 0) width = min(-width, imgWidth)
if (height < 0) height = min(-height, imgHeight)
// if dimensions are forced (or limited), apply but save aspect
if (width == 0 && height == 0) {
width = imgWidth
height = imgHeight
} else if (width == 0) { // fit width preserving forced/max height
width = (height.toDouble() / imgHeight * imgWidth).toInt()
} else if (height == 0) { // fit height preserving forced/max width
height = (width.toDouble() / imgWidth * imgHeight).toInt()
} else { // preserve aspect ratio by limiting image size in both directions
val scale = min(width.toDouble() / imgWidth, height.toDouble() / imgHeight)
width = (scale * imgWidth).toInt()
height = (scale * imgHeight).toInt()
}
drawable.setBounds(0, 0, width, height)
compoundDrawableAt(index, drawable)
}
}
private val targets = arrayOfNulls<DrawableTarget>(7)
private fun targetAt(index: Int): CompoundDrawable =
(targets[index] as CompoundDrawable?) ?: CompoundDrawable(index).also { targets[index] = it }
val leftDrawable: CompoundDrawable @JvmName("leftDrawable") get() = targetAt(0)
val topDrawable: CompoundDrawable @JvmName("topDrawable") get() = targetAt(1)
val rightDrawable: CompoundDrawable @JvmName("rightDrawable") get() = targetAt(2)
val bottomDrawable: CompoundDrawable @JvmName("bottomDrawable") get() = targetAt(3)
val startDrawable: CompoundDrawable @JvmName("startDrawable") get() = targetAt(4)
val endDrawable: CompoundDrawable @JvmName("endDrawable") get() = targetAt(5)
val backgroundDrawable: DrawableTarget
@JvmName("backgroundDrawable") get() =
targets[6] ?:
object : DrawableTarget() {
override fun getDrawable(): Drawable? = background
override fun setDrawable(drawable: Drawable?) { background = drawable }
}.also { targets[6] = it }
private fun compoundDrawableAt(index: Int): Drawable? =
if (index < 4) compoundDrawables[index] else compoundDrawablesRelative[2 * (index - 4)]
private fun compoundDrawableAt(index: Int, drawable: Drawable?): Unit =
if (index < 4) compoundDrawables.let {
it[index] = drawable
setCompoundDrawables(it[0], it[1], it[2], it[3])
} else compoundDrawablesRelative.let {
it[2 * (index - 4)] = drawable
setCompoundDrawablesRelative(it[0], it[1], it[2], it[3])
}
}
@OptIn(ExperimentalContracts::class)
inline fun Context.picassoTextView(init: PicassoTextView.() -> Unit = {}): PicassoTextView {
contract { callsInPlace(init, InvocationKind.EXACTLY_ONCE) }
return PicassoTextView(this).apply(init)
}
@OptIn(ExperimentalContracts::class)
inline fun View.picassoTextView(init: PicassoTextView.() -> Unit = {}): PicassoTextView {
contract { callsInPlace(init, InvocationKind.EXACTLY_ONCE) }
return PicassoTextView(context).apply(init)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment