diff --git a/app/build.gradle b/app/build.gradle index 0ef347178..4a7d26a2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,9 +68,10 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.3.1" // UI + implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.dynamicanimation:dynamicanimation:1.0.0" - + // Lifecycle def lifecycle_version = "2.3.0" implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version" diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index bd6c7d404..e3a688836 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -78,6 +78,7 @@ class MusicStore private constructor() { * Find a song from this instance in a safe manner. * Using a normal search of the songs list runs the risk of getting the *wrong* song with * the same name, so the album name is also used to fix the above problem. + * FIXME: Artist names are more unique than album names, use those * @param name The name of the song * @param albumName The name of the song's album. * @return The song requested, null if there isnt one. diff --git a/app/src/main/java/org/oxycblt/auxio/songs/FastScrollThumb.kt b/app/src/main/java/org/oxycblt/auxio/songs/FastScrollThumb.kt deleted file mode 100644 index 1992f49da..000000000 --- a/app/src/main/java/org/oxycblt/auxio/songs/FastScrollThumb.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.oxycblt.auxio.songs - -import android.content.Context -import android.graphics.drawable.GradientDrawable -import android.os.Build -import android.util.AttributeSet -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -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 org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.ViewScrollThumbBinding -import org.oxycblt.auxio.ui.Accent -import org.oxycblt.auxio.ui.inflater - -/** - * The companion thumb for [FastScrollView]. This does not need any setup, instead pass it as an - * argument to [FastScrollView.setup]. - * This code is fundamentally an adaptation of Reddit's IndicatorFastScroll, albeit specialized - * towards Auxio. The original library is here: https://github.com/reddit/IndicatorFastScroll/ - * @author OxygenCobalt - */ -class FastScrollThumb @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = -1 -) : ConstraintLayout(context, attrs, defStyleAttr) { - private val thumbAnim: SpringAnimation - private val binding = ViewScrollThumbBinding.inflate(context.inflater, this, true) - - init { - val accent = Accent.get().getStateList(context) - - binding.thumbLayout.apply { - backgroundTintList = accent - - // Workaround for API 21 tint bug - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { - (background as GradientDrawable).apply { - mutate() - color = accent - } - } - } - - binding.thumbText.apply { - isVisible = true - TextViewCompat.setTextAppearance(this, R.style.TextAppearance_ThumbIndicator) - } - - thumbAnim = SpringAnimation(binding.thumbLayout, DynamicAnimation.TRANSLATION_Y).apply { - spring = SpringForce().also { - it.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY - } - } - - visibility = View.INVISIBLE - isActivated = false - - post { - visibility = View.VISIBLE - } - } - - /** - * Make the thumb jump to a new position and update its text to the given [indicator]. - * This is not meant for use outside of the main [FastScrollView] code. Do not use it. - */ - fun jumpTo(indicator: FastScrollView.Indicator, centerY: Int) { - binding.thumbText.text = indicator.char.toString() - thumbAnim.animateToFinalPosition( - centerY.toFloat() - (binding.thumbLayout.measuredHeight / 2) - ) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt b/app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt index 7dd967c52..9cde7fc86 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt @@ -4,22 +4,26 @@ import android.content.Context import android.os.Build import android.util.AttributeSet import android.util.TypedValue -import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.widget.TextViewCompat +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ViewFastScrollBinding import org.oxycblt.auxio.logD import org.oxycblt.auxio.ui.Accent +import org.oxycblt.auxio.ui.canScroll +import org.oxycblt.auxio.ui.inflater import org.oxycblt.auxio.ui.resolveAttr import org.oxycblt.auxio.ui.toColor import kotlin.math.ceil import kotlin.math.min +import kotlin.math.roundToInt /** * A view that allows for quick scrolling through a [RecyclerView] with many items. Unlike other @@ -32,22 +36,23 @@ class FastScrollView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1 -) : LinearLayout(context, attrs, defStyleAttr) { +) : ConstraintLayout(context, attrs, defStyleAttr) { - // --- BASIC SETUP --- + // --- UI --- + + private val binding = ViewFastScrollBinding.inflate(context.inflater, this, true) + private val thumbAnim: SpringAnimation + + // --- RECYCLER --- private var mRecycler: RecyclerView? = null - private var mThumb: FastScrollThumb? = null private var mGetItem: ((Int) -> Char)? = null // --- INDICATORS --- - /** Representation of a single Indicator character in the view */ - data class Indicator(val char: Char, val pos: Int) + private data class Indicator(val char: Char, val pos: Int) private var indicators = listOf() - - private val indicatorText: TextView private val activeColor = Accent.get().color.toColor(context) private val inactiveColor = android.R.attr.textColorSecondary.resolveAttr(context) @@ -59,34 +64,21 @@ class FastScrollView @JvmOverloads constructor( init { isFocusableInTouchMode = true isClickable = true - gravity = Gravity.CENTER - val textPadding = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 4F, resources.displayMetrics - ) - - // Making this entire view a TextView will cause distortions due to the touch calculations - // using a height that is not wrapped to the text. - indicatorText = AppCompatTextView(context).apply { - gravity = Gravity.CENTER - includeFontPadding = false - - TextViewCompat.setTextAppearance(this, R.style.TextAppearance_FastScroll) - setLineSpacing(textPadding, lineSpacingMultiplier) - setTextColor(inactiveColor) + thumbAnim = SpringAnimation(binding.scrollThumb, DynamicAnimation.TRANSLATION_Y).apply { + spring = SpringForce().also { + it.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + } } - - addView(indicatorText) } /** * Set up this view with a [RecyclerView] and a corresponding [FastScrollThumb]. */ - fun setup(recycler: RecyclerView, thumb: FastScrollThumb, getItem: (Int) -> Char) { + fun setup(recycler: RecyclerView, getItem: (Int) -> Char) { check(mRecycler == null) { "Only set up this view once." } mRecycler = recycler - mThumb = thumb mGetItem = getItem postIndicatorUpdate() @@ -105,6 +97,9 @@ class FastScrollView @JvmOverloads constructor( updateIndicators() } + // Hide this view if there is nothing to scroll + isVisible = recycler.canScroll() + hasPostedItemUpdate = false } } @@ -140,9 +135,9 @@ class FastScrollView @JvmOverloads constructor( } } - indicatorText.apply { - tag = indicators - text = indicators.joinToString("\n") { it.char.toString() } + // Then set it as the unified TextView text, for efficiency purposes. + binding.scrollIndicatorText.text = indicators.joinToString("\n") { indicator -> + indicator.char.toString() } } @@ -152,37 +147,38 @@ class FastScrollView @JvmOverloads constructor( override fun onTouchEvent(event: MotionEvent): Boolean { performClick() - val success = handleTouch(event.action, event.y.toInt()) + val success = handleTouch(event.action, event.y.roundToInt()) // Depending on the results, update the visibility of the thumb and the pressed state of // this view. isPressed = success - mThumb?.isActivated = success + binding.scrollThumb.isActivated = success return success } - @Suppress("UNCHECKED_CAST") private fun handleTouch(action: Int, touchY: Int): Boolean { if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - indicatorText.setTextColor(inactiveColor) + binding.scrollIndicatorText.setTextColor(inactiveColor) lastPos = -1 return false } - if (touchY in (indicatorText.top until indicatorText.bottom)) { - // Try to roughly caculate which indicator the user is currently touching [Since the - val textHeight = indicatorText.height / indicators.size - val indicatorIndex = min( - (touchY - indicatorText.top) / textHeight, indicators.lastIndex - ) + // Try to figure out which indicator the pointer has landed on + if (touchY in (binding.scrollIndicatorText.top until binding.scrollIndicatorText.bottom)) { + // Get the touch position in regards to the TextView and the rough text height + val indicatorTouchY = touchY - binding.scrollIndicatorText.top + val textHeight = binding.scrollIndicatorText.height / indicators.size - val centerY = y.toInt() + (textHeight / 2) + (indicatorIndex * textHeight) + // Use that to calculate the indicator index, if the calculation is + // invalid just ignore it. + val index = min(indicatorTouchY / textHeight, indicators.lastIndex) - val touchedIndicator = indicators[indicatorIndex] + // Also calculate the rough center position of the indicator for the scroll thumb + val centerY = binding.scrollIndicatorText.y + (textHeight / 2) + (index * textHeight) - selectIndicator(touchedIndicator, centerY) + selectIndicator(indicators[index], centerY) return true } @@ -190,20 +186,20 @@ class FastScrollView @JvmOverloads constructor( return false } - private fun selectIndicator(indicator: Indicator, indicatorCenterY: Int) { + private fun selectIndicator(indicator: Indicator, centerY: Float) { if (indicator.pos != lastPos) { lastPos = indicator.pos - indicatorText.setTextColor(activeColor) + binding.scrollIndicatorText.setTextColor(activeColor) // Stop any scroll momentum and snap-scroll to the position mRecycler?.apply { stopScroll() - (layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - indicator.pos, 0 - ) + (layoutManager as LinearLayoutManager).scrollToPositionWithOffset(indicator.pos, 0) } - mThumb?.jumpTo(indicator, indicatorCenterY) + // Update the thumb position/text + binding.scrollThumbText.text = indicator.char.toString() + thumbAnim.animateToFinalPosition(centerY - (binding.scrollThumb.measuredHeight / 2)) performHapticFeedback( if (Build.VERSION.SDK_INT >= 27) { 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 3351383cc..796803174 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -12,7 +12,6 @@ import org.oxycblt.auxio.databinding.FragmentSongsBinding import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.ui.canScroll import org.oxycblt.auxio.ui.fixAnimInfoLeak import org.oxycblt.auxio.ui.getSpans import org.oxycblt.auxio.ui.newMenu @@ -55,17 +54,9 @@ class SongsFragment : Fragment() { if (spans != 1) { layoutManager = GridLayoutManager(requireContext(), spans) } - - post { - if (!canScroll()) { - // Disable fast scrolling if there is nothing to scroll - binding.songFastScroll.visibility = View.GONE - binding.songFastScrollThumb.visibility = View.GONE - } - } } - binding.songFastScroll.setup(binding.songRecycler, binding.songFastScrollThumb) { pos -> + binding.songFastScroll.setup(binding.songRecycler) { pos -> val char = musicStore.songs[pos].name.first if (char.isDigit()) '#' else char diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index fd1ee2ca3..1ab0a83b5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -84,7 +84,7 @@ fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int): String { * @param T The system service in question. * @param serviceClass The service's kotlin class [Java class will be used in function call] * @return The system service - * @throws IllegalStateException If the system service cannot be retrieved. + * @throws IllegalArgumentException If the system service cannot be retrieved. */ fun Context.getSystemServiceSafe(serviceClass: KClass): T { return requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) { @@ -218,7 +218,6 @@ fun Activity.isIrregularLandscape(): Boolean { * Check if the system bars are on the bottom. * @return If the system bars are on the bottom, false if no. */ -@Suppress("DEPRECATION") private fun isSystemBarOnBottom(activity: Activity): Boolean { val realPoint = Point() val metrics = DisplayMetrics() @@ -236,6 +235,7 @@ private fun isSystemBarOnBottom(activity: Activity): Boolean { } } } else { + @Suppress("DEPRECATION") (activity.getSystemServiceSafe(WindowManager::class)).apply { defaultDisplay.getRealSize(realPoint) defaultDisplay.getMetrics(metrics) diff --git a/app/src/main/res/drawable/ui_circle.xml b/app/src/main/res/drawable/ui_circle.xml new file mode 100644 index 000000000..a6f3dfaa6 --- /dev/null +++ b/app/src/main/res/drawable/ui_circle.xml @@ -0,0 +1,5 @@ + + + + \ 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 8e15c4355..c6d23a922 100644 --- a/app/src/main/res/layout/fragment_songs.xml +++ b/app/src/main/res/layout/fragment_songs.xml @@ -39,13 +39,5 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/song_toolbar" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_fast_scroll.xml b/app/src/main/res/layout/view_fast_scroll.xml new file mode 100644 index 000000000..b78dc683c --- /dev/null +++ b/app/src/main/res/layout/view_fast_scroll.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_scroll_thumb.xml b/app/src/main/res/layout/view_scroll_thumb.xml deleted file mode 100644 index 775d1fcbd..000000000 --- a/app/src/main/res/layout/view_scroll_thumb.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 2268a0d01..07d8cbfce 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -24,11 +24,6 @@ android:name="org.oxycblt.auxio.MainFragment" android:label="MainFragment" tools:layout="@layout/fragment_main"> - 70dp 36dp - 32dp + 48dp 32dp 60dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 341a2055b..080c65ba7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -98,18 +98,6 @@ @font/inter_exbold - - - - - -