Pills : finalize avatar retrieval

This commit is contained in:
ganfra 2019-03-04 16:52:44 +01:00
parent fffdf4b8c1
commit ef3fb561e9
4 changed files with 91 additions and 49 deletions

View File

@ -20,11 +20,16 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
@ -32,6 +37,9 @@ import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.glide.GlideRequest import im.vector.riotredesign.core.glide.GlideRequest
import im.vector.riotredesign.core.glide.GlideRequests import im.vector.riotredesign.core.glide.GlideRequests
/**
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
*/
object AvatarRenderer { object AvatarRenderer {
@UiThread @UiThread
@ -46,23 +54,53 @@ object AvatarRenderer {
@UiThread @UiThread
fun render(avatarUrl: String?, name: String?, imageView: ImageView) { fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, imageView.height, DrawableImageViewTarget(imageView))
}
@UiThread
fun render(context: Context,
glideRequest: GlideRequests,
avatarUrl: String?,
name: String?,
size: Int,
target: Target<Drawable>) {
if (name.isNullOrEmpty()) { if (name.isNullOrEmpty()) {
return return
} }
val placeholder = buildPlaceholderDrawable(imageView.context, name) val placeholder = buildPlaceholderDrawable(context, name)
buildGlideRequest(GlideApp.with(imageView), avatarUrl) buildGlideRequest(glideRequest, avatarUrl, size)
.placeholder(placeholder) .placeholder(placeholder)
.into(imageView) .into(target)
} }
fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { @WorkerThread
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) fun getCachedOrPlaceholder(context: Context,
glideRequest: GlideRequests,
avatarUrl: String?,
text: String,
size: Int): Drawable {
val future = buildGlideRequest(glideRequest, avatarUrl, size).onlyRetrieveFromCache(true).submit()
return try {
future.get()
} catch (exception: Exception) {
buildPlaceholderDrawable(context, text)
}
}
// PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest<Drawable> {
val resolvedUrl = Matrix.getInstance().currentSession
.contentUrlResolver()
.resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE)
return glideRequest return glideRequest
.load(resolvedUrl) .load(resolvedUrl)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.diskCacheStrategy(DiskCacheStrategy.DATA)
} }
fun buildPlaceholderDrawable(context: Context, text: String): Drawable { private fun buildPlaceholderDrawable(context: Context, text: String): Drawable {
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
return if (text.isEmpty()) { return if (text.isEmpty()) {
TextDrawable.builder().buildRound("", avatarColor) TextDrawable.builder().buildRound("", avatarColor)

View File

@ -48,11 +48,6 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
findPillsAndProcess { it.bind(holder.messageView) } findPillsAndProcess { it.bind(holder.messageView) }
} }
override fun unbind(holder: Holder) {
findPillsAndProcess { it.unbind() }
super.unbind(holder)
}
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
val pillImageSpans: Array<PillImageSpan>? = withContext(Dispatchers.IO) { val pillImageSpans: Array<PillImageSpan>? = withContext(Dispatchers.IO) {

View File

@ -148,7 +148,7 @@ private class MxLinkHandler(private val glideRequests: GlideRequests,
val permalinkData = PermalinkParser.parse(link) val permalinkData = PermalinkParser.parse(link)
when (permalinkData) { when (permalinkData) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
val user = session.getUser(permalinkData.userId) ?: return val user = session.getUser(permalinkData.userId)
val span = PillImageSpan(glideRequests, context, permalinkData.userId, user) val span = PillImageSpan(glideRequests, context, permalinkData.userId, user)
SpannableBuilder.setSpans( SpannableBuilder.setSpans(
visitor.builder(), visitor.builder(),

View File

@ -22,7 +22,7 @@ import android.graphics.Paint
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.widget.TextView import android.widget.TextView
import androidx.annotation.MainThread import androidx.annotation.UiThread
import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
@ -32,36 +32,33 @@ import im.vector.riotredesign.core.glide.GlideRequests
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
/**
* This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
*/
private const val PILL_AVATAR_SIZE = 80
class PillImageSpan(private val glideRequests: GlideRequests, class PillImageSpan(private val glideRequests: GlideRequests,
private val context: Context, private val context: Context,
private val userId: String, private val userId: String,
private val user: User?) : ReplacementSpan() { private val user: User?) : ReplacementSpan() {
private val pillDrawable = createChipDrawable(context, userId, user) private val displayName by lazy {
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
}
private val pillDrawable = createChipDrawable()
private val target = PillImageSpanTarget(this) private val target = PillImageSpanTarget(this)
private var tv: WeakReference<TextView>? = null private var tv: WeakReference<TextView>? = null
@MainThread @UiThread
fun bind(textView: TextView) { fun bind(textView: TextView) {
tv = WeakReference(textView) tv = WeakReference(textView)
AvatarRenderer.buildGlideRequest(glideRequests, user?.avatarUrl).into(target) AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE, target)
} }
@MainThread // ReplacementSpan *****************************************************************************
fun unbind() {
glideRequests.clear(target)
tv = null
}
@MainThread
private fun updateAvatarDrawable(drawable: Drawable?) {
pillDrawable.apply {
chipIcon = drawable
}
tv?.get()?.apply {
invalidate()
}
}
override fun getSize(paint: Paint, text: CharSequence, override fun getSize(paint: Paint, text: CharSequence,
start: Int, start: Int,
@ -85,7 +82,6 @@ class PillImageSpan(private val glideRequests: GlideRequests,
y: Int, y: Int,
bottom: Int, bottom: Int,
paint: Paint) { paint: Paint) {
canvas.save() canvas.save()
val transY = bottom - pillDrawable.bounds.bottom val transY = bottom - pillDrawable.bounds.bottom
canvas.translate(x, transY.toFloat()) canvas.translate(x, transY.toFloat())
@ -93,37 +89,50 @@ class PillImageSpan(private val glideRequests: GlideRequests,
canvas.restore() canvas.restore()
} }
private fun createChipDrawable(context: Context, userId: String, user: User?): ChipDrawable { internal fun updateAvatarDrawable(drawable: Drawable?) {
pillDrawable.apply {
chipIcon = drawable
}
tv?.get()?.apply {
invalidate()
}
}
// Private methods *****************************************************************************
private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val displayName = if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
setText(displayName) setText(displayName)
textEndPadding = textPadding textEndPadding = textPadding
textStartPadding = textPadding textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height) setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size) setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = AvatarRenderer.buildPlaceholderDrawable(context, displayName) chipIcon = AvatarRenderer.getCachedOrPlaceholder(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE)
setBounds(0, 0, intrinsicWidth, intrinsicHeight) setBounds(0, 0, intrinsicWidth, intrinsicHeight)
} }
} }
private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget<Drawable>() { }
private val pillImageSpan = WeakReference(pillImageSpan) /**
* Glide target to handle avatar retrieval into [PillImageSpan].
*/
private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget<Drawable>() {
override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) { private val pillImageSpan = WeakReference(pillImageSpan)
updateWith(drawable)
}
override fun onLoadCleared(placeholder: Drawable?) { override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
updateWith(placeholder) updateWith(drawable)
}
private fun updateWith(drawable: Drawable?) {
pillImageSpan.get()?.apply {
this.updateAvatarDrawable(drawable)
}
}
} }
override fun onLoadCleared(placeholder: Drawable?) {
updateWith(placeholder)
}
private fun updateWith(drawable: Drawable?) {
pillImageSpan.get()?.apply {
updateAvatarDrawable(drawable)
}
}
} }