From 9071a89e48795b81be4b6fa19f4b73e892eaca72 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 22 Feb 2024 14:33:44 +0100 Subject: [PATCH] feat: Display uncropped media on account media pages (#464) Previously media on the "Media" tab was displayed scaled and cropped to a square aspect ratio, effectively forcing the user to tap every image to see it. Now, display the images scaled but not cropped, layed out with `StaggeredGridLayoutManager`. This shows each image in full (still scaled) providing a better experience when scrolling down. Scrolling up can occasionally introduce gaps in the grid as images are re-placed as viewholders are reused. When this happens images animate to a better position when scrolling stops. --- .../account/media/AccountMediaFragment.kt | 12 +- .../account/media/AccountMediaGridAdapter.kt | 105 ++++++++---------- .../media/GridSpacingItemDecoration.kt | 48 -------- .../account/media/SquareImageView.kt | 20 ---- .../main/res/layout/item_account_media.xml | 16 ++- 5 files changed, 61 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/app/pachli/components/account/media/GridSpacingItemDecoration.kt delete mode 100644 app/src/main/java/app/pachli/components/account/media/SquareImageView.kt diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt index 0ab7ca797..abf0aae52 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt @@ -29,7 +29,8 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState -import androidx.recyclerview.widget.GridLayoutManager +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 @@ -94,11 +95,10 @@ class AccountMediaFragment : ) val columnCount = view.context.resources.getInteger(DR.integer.profile_media_column_count) - val imageSpacing = view.context.resources.getDimensionPixelSize(DR.dimen.profile_media_spacing) - - binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0)) - - binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount) + val layoutManager = StaggeredGridLayoutManager(columnCount, VERTICAL) + layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = adapter binding.swipeRefreshLayout.isEnabled = false diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt index 2940b82b3..313334cba 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt @@ -1,7 +1,6 @@ package app.pachli.components.account.media import android.content.Context -import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -20,8 +19,6 @@ import app.pachli.util.BindingHolder import app.pachli.util.getFormattedDescription import app.pachli.util.iconResource import com.bumptech.glide.Glide -import com.google.android.material.color.MaterialColors -import java.util.Random class AccountMediaGridAdapter( private val useBlurhash: Boolean, @@ -39,81 +36,69 @@ class AccountMediaGridAdapter( }, ) { - private val baseItemBackgroundColor = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK) 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 itemBgBaseHSV = FloatArray(3) - private val random = Random() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false) - Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV) - itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f - binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) return BindingHolder(binding) } - override fun onBindViewHolder(holder: BindingHolder, position: Int) { - val context = holder.binding.root.context + override fun onBindViewHolder(holder: BindingHolder, position: Int) = with(holder.binding) { + val item = getItem(position) ?: return - getItem(position)?.let { item -> - val imageView = holder.binding.accountMediaImageView - val overlay = holder.binding.accountMediaImageViewOverlay + val context = root.context - val placeholder = item.attachment.blurhash?.let { - if (useBlurhash) decodeBlurHash(context, it) else null + val placeholder = item.attachment.blurhash?.let { + if (useBlurhash) decodeBlurHash(context, it) else null + } + + when { + item.sensitive && !item.isRevealed -> { + overlay.show() + overlay.setImageDrawable(mediaHiddenDrawable) + + Glide.with(preview) + .load(placeholder) + .centerInside() + .into(preview) + + preview.contentDescription = context.getString(R.string.post_media_hidden_title) } - when { - item.sensitive && !item.isRevealed -> { - overlay.show() - overlay.setImageDrawable(mediaHiddenDrawable) + item.attachment.isPreviewable() -> { + if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) + overlay.visible(item.attachment.type.isPlayable()) - Glide.with(imageView) - .load(placeholder) - .centerInside() - .into(imageView) + Glide.with(preview) + .asBitmap() + .load(item.attachment.previewUrl) + .placeholder(placeholder) + .into(preview) - imageView.contentDescription = context.getString(R.string.post_media_hidden_title) - } - - item.attachment.isPreviewable() -> { - if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) - overlay.visible(item.attachment.type.isPlayable()) - - Glide.with(imageView) - .asBitmap() - .load(item.attachment.previewUrl) - .placeholder(placeholder) - .centerInside() - .into(imageView) - - imageView.contentDescription = item.attachment.getFormattedDescription(context) - } - - else -> { - if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) - overlay.visible(item.attachment.type.isPlayable()) - - Glide.with(imageView) - .load(item.attachment.iconResource()) - .centerInside() - .into(imageView) - - imageView.contentDescription = item.attachment.getFormattedDescription(context) - } + preview.contentDescription = item.attachment.getFormattedDescription(context) } - holder.binding.root.setOnClickListener { - onAttachmentClickListener(item, imageView) - } + else -> { + if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon) + overlay.visible(item.attachment.type.isPlayable()) - holder.binding.root.setOnLongClickListener { view -> - val description = item.attachment.getFormattedDescription(view.context) - Toast.makeText(context, description, Toast.LENGTH_LONG).show() - true + Glide.with(preview) + .load(item.attachment.iconResource()) + .into(preview) + + preview.contentDescription = item.attachment.getFormattedDescription(context) } } + + root.setOnClickListener { + onAttachmentClickListener(item, preview) + } + + root.setOnLongClickListener { view -> + val description = item.attachment.getFormattedDescription(view.context) + Toast.makeText(context, description, Toast.LENGTH_LONG).show() + true + } } } diff --git a/app/src/main/java/app/pachli/components/account/media/GridSpacingItemDecoration.kt b/app/src/main/java/app/pachli/components/account/media/GridSpacingItemDecoration.kt deleted file mode 100644 index 32adf11e3..000000000 --- a/app/src/main/java/app/pachli/components/account/media/GridSpacingItemDecoration.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright 2022 Tusky Contributors - * - * This file is a part of Pachli. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Pachli; if not, - * see . - */ - -package app.pachli.components.account.media - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ItemDecoration - -class GridSpacingItemDecoration( - private val spanCount: Int, - private val spacing: Int, - private val topOffset: Int, -) : ItemDecoration() { - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State, - ) { - val position = parent.getChildAdapterPosition(view) // item position - if (position < topOffset) return - - val column = (position - topOffset) % spanCount // item column - - outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing) - outRect.right = - spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) - if (position - topOffset >= spanCount) { - outRect.top = spacing // item top - } - } -} diff --git a/app/src/main/java/app/pachli/components/account/media/SquareImageView.kt b/app/src/main/java/app/pachli/components/account/media/SquareImageView.kt deleted file mode 100644 index e00387856..000000000 --- a/app/src/main/java/app/pachli/components/account/media/SquareImageView.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.pachli.components.account.media - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatImageView - -class SquareImageView : AppCompatImageView { - constructor(context: Context) : super(context) - - constructor(context: Context, attributes: AttributeSet) : super(context, attributes) - - constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) : - super(context, attributes, defStyleAttr) - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val width = measuredWidth - setMeasuredDimension(width, width) - } -} diff --git a/app/src/main/res/layout/item_account_media.xml b/app/src/main/res/layout/item_account_media.xml index 731d3dcfe..ef5b16ee1 100644 --- a/app/src/main/res/layout/item_account_media.xml +++ b/app/src/main/res/layout/item_account_media.xml @@ -1,16 +1,20 @@ + android:layout_height="wrap_content" + android:padding="@dimen/profile_media_spacing"> - + + android:layout_height="match_parent" + android:adjustViewBounds="true" + tools:ignore="ContentDescription" />