diff --git a/app/build.gradle b/app/build.gradle
index 9bac620b2..d634d7a01 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -88,8 +88,9 @@ dependencies {
// Material
implementation 'com.google.android.material:material:1.3.0-alpha03'
- // Fast-Scroll [Too lazy to make it myself]
+ // Fast-Scroll
implementation 'com.reddit:indicator-fast-scroll:1.3.0'
+
// --- DEV ---
// Lint
diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/NoLeakThumbView.kt b/app/src/main/java/org/oxycblt/auxio/recycler/NoLeakThumbView.kt
new file mode 100644
index 000000000..d14e7fdff
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/recycler/NoLeakThumbView.kt
@@ -0,0 +1,165 @@
+package org.oxycblt.auxio.recycler
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.core.widget.TextViewCompat
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.reddit.indicatorfastscroll.FastScrollItemIndicator
+import com.reddit.indicatorfastscroll.FastScrollerView
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.ui.accent
+import org.oxycblt.auxio.ui.toColor
+
+/**
+ * A source code copy of [com.reddit.indicatorfastscroll.FastScrollerThumbView] that fixes a
+ * memory leak that occurs from having nested fragments. All credit goes to the authors of
+ * the fast scroll library.
+ * Link to repo
+ * @author Reddit, OxygenCobalt
+ */
+class NoLeakThumbView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = R.attr.indicatorFastScrollerThumbStyle
+) : ConstraintLayout(
+ context,
+ attrs,
+ defStyleAttr
+),
+ FastScrollerView.ItemIndicatorSelectedCallback {
+
+ private var thumbColor = ColorStateList.valueOf(accent.first.toColor(context))
+ var iconColor = R.color.background.toColor(context)
+ var textAppearanceRes = R.style.TextAppearance_ThumbIndicator
+ var textColor = R.color.background.toColor(context)
+
+ private val thumbView: ViewGroup
+ private val textView: TextView
+ private val iconView: ImageView
+
+ private val isSetup: Boolean get() = (fastScrollerView != null)
+ private var fastScrollerView: FastScrollerView? = null
+
+ private val thumbAnimation: SpringAnimation
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.fast_scroller_thumb_view, this, true)
+ thumbView = findViewById(R.id.fast_scroller_thumb)
+ textView = thumbView.findViewById(R.id.fast_scroller_thumb_text)
+ iconView = thumbView.findViewById(R.id.fast_scroller_thumb_icon)
+
+ applyStyle()
+
+ thumbAnimation = SpringAnimation(thumbView, DynamicAnimation.TRANSLATION_Y).apply {
+ spring = SpringForce().apply {
+ dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+ }
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ fun setupWithFastScroller(fastScrollerView: FastScrollerView) {
+ check(!isSetup) { "Only set this view's FastScrollerView once!" }
+ this.fastScrollerView = fastScrollerView
+
+ fastScrollerView.itemIndicatorSelectedCallbacks += this
+
+ // FastScrollerView's "onItemIndicatorTouched" [Which I would've used here] is internal,
+ // so instead I just use a setOnTouchListener to get the same-ish effect.
+ fastScrollerView.setOnTouchListener { v, event ->
+ fastScrollerView.onTouchEvent(event)
+ fastScrollerView.performClick()
+
+ if (event.actionMasked in intArrayOf(MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL)) {
+ isActivated = false
+ return@setOnTouchListener true
+ }
+
+ isActivated = isPointerOnItem(fastScrollerView, event.y.toInt())
+
+ true
+ }
+ }
+
+ /**
+ * Hack so that I can detect when the pointer is off the FastScrollerView's items
+ * without using onItemIndicatorTouched [Which is internal]
+ * @author OxygenCobalt
+ */
+ private fun isPointerOnItem(fastScrollerView: FastScrollerView, touchY: Int): Boolean {
+ fun View.containsY(y: Int) = y in (top until bottom)
+
+ var consumed = false
+
+ fastScrollerView.apply {
+ children.forEach { view ->
+ if (view.containsY(touchY)) {
+ when (view) {
+ is ImageView -> {
+ consumed = true
+ }
+ is TextView -> {
+ consumed = true
+ }
+ }
+ }
+ }
+ }
+
+ return consumed
+ }
+
+ private fun applyStyle() {
+ thumbView.backgroundTintList = thumbColor
+ if (Build.VERSION.SDK_INT == 21) {
+ // Workaround for 21 background tint bug
+ (thumbView.background as GradientDrawable).apply {
+ mutate()
+ color = thumbColor
+ }
+ }
+
+ TextViewCompat.setTextAppearance(textView, textAppearanceRes)
+ textView.setTextColor(textColor)
+ iconView.imageTintList = ColorStateList.valueOf(iconColor)
+ }
+
+ override fun onItemIndicatorSelected(
+ indicator: FastScrollItemIndicator,
+ indicatorCenterY: Int,
+ itemPosition: Int
+ ) {
+ val thumbTargetY = indicatorCenterY.toFloat() - (thumbView.measuredHeight / 2)
+ thumbAnimation.animateToFinalPosition(thumbTargetY)
+
+ when (indicator) {
+ is FastScrollItemIndicator.Text -> {
+ textView.isVisible = true
+ iconView.isVisible = false
+
+ textView.text = indicator.text
+ }
+ is FastScrollItemIndicator.Icon -> {
+ textView.isVisible = false
+ iconView.isVisible = true
+
+ iconView.setImageResource(indicator.iconRes)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt
index b468d65bd..520bddbaf 100644
--- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt
+++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt
@@ -5,7 +5,13 @@ import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.BaseModel
-// ViewHolder abstraction that automates some of the things that are common for all ViewHolders.
+/**
+ * A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders.
+ * @property baseBinding Basic [ViewDataBinding] required to set up click listeners & sizing.
+ * @property doOnClick Function that specifies what to do when an item is clicked. Specify null if you want no action to occur.
+ * @property doOnLongClick Function that specifies what to do when an item is long clicked. Specify null if you want no action to occur.
+ * @author OxygenCobalt
+ */
abstract class BaseViewHolder(
private val baseBinding: ViewDataBinding,
private val doOnClick: ((data: T) -> Unit)?,
@@ -18,6 +24,11 @@ abstract class BaseViewHolder(
)
}
+ /**
+ * Bind the viewholder with whatever [BaseModel] instance that has been specified.
+ * Will call [onBind] on the inheriting ViewHolder.
+ * @param data Data that the viewholder should be binded with
+ */
fun bind(data: T) {
doOnClick?.let { onClick ->
baseBinding.root.setOnClickListener {
@@ -38,5 +49,9 @@ abstract class BaseViewHolder(
baseBinding.executePendingBindings()
}
+ /**
+ * Function that performs binding operations unique to the inheriting viewholder.
+ * Add any specialized code to an override of this instead of [BaseViewHolder] itself.
+ */
protected abstract fun onBind(data: T)
}
diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt
index cf49471b0..6c0addb18 100644
--- a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt
+++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt
@@ -1,24 +1,38 @@
package org.oxycblt.auxio.songs
+import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
+import org.oxycblt.auxio.databinding.ItemBasicSongBinding
import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
+import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
class SongsAdapter(
private val data: List,
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> Unit
-) : RecyclerView.Adapter() {
+) : RecyclerView.Adapter() {
override fun getItemCount(): Int = data.size
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
- return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(ItemBasicSongBinding.inflate(LayoutInflater.from(parent.context)))
}
- override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
+
+ inner class ViewHolder(
+ private val binding: ItemBasicSongBinding
+ ) : BaseViewHolder(binding, doOnClick, doOnLongClick) {
+
+ override fun onBind(data: Song) {
+ binding.song = data
+
+ binding.songName.requestLayout()
+ binding.songInfo.requestLayout()
+ }
+ }
}
diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt
index 560dbbf26..5e49f8401 100644
--- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt
@@ -1,12 +1,12 @@
package org.oxycblt.auxio.songs
-import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
+import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
@@ -17,16 +17,14 @@ import org.oxycblt.auxio.databinding.FragmentSongsBinding
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
-import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.setupSongActions
-import org.oxycblt.auxio.ui.toColor
/**
* A [Fragment] that shows a list of all songs on the device. Contains options to search/shuffle
* them.
* @author OxygenCobalt
*/
-class SongsFragment : Fragment() {
+class SongsFragment : Fragment(), SearchView.OnQueryTextListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView(
@@ -50,11 +48,13 @@ class SongsFragment : Fragment() {
// --- UI SETUP ---
- binding.songToolbar.setOnMenuItemClickListener {
- if (it.itemId == R.id.action_shuffle) {
- playbackModel.shuffleAll()
+ binding.songToolbar.apply {
+ setOnMenuItemClickListener {
+ if (it.itemId == R.id.action_shuffle) {
+ playbackModel.shuffleAll()
+ }
+ true
}
- true
}
binding.songRecycler.apply {
@@ -69,6 +69,14 @@ class SongsFragment : Fragment() {
return binding.root
}
+ override fun onQueryTextChange(newText: String?): Boolean {
+ return false
+ }
+
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return false
+ }
+
private fun setupFastScroller(binding: FragmentSongsBinding) {
val musicStore = MusicStore.getInstance()
@@ -105,6 +113,8 @@ class SongsFragment : Fragment() {
if (char.isDigit()) {
if (!hasAddedNumber) {
hasAddedNumber = true
+
+ return@setupWithRecyclerView FastScrollItemIndicator.Text("#")
} else {
return@setupWithRecyclerView null
}
@@ -117,23 +127,20 @@ class SongsFragment : Fragment() {
}
)
- textAppearanceRes = R.style.TextAppearance_FastScroll
- textColor = ColorStateList.valueOf(accent.first.toColor(requireContext()))
useDefaultScroller = false
- itemIndicatorSelectedCallbacks.add(
- object : FastScrollerView.ItemIndicatorSelectedCallback {
- override fun onItemIndicatorSelected(
- indicator: FastScrollItemIndicator,
- indicatorCenterY: Int,
- itemPosition: Int
- ) {
- val layoutManager = binding.songRecycler.layoutManager
- as LinearLayoutManager
+ itemIndicatorSelectedCallbacks.add(object : FastScrollerView.ItemIndicatorSelectedCallback {
+ override fun onItemIndicatorSelected(
+ indicator: FastScrollItemIndicator,
+ indicatorCenterY: Int,
+ itemPosition: Int
+ ) {
+ val layoutManager = binding.songRecycler.layoutManager
+ as LinearLayoutManager
- layoutManager.scrollToPositionWithOffset(itemPosition, 0)
- }
+ layoutManager.scrollToPositionWithOffset(itemPosition, 0)
}
+ }
)
}
diff --git a/app/src/main/res/color/ui_state_color.xml b/app/src/main/res/color/ui_state_color.xml
new file mode 100644
index 000000000..86937ee12
--- /dev/null
+++ b/app/src/main/res/color/ui_state_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 000000000..162b11342
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_queue.xml b/app/src/main/res/layout/fragment_queue.xml
index d3f0b9021..7da6ff940 100644
--- a/app/src/main/res/layout/fragment_queue.xml
+++ b/app/src/main/res/layout/fragment_queue.xml
@@ -34,7 +34,7 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/queue_header"
tools:layout_editor_absoluteX="0dp"
- tools:listitem="@layout/item_song" />
+ tools:listitem="@layout/item_basic_song" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_songs.xml b/app/src/main/res/layout/fragment_songs.xml
index b8d3a257e..b6fb9a2b5 100644
--- a/app/src/main/res/layout/fragment_songs.xml
+++ b/app/src/main/res/layout/fragment_songs.xml
@@ -28,10 +28,10 @@
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/song_fast_scroll"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/song_toolbar"
- tools:listitem="@layout/item_song" />
+ tools:listitem="@layout/item_basic_song" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ca35f7d2b..fcf2a312d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
Next in Queue
Music Playback
The music playback service for Auxio.
+ Settings
State saved
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index da2870ef3..b3df616b4 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -5,6 +5,7 @@
- @color/background
- @android:color/black
- @font/inter
+ - @style/FastScrollTheme
- @drawable/ui_cursor
- true
@@ -47,9 +48,13 @@
- @color/background
+
+