Last active
April 26, 2021 18:48
-
-
Save Miha-x64/c3b0cff4f38690455d85d5c415024ebe to your computer and use it in GitHub Desktop.
A TextView with Picasso targets for compound drawables
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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