playback: improve queue ui
Improve the queue UI some more: - Fixed an issue where clearing the user queue by clearing all items would result in it bugging out - Queue items now show a Material-y background when they are swiped away. This was way harder than you might think it was.
This commit is contained in:
parent
9fc56c1c3d
commit
2d5c438c58
10 changed files with 137 additions and 81 deletions
|
@ -60,7 +60,6 @@ import org.oxycblt.auxio.util.makeScrollingViewFade
|
|||
* - Edge-to-edge is borked still, unsure how to really fix this aside from making some
|
||||
* magic layout like Material Files, but even then it might not work since the scrolling
|
||||
* views are not laid side-by-side to the layout itself.
|
||||
* So excited to have enough time to get to these in like...november.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class HomeFragment : Fragment() {
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback.queue
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
|
@ -35,6 +36,7 @@ import org.oxycblt.auxio.ui.DiffCallback
|
|||
import org.oxycblt.auxio.ui.HeaderViewHolder
|
||||
import org.oxycblt.auxio.util.applyMaterialDrawable
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
||||
/**
|
||||
|
@ -113,6 +115,8 @@ class QueueAdapter(
|
|||
fun removeItem(adapterIndex: Int) {
|
||||
data.removeAt(adapterIndex)
|
||||
|
||||
logD(data)
|
||||
|
||||
/*
|
||||
* If the data from the next queue is now entirely empty [Signified by a header at the
|
||||
* end, remove the next queue header as notify as such.
|
||||
|
@ -123,14 +127,15 @@ class QueueAdapter(
|
|||
* Otherwise just remove the item as usual.
|
||||
*/
|
||||
if (data[data.lastIndex] is Header) {
|
||||
logD("Queue is empty, removing header")
|
||||
|
||||
val lastIndex = data.lastIndex
|
||||
|
||||
data.removeAt(lastIndex)
|
||||
|
||||
notifyItemRangeRemoved(lastIndex, 2)
|
||||
} else if (data.lastIndex >= 1 && data[0] is Header && data[1] is Header) {
|
||||
data.removeAt(0)
|
||||
} else if (data.lastIndex >= 1 && data[0] is ActionHeader && data[1] is Header) {
|
||||
logD("User queue is empty, removing header")
|
||||
|
||||
data.removeAt(0)
|
||||
notifyItemRangeRemoved(0, 2)
|
||||
} else {
|
||||
notifyItemRemoved(adapterIndex)
|
||||
|
@ -143,9 +148,11 @@ class QueueAdapter(
|
|||
inner class QueueSongViewHolder(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
) : BaseViewHolder<Song>(binding) {
|
||||
val bodyView: View get() = binding.body
|
||||
val backgroundView: View get() = binding.background
|
||||
|
||||
init {
|
||||
binding.root.applyMaterialDrawable()
|
||||
binding.body.applyMaterialDrawable()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.auxio.playback.queue
|
|||
|
||||
import android.graphics.Canvas
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
|
@ -31,8 +32,9 @@ import kotlin.math.min
|
|||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* A highly customized [ItemTouchHelper.Callback] that handles queue item moving, removal, and some
|
||||
* of the UI magic that makes up the queue UI.
|
||||
* A highly customized [ItemTouchHelper.Callback] that handles the queue system while basically
|
||||
* rebuilding most the "Material-y" aspects of an editable list because Google's implementations
|
||||
* are hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||
|
@ -88,48 +90,63 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
// themselves when being dragged. Too bad google's implementation of this doesn't even
|
||||
// work! To emulate it on my own, I check if this child is in a drag state and then animate
|
||||
// an elevation change.
|
||||
// TODO: Maybe restrict the item from being drawn over the recycler bounds?
|
||||
// Seems like its possible with enough UI magic
|
||||
// TODO: Add an accented BG to the removal action
|
||||
// TODO: Some other enhancements I could make maybe
|
||||
// - Maybe stopping dragged items from extending beyond their specific part of the queue?
|
||||
|
||||
val view = viewHolder.itemView
|
||||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
|
||||
if (shouldLift && isCurrentlyActive && actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
val bg = view.background as MaterialShapeDrawable
|
||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
||||
val elevation = recyclerView.resources.getDimension(R.dimen.elevation_small)
|
||||
|
||||
view.animate()
|
||||
.translationZ(view.resources.getDimension(R.dimen.elevation_small))
|
||||
holder.itemView.animate()
|
||||
.translationZ(elevation)
|
||||
.setDuration(100)
|
||||
.setUpdateListener { bg.elevation = view.translationZ }
|
||||
.setUpdateListener {
|
||||
bg.elevation = holder.itemView.translationZ
|
||||
}
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
shouldLift = false
|
||||
}
|
||||
|
||||
view.translationX = dX
|
||||
view.translationY = dY
|
||||
// We show a background with a clear icon behind the queue song each time one is swiped
|
||||
// away. To avoid any canvas shenanigans, we just place a custom background view behind the
|
||||
// main "body" layout of the queue item and then translate that.
|
||||
//
|
||||
// That comes with a couple of problems, however. For one, the background view will always
|
||||
// lag behind the body view, resulting in a noticeable pixel offset when dragging. To fix
|
||||
// this, we make this a separate view and make this view invisible whenever the item is
|
||||
// not being swiped. We cannot merge this view with the FrameLayout, as that will cause
|
||||
// another weird pixel desync issue that is less visible but still incredibly annoying.
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
holder.backgroundView.isInvisible = dX == 0f
|
||||
}
|
||||
|
||||
holder.bodyView.translationX = dX
|
||||
holder.itemView.translationY = dY
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
// When an elevated item is cleared, we reset the elevation using another animation.
|
||||
val view = viewHolder.itemView
|
||||
val holder = viewHolder as QueueAdapter.QueueSongViewHolder
|
||||
|
||||
if (view.translationZ != 0.0f) {
|
||||
val bg = view.background as MaterialShapeDrawable
|
||||
if (holder.itemView.translationZ != 0.0f) {
|
||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
||||
|
||||
view.animate()
|
||||
holder.itemView.animate()
|
||||
.translationZ(0.0f)
|
||||
.setDuration(100)
|
||||
.setUpdateListener { bg.elevation = view.translationZ }
|
||||
.setUpdateListener { bg.elevation = holder.itemView.translationZ }
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
|
||||
shouldLift = true
|
||||
|
||||
view.translationX = 0f
|
||||
view.translationY = 0f
|
||||
holder.bodyView.translationX = 0f
|
||||
holder.itemView.translationY = 0f
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
|
|
|
@ -34,8 +34,6 @@ import org.oxycblt.auxio.util.logE
|
|||
* will not properly respond to RecyclerView events.
|
||||
* **Note:** This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what
|
||||
* scrolling view to use. Failure to specify this will result in the layout not working.
|
||||
* FIXME: Fix issue where elevation change will always animate
|
||||
* FIXME: Fix issue where expanded state does not work correctly when switching orientations
|
||||
*/
|
||||
class LiftAppBarLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.view.WindowInsets
|
|||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -111,6 +112,15 @@ fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
|
|||
fun @receiver:ColorRes Int.resolveStateList(context: Context) =
|
||||
ContextCompat.getColorStateList(context, this)
|
||||
|
||||
/*
|
||||
* Resolve a color and turn it into a [ColorStateList]
|
||||
* @param context [Context] required
|
||||
* @return The resolved color as a [ColorStateList]
|
||||
* @see resolveColor
|
||||
*/
|
||||
fun @receiver:DrawableRes Int.resolveDrawable(context: Context) =
|
||||
requireNotNull(ContextCompat.getDrawable(context, this))
|
||||
|
||||
/**
|
||||
* Resolve this int into a color as if it was an attribute
|
||||
*/
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#111111"
|
||||
android:strokeColor="#00000000"
|
||||
android:pathData="M0 0h108v108H0z" />
|
||||
</vector>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M54 29.83v28.333c-1.585-0.913-3.41-1.477-5.371-1.477-5.935 0-10.743 4.807-10.743 10.743 0 5.935 4.807 10.742 10.743 10.742 5.935 0 10.743-4.807 10.743-10.742V40.573h10.742V29.831z"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillColor="@color/fill_icon_fg">
|
||||
</path>
|
||||
</vector>
|
||||
|
|
|
@ -11,7 +11,29 @@
|
|||
type="org.oxycblt.auxio.music.Song" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<View
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_clear"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:padding="@dimen/spacing_medium"
|
||||
app:tint="?attr/colorSurface"
|
||||
android:contentDescription="@string/desc_clear_queue_item"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface">
|
||||
|
@ -58,13 +80,13 @@
|
|||
android:id="@+id/song_drag_handle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/desc_queue_handle"
|
||||
android:focusable="true"
|
||||
android:minWidth="@dimen/size_btn_small"
|
||||
android:minHeight="@dimen/size_btn_small"
|
||||
android:paddingStart="@dimen/spacing_medium"
|
||||
android:paddingEnd="@dimen/spacing_medium"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/desc_queue_handle"
|
||||
android:focusable="true"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_handle"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_cover"
|
||||
|
@ -72,4 +94,5 @@
|
|||
app:layout_constraintTop_toTopOf="@+id/song_name" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
</layout>
|
|
@ -119,6 +119,7 @@
|
|||
<string name="desc_shuffle">Turn shuffle on or off</string>
|
||||
|
||||
<string name="desc_clear_user_queue">Clear queue</string>
|
||||
<string name="desc_clear_queue_item">Remove this queue item</string>
|
||||
<string name="desc_queue_handle">Move queue song</string>
|
||||
<string name="desc_clear_search">Clear search query</string>
|
||||
<string name="desc_blacklist_delete">Remove excluded directory</string>
|
||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||
|
||||
|
|
Loading…
Reference in a new issue