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.
This commit is contained in:
parent
203784d718
commit
9071a89e48
|
@ -29,7 +29,8 @@ import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.LoadState
|
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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.core.accounts.AccountManager
|
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 columnCount = view.context.resources.getInteger(DR.integer.profile_media_column_count)
|
||||||
val imageSpacing = view.context.resources.getDimensionPixelSize(DR.dimen.profile_media_spacing)
|
val layoutManager = StaggeredGridLayoutManager(columnCount, VERTICAL)
|
||||||
|
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||||
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0))
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
binding.swipeRefreshLayout.isEnabled = false
|
binding.swipeRefreshLayout.isEnabled = false
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package app.pachli.components.account.media
|
package app.pachli.components.account.media
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -20,8 +19,6 @@ import app.pachli.util.BindingHolder
|
||||||
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
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
class AccountMediaGridAdapter(
|
class AccountMediaGridAdapter(
|
||||||
private val useBlurhash: Boolean,
|
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 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)
|
||||||
|
|
||||||
private val itemBgBaseHSV = FloatArray(3)
|
|
||||||
private val random = Random()
|
|
||||||
|
|
||||||
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)
|
||||||
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
|
||||||
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
|
|
||||||
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemAccountMediaBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemAccountMediaBinding>, position: Int) = with(holder.binding) {
|
||||||
val context = holder.binding.root.context
|
val item = getItem(position) ?: return
|
||||||
|
|
||||||
getItem(position)?.let { item ->
|
val context = root.context
|
||||||
val imageView = holder.binding.accountMediaImageView
|
|
||||||
val overlay = holder.binding.accountMediaImageViewOverlay
|
|
||||||
|
|
||||||
val placeholder = item.attachment.blurhash?.let {
|
val placeholder = item.attachment.blurhash?.let {
|
||||||
if (useBlurhash) decodeBlurHash(context, it) else null
|
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.attachment.isPreviewable() -> {
|
||||||
item.sensitive && !item.isRevealed -> {
|
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
|
||||||
overlay.show()
|
overlay.visible(item.attachment.type.isPlayable())
|
||||||
overlay.setImageDrawable(mediaHiddenDrawable)
|
|
||||||
|
|
||||||
Glide.with(imageView)
|
Glide.with(preview)
|
||||||
.load(placeholder)
|
.asBitmap()
|
||||||
.centerInside()
|
.load(item.attachment.previewUrl)
|
||||||
.into(imageView)
|
.placeholder(placeholder)
|
||||||
|
.into(preview)
|
||||||
|
|
||||||
imageView.contentDescription = context.getString(R.string.post_media_hidden_title)
|
preview.contentDescription = item.attachment.getFormattedDescription(context)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.root.setOnClickListener {
|
else -> {
|
||||||
onAttachmentClickListener(item, imageView)
|
if (item.attachment.type.isPlayable()) overlay.setImageDrawable(playableIcon)
|
||||||
}
|
overlay.visible(item.attachment.type.isPlayable())
|
||||||
|
|
||||||
holder.binding.root.setOnLongClickListener { view ->
|
Glide.with(preview)
|
||||||
val description = item.attachment.getFormattedDescription(view.context)
|
.load(item.attachment.iconResource())
|
||||||
Toast.makeText(context, description, Toast.LENGTH_LONG).show()
|
.into(preview)
|
||||||
true
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,20 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/profile_media_spacing">
|
||||||
|
|
||||||
<app.pachli.components.account.media.SquareImageView
|
<!-- content description is set in code -->
|
||||||
android:id="@+id/accountMediaImageView"
|
<ImageView
|
||||||
|
android:id="@+id/preview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:scaleType="centerCrop" />
|
android:adjustViewBounds="true"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/accountMediaImageViewOverlay"
|
android:id="@+id/overlay"
|
||||||
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"
|
||||||
|
|
Loading…
Reference in New Issue