fix: Show sized placeholder for hidden account media (#516)

Previous code showed a small icon for account media that the user has
hidden.

Now determine the correct size / aspect ratio for the media and use that
to compute the placeholder (either a blurhash, or the link colour for
consistency with the view on a timeline).

Fixes #513
This commit is contained in:
Nik Clayton 2024-03-10 23:13:58 +01:00 committed by GitHub
parent bdf2d9329e
commit a4dc3b85bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 80 additions and 23 deletions

View File

@ -33,7 +33,6 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager.VERTICAL import androidx.recyclerview.widget.StaggeredGridLayoutManager.VERTICAL
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import app.pachli.R import app.pachli.R
import app.pachli.core.accounts.AccountManager
import app.pachli.core.activity.RefreshableFragment import app.pachli.core.activity.RefreshableFragment
import app.pachli.core.activity.openLink import app.pachli.core.activity.openLink
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
@ -43,8 +42,6 @@ import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.navigation.ViewMediaActivityIntent
import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Attachment
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.databinding.FragmentTimelineBinding import app.pachli.databinding.FragmentTimelineBinding
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
@ -52,7 +49,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -66,12 +62,6 @@ class AccountMediaFragment :
RefreshableFragment, RefreshableFragment,
MenuProvider { MenuProvider {
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var sharedPreferencesRepository: SharedPreferencesRepository
private val binding by viewBinding(FragmentTimelineBinding::bind) private val binding by viewBinding(FragmentTimelineBinding::bind)
private val viewModel: AccountMediaViewModel by viewModels() private val viewModel: AccountMediaViewModel by viewModels()
@ -86,11 +76,9 @@ class AccountMediaFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
val useBlurhash = sharedPreferencesRepository.getBoolean(PrefKeys.USE_BLURHASH, true)
adapter = AccountMediaGridAdapter( adapter = AccountMediaGridAdapter(
useBlurhash = useBlurhash,
context = view.context, context = view.context,
statusDisplayOptions = viewModel.statusDisplayOptions.value,
onAttachmentClickListener = ::onAttachmentClick, onAttachmentClickListener = ::onAttachmentClick,
) )

View File

@ -1,6 +1,8 @@
package app.pachli.components.account.media package app.pachli.components.account.media
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.PaintDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -14,15 +16,18 @@ import app.pachli.core.activity.decodeBlurHash
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.visible import app.pachli.core.common.extensions.visible
import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.model.Attachment
import app.pachli.databinding.ItemAccountMediaBinding import app.pachli.databinding.ItemAccountMediaBinding
import app.pachli.util.BindingHolder import app.pachli.util.BindingHolder
import app.pachli.util.StatusDisplayOptions
import app.pachli.util.getFormattedDescription import app.pachli.util.getFormattedDescription
import app.pachli.util.iconResource import app.pachli.util.iconResource
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
class AccountMediaGridAdapter( class AccountMediaGridAdapter(
private val useBlurhash: Boolean,
context: Context, context: Context,
statusDisplayOptions: StatusDisplayOptions,
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit, private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit,
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>( ) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
object : DiffUtil.ItemCallback<AttachmentViewData>() { object : DiffUtil.ItemCallback<AttachmentViewData>() {
@ -35,10 +40,17 @@ class AccountMediaGridAdapter(
} }
}, },
) { ) {
var statusDisplayOptions = statusDisplayOptions
set(value) {
field = value
notifyItemRangeChanged(0, itemCount)
}
private val playableIcon = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator) private val playableIcon = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp) private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
val defaultSize = context.resources.getDimensionPixelSize(app.pachli.core.designsystem.R.dimen.account_media_grid_default)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding) return BindingHolder(binding)
@ -49,17 +61,17 @@ class AccountMediaGridAdapter(
val context = root.context val context = root.context
val placeholder = item.attachment.blurhash?.let {
if (useBlurhash) decodeBlurHash(context, it) else null
}
when { when {
item.sensitive && !item.isRevealed -> { item.sensitive && !item.isRevealed -> {
overlay.show() overlay.show()
overlay.setImageDrawable(mediaHiddenDrawable) overlay.setImageDrawable(mediaHiddenDrawable)
overlay.setBackgroundResource(R.drawable.media_warning_bg)
val (placeholder, width, height) = item.attachment.placeholder(context, preview)
Glide.with(preview) Glide.with(preview)
.load(placeholder) .load(placeholder)
.override(width, height)
.centerInside() .centerInside()
.into(preview) .into(preview)
@ -68,8 +80,11 @@ class AccountMediaGridAdapter(
item.attachment.isPreviewable() -> { item.attachment.isPreviewable() -> {
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
overlay.setBackgroundResource(0)
overlay.visible(item.attachment.type.isPlayable()) overlay.visible(item.attachment.type.isPlayable())
val (placeholder, _, _) = item.attachment.placeholder(context, preview)
Glide.with(preview) Glide.with(preview)
.asBitmap() .asBitmap()
.load(item.attachment.previewUrl) .load(item.attachment.previewUrl)
@ -81,6 +96,7 @@ class AccountMediaGridAdapter(
else -> { else -> {
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
overlay.setBackgroundResource(0)
overlay.visible(item.attachment.type.isPlayable()) overlay.visible(item.attachment.type.isPlayable())
Glide.with(preview) Glide.with(preview)
@ -101,4 +117,50 @@ class AccountMediaGridAdapter(
true true
} }
} }
/**
* Determine the placeholder for this [Attachment].
*
* @return A triple of the [Drawable] that should be used for the placeholder, and the
* width and height to set on the imageview displaying the placeholder.
*/
fun Attachment.placeholder(context: Context, view: View): Triple<Drawable?, Int, Int> {
// To avoid the list jumping when the user taps the placeholder to reveal the media
// the placeholder must have the same size as the underlying preview.
//
// To do this take the height and width of the `small` image from the attachment
// metadata. If height doesn't exist / is null try and compute it from the width and
// the aspect ratio, falling back to 100 if both are missing.
//
// Do the same to compute the width.
val height = when {
meta?.small?.height != null -> meta?.small?.height!!
meta?.small?.width != null && meta?.small?.aspect != null ->
(meta?.small?.width!! / meta?.small?.aspect!!).toInt()
else -> defaultSize
}
val width = when {
meta?.small?.width != null -> meta?.small?.width!!
meta?.small?.aspect != null -> (height * meta?.small?.aspect!!).toInt()
else -> defaultSize
}
// The drawable's height and width does not need to be as large, as it will be
// automatically scaled by Glide. Set to a max height of 32, and scale the width
// appropriately.
val placeholderHeight = 32
val placeholderWidth = (placeholderHeight * (meta?.small?.aspect ?: 1.0)).toInt()
val placeholder = if (statusDisplayOptions.useBlurhash) {
blurhash?.let { decodeBlurHash(context, it, placeholderWidth, placeholderHeight) }
} else {
PaintDrawable(MaterialColors.getColor(view, android.R.attr.textColorLink)).apply {
intrinsicHeight = placeholderHeight
intrinsicWidth = placeholderWidth
}
}
return Triple(placeholder, width, height)
}
} }

View File

@ -25,6 +25,7 @@ import androidx.paging.cachedIn
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.util.StatusDisplayOptionsRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -32,6 +33,7 @@ import javax.inject.Inject
class AccountMediaViewModel @Inject constructor( class AccountMediaViewModel @Inject constructor(
accountManager: AccountManager, accountManager: AccountManager,
api: MastodonApi, api: MastodonApi,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
) : ViewModel() { ) : ViewModel() {
lateinit var accountId: String lateinit var accountId: String
@ -42,6 +44,8 @@ class AccountMediaViewModel @Inject constructor(
val activeAccount = accountManager.activeAccount!! val activeAccount = accountManager.activeAccount!!
val statusDisplayOptions = statusDisplayOptionsRepository.flow
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
val media = Pager( val media = Pager(
config = PagingConfig( config = PagingConfig(

View File

@ -18,6 +18,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:padding="4dp"
android:importantForAccessibility="no" /> android:importantForAccessibility="no" />
</FrameLayout> </FrameLayout>

View File

@ -69,6 +69,6 @@ fun loadAvatar(
} }
} }
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable { fun decodeBlurHash(context: Context, blurhash: String, width: Int = 32, height: Int = 32): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f)) return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, width, height, 1f))
} }

View File

@ -61,7 +61,7 @@
<dimen name="avatar_toolbar_nav_icon_size">36dp</dimen> <dimen name="avatar_toolbar_nav_icon_size">36dp</dimen>
<dimen name="profile_media_spacing">3dp</dimen> <dimen name="profile_media_spacing">4dp</dimen>
<dimen name="preview_image_spacing">4dp</dimen> <dimen name="preview_image_spacing">4dp</dimen>
@ -72,6 +72,8 @@
<dimen name="timeline_status_avatar_height">48dp</dimen> <dimen name="timeline_status_avatar_height">48dp</dimen>
<dimen name="timeline_status_avatar_width">48dp</dimen> <dimen name="timeline_status_avatar_width">48dp</dimen>
<dimen name="account_media_grid_default">130dp</dimen>
<!-- Adjust the dimensions of items in the Material drawer to provide a <!-- Adjust the dimensions of items in the Material drawer to provide a
slightly tighter layout while ensuring that minimum touch sizes are slightly tighter layout while ensuring that minimum touch sizes are
maintained. maintained.