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.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import app.pachli.R
import app.pachli.core.accounts.AccountManager
import app.pachli.core.activity.RefreshableFragment
import app.pachli.core.activity.openLink
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.ViewMediaActivityIntent
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 com.google.android.material.color.MaterialColors
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.sizeDp
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -66,12 +62,6 @@ class AccountMediaFragment :
RefreshableFragment,
MenuProvider {
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var sharedPreferencesRepository: SharedPreferencesRepository
private val binding by viewBinding(FragmentTimelineBinding::bind)
private val viewModel: AccountMediaViewModel by viewModels()
@ -86,11 +76,9 @@ class AccountMediaFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
val useBlurhash = sharedPreferencesRepository.getBoolean(PrefKeys.USE_BLURHASH, true)
adapter = AccountMediaGridAdapter(
useBlurhash = useBlurhash,
context = view.context,
statusDisplayOptions = viewModel.statusDisplayOptions.value,
onAttachmentClickListener = ::onAttachmentClick,
)

View File

@ -1,6 +1,8 @@
package app.pachli.components.account.media
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.PaintDrawable
import android.view.LayoutInflater
import android.view.View
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.visible
import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.model.Attachment
import app.pachli.databinding.ItemAccountMediaBinding
import app.pachli.util.BindingHolder
import app.pachli.util.StatusDisplayOptions
import app.pachli.util.getFormattedDescription
import app.pachli.util.iconResource
import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
class AccountMediaGridAdapter(
private val useBlurhash: Boolean,
context: Context,
statusDisplayOptions: StatusDisplayOptions,
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit,
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
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 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> {
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
@ -49,17 +61,17 @@ class AccountMediaGridAdapter(
val context = root.context
val placeholder = item.attachment.blurhash?.let {
if (useBlurhash) decodeBlurHash(context, it) else null
}
when {
item.sensitive && !item.isRevealed -> {
overlay.show()
overlay.setImageDrawable(mediaHiddenDrawable)
overlay.setBackgroundResource(R.drawable.media_warning_bg)
val (placeholder, width, height) = item.attachment.placeholder(context, preview)
Glide.with(preview)
.load(placeholder)
.override(width, height)
.centerInside()
.into(preview)
@ -68,8 +80,11 @@ class AccountMediaGridAdapter(
item.attachment.isPreviewable() -> {
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
overlay.setBackgroundResource(0)
overlay.visible(item.attachment.type.isPlayable())
val (placeholder, _, _) = item.attachment.placeholder(context, preview)
Glide.with(preview)
.asBitmap()
.load(item.attachment.previewUrl)
@ -81,6 +96,7 @@ class AccountMediaGridAdapter(
else -> {
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
overlay.setBackgroundResource(0)
overlay.visible(item.attachment.type.isPlayable())
Glide.with(preview)
@ -101,4 +117,50 @@ class AccountMediaGridAdapter(
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.navigation.AttachmentViewData
import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.util.StatusDisplayOptionsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -32,6 +33,7 @@ import javax.inject.Inject
class AccountMediaViewModel @Inject constructor(
accountManager: AccountManager,
api: MastodonApi,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
) : ViewModel() {
lateinit var accountId: String
@ -42,6 +44,8 @@ class AccountMediaViewModel @Inject constructor(
val activeAccount = accountManager.activeAccount!!
val statusDisplayOptions = statusDisplayOptionsRepository.flow
@OptIn(ExperimentalPagingApi::class)
val media = Pager(
config = PagingConfig(

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="7dp" />
<solid android:color="@color/color_background_transparent_60" />
</shape>
<solid android:color="@color/color_background_transparent_60" />
</shape>

View File

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

View File

@ -69,6 +69,6 @@ fun loadAvatar(
}
}
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
fun decodeBlurHash(context: Context, blurhash: String, width: Int = 32, height: Int = 32): BitmapDrawable {
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="profile_media_spacing">3dp</dimen>
<dimen name="profile_media_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_width">48dp</dimen>
<dimen name="account_media_grid_default">130dp</dimen>
<!-- Adjust the dimensions of items in the Material drawer to provide a
slightly tighter layout while ensuring that minimum touch sizes are
maintained.