diff --git a/app/build.gradle b/app/build.gradle
index a5ba391eb..b76c5a506 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -106,7 +106,6 @@ dependencies {
spotless {
kotlin {
target "src/**/*.kt"
-
ktfmt('0.30').dropboxStyle()
licenseHeaderFile("NOTICE")
}
diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
index ef04cd764..b75e62ede 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
@@ -40,8 +40,11 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* The single [AppCompatActivity] for Auxio.
*
- * TODO: Add a new view for crashes with a stack trace TODO: Custom language support TODO: Rework
- * menus [perhaps add multi-select]
+ * TODO: Add a new view for crashes with a stack trace
+ *
+ * TODO: Custom language support
+ *
+ * TODO: Rework menus [perhaps add multi-select]
*/
class MainActivity : AppCompatActivity() {
private val playbackModel: PlaybackViewModel by viewModels()
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
index 80442000c..3edbeb4d4 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -55,7 +55,9 @@ import org.oxycblt.auxio.util.logTraceOrThrow
* respective item.
* @author OxygenCobalt
*
- * TODO: Make tabs invisible when there is only one TODO: Add duration and song count sorts
+ * TODO: Make tabs invisible when there is only one
+ *
+ * TODO: Add duration and song count sorts
*/
class HomeFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt
deleted file mode 100644
index 4780775bd..000000000
--- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (c) 2021 Auxio Project
- *
- * 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package org.oxycblt.auxio.home.fastscroll
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.ColorFilter
-import android.graphics.Matrix
-import android.graphics.Outline
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.PixelFormat
-import android.graphics.Rect
-import android.graphics.drawable.Drawable
-import android.os.Build
-import android.view.View
-import androidx.core.graphics.drawable.DrawableCompat
-import kotlin.math.sqrt
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.util.getAttrColorSafe
-import org.oxycblt.auxio.util.getDimenOffsetSafe
-
-/**
- * The custom drawable used as FastScrollRecyclerView's popup background. This is an adaptation from
- * AndroidFastScroll's MD2 theme.
- *
- * Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang
- * [https://github.com/zhanghai] PROJECT: Android Fast Scroll
- * [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/]
- *
- * !!! MODIFICATIONS !!!:
- * - Use modified Auxio resources instead of AFS resources
- * - Variable names are no longer prefixed with m
- * - Made path management compat-friendly
- * - Converted to kotlin
- *
- * @author Hai Zhang, OxygenCobalt
- */
-class FastScrollPopupDrawable(context: Context) : Drawable() {
- private val paint: Paint =
- Paint().apply {
- isAntiAlias = true
- color = context.getAttrColorSafe(R.attr.colorSecondary)
- style = Paint.Style.FILL
- }
-
- private val path = Path()
- private val matrix = Matrix()
-
- private val paddingStart = context.getDimenOffsetSafe(R.dimen.spacing_medium)
- private val paddingEnd = context.getDimenOffsetSafe(R.dimen.popup_padding_end)
-
- override fun draw(canvas: Canvas) {
- canvas.drawPath(path, paint)
- }
-
- override fun onBoundsChange(bounds: Rect) {
- updatePath()
- }
-
- override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
- updatePath()
- return true
- }
-
- @Suppress("DEPRECATION")
- override fun getOutline(outline: Outline) {
- when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> outline.setPath(path)
-
- // Paths don't need to be convex on android Q, but the API was mislabeled and so
- // we still have to use this method.
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path)
- else ->
- if (!path.isConvex) {
- // The outline path must be convex before Q, but we may run into floating point
- // errors caused by calculations involving sqrt(2) or OEM implementation
- // differences,
- // so in this case we just omit the shadow instead of crashing.
- super.getOutline(outline)
- }
- }
- }
-
- override fun getPadding(padding: Rect): Boolean {
- if (isRtl) {
- padding[paddingEnd, 0, paddingStart] = 0
- } else {
- padding[paddingStart, 0, paddingEnd] = 0
- }
-
- return true
- }
-
- override fun isAutoMirrored(): Boolean = true
- override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
- override fun setAlpha(alpha: Int) {}
- override fun setColorFilter(colorFilter: ColorFilter?) {}
-
- private fun updatePath() {
- path.reset()
-
- var width = bounds.width().toFloat()
- val height = bounds.height().toFloat()
- val r = height / 2
- val sqrt2 = sqrt(2.0).toFloat()
-
- // Ensure we are convex
- width = (r + sqrt2 * r).coerceAtLeast(width)
- pathArcTo(path, r, r, r, 90f, 180f)
-
- val o1X = width - sqrt2 * r
- pathArcTo(path, o1X, r, r, -90f, 45f)
-
- val r2 = r / 5
- val o2X = width - sqrt2 * r2
- pathArcTo(path, o2X, r, r2, -45f, 90f)
- pathArcTo(path, o1X, r, r, 45f, 45f)
-
- path.close()
-
- if (isRtl) {
- matrix.setScale(-1f, 1f, width / 2, 0f)
- } else {
- matrix.reset()
- }
-
- matrix.postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
-
- path.transform(matrix)
- }
-
- private fun pathArcTo(
- path: Path,
- centerX: Float,
- centerY: Float,
- radius: Float,
- startAngle: Float,
- sweepAngle: Float
- ) {
- path.arcTo(
- centerX - radius,
- centerY - radius,
- centerX + radius,
- centerY + radius,
- startAngle,
- sweepAngle,
- false)
- }
-
- private val isRtl: Boolean
- get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt
new file mode 100644
index 000000000..9619016e5
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2022 Auxio Project
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package org.oxycblt.auxio.home.fastscroll
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.Gravity
+import androidx.core.widget.TextViewCompat
+import com.google.android.material.textview.MaterialTextView
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.util.getAttrColorSafe
+import org.oxycblt.auxio.util.getDimenOffsetSafe
+import org.oxycblt.auxio.util.getDimenSizeSafe
+import org.oxycblt.auxio.util.isRtl
+
+class FastScrollPopupView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :
+ MaterialTextView(context, attrs, defStyleRes) {
+ init {
+ minimumWidth = context.getDimenSizeSafe(R.dimen.fast_scroll_popup_min_width)
+ minimumHeight = context.getDimenSizeSafe(R.dimen.fast_scroll_popup_min_height)
+
+ TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
+ setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
+ ellipsize = TextUtils.TruncateAt.MIDDLE
+ gravity = Gravity.CENTER
+ includeFontPadding = false
+
+ alpha = 0f
+ elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
+ background = FastScrollPopupDrawable(context)
+ }
+
+ private class FastScrollPopupDrawable(context: Context) : Drawable() {
+ private val paint: Paint =
+ Paint().apply {
+ isAntiAlias = true
+ color = context.getAttrColorSafe(R.attr.colorSecondary)
+ style = Paint.Style.FILL
+ }
+
+ private val path = Path()
+ private val matrix = Matrix()
+
+ private val paddingStart =
+ context.getDimenOffsetSafe(R.dimen.fast_scroll_popup_padding_start)
+ private val paddingEnd = context.getDimenOffsetSafe(R.dimen.fast_scroll_popup_padding_end)
+
+ override fun draw(canvas: Canvas) {
+ canvas.drawPath(path, paint)
+ }
+
+ override fun onBoundsChange(bounds: Rect) {
+ updatePath()
+ }
+
+ override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
+ updatePath()
+ return true
+ }
+
+ @Suppress("DEPRECATION")
+ override fun getOutline(outline: Outline) {
+ when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> outline.setPath(path)
+
+ // Paths don't need to be convex on android Q, but the API was mislabeled and so
+ // we still have to use this method.
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path)
+ else ->
+ if (!path.isConvex) {
+ // The outline path must be convex before Q, but we may run into floating
+ // point errors caused by calculations involving sqrt(2) or OEM differences,
+ // so in this case we just omit the shadow instead of crashing.
+ super.getOutline(outline)
+ }
+ }
+ }
+
+ override fun getPadding(padding: Rect): Boolean {
+ if (isRtl) {
+ padding[paddingEnd, 0, paddingStart] = 0
+ } else {
+ padding[paddingStart, 0, paddingEnd] = 0
+ }
+
+ return true
+ }
+
+ override fun isAutoMirrored(): Boolean = true
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
+ override fun setAlpha(alpha: Int) {}
+ override fun setColorFilter(colorFilter: ColorFilter?) {}
+
+ private fun updatePath() {
+ val r = bounds.height().toFloat() / 2
+ val w = (r + SQRT2 * r).coerceAtLeast(bounds.width().toFloat())
+
+ path.apply {
+ reset()
+
+ // Draw the left pill shape
+ val o1X = w - SQRT2 * r
+ arcToSafe(r, r, r, 90f, 180f)
+ arcToSafe(o1X, r, r, -90f, 45f)
+
+ // Draw the right arrow shape
+ val point = r / 5
+ val o2X = w - SQRT2 * point
+ arcToSafe(o2X, r, point, -45f, 90f)
+ arcToSafe(o1X, r, r, 45f, 45f)
+
+ close()
+ }
+
+ matrix.apply {
+ reset()
+ if (isRtl) setScale(-1f, 1f, w / 2, 0f)
+ postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
+ }
+
+ path.transform(matrix)
+ }
+
+ private fun Path.arcToSafe(
+ centerX: Float,
+ centerY: Float,
+ radius: Float,
+ startAngle: Float,
+ sweepAngle: Float
+ ) {
+ path.arcTo(
+ centerX - radius,
+ centerY - radius,
+ centerX + radius,
+ centerY + radius,
+ startAngle,
+ sweepAngle,
+ false)
+ }
+ }
+
+ companion object {
+ private const val SQRT2 = 1.4142135623730950488f
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
index 3851aeca0..077588a84 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt
@@ -17,11 +17,9 @@
package org.oxycblt.auxio.home.fastscroll
-import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
-import android.text.TextUtils
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
@@ -30,12 +28,9 @@ import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.WindowInsets
import android.widget.FrameLayout
-import android.widget.TextView
import androidx.annotation.AttrRes
-import androidx.appcompat.widget.AppCompatTextView
import androidx.core.math.MathUtils
import androidx.core.view.isInvisible
-import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -43,19 +38,21 @@ import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.EdgeRecyclerView
import org.oxycblt.auxio.util.canScroll
-import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getDimenOffsetSafe
import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDrawableSafe
+import org.oxycblt.auxio.util.isRtl
+import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
* Hai Zhang's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
*
- * Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang
- * [https://github.com/zhanghai] PROJECT: Android Fast Scroll
- * [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/]
+ * Attributions as per the Apache 2.0 license:
+ * - ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
+ * - PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
+ * - MODIFIER: OxygenCobalt [https://github.com/oxygencobalt]
*
* !!! MODIFICATIONS !!!:
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with
@@ -72,11 +69,90 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* - Added documentation
*
* @author Hai Zhang, OxygenCobalt
+ *
+ * TODO: Fix strange touch behavior when the pointer is slightly outside of the view.
+ *
+ * TODO: Really try to make this view less insane.
*/
class FastScrollRecyclerView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
EdgeRecyclerView(context, attrs, defStyleAttr) {
+ private val minTouchTargetSize =
+ context.getDimenSizeSafe(R.dimen.fast_scroll_thumb_touch_target_size)
+ private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
+
+ // Thumb
+ private val thumbView =
+ View(context).apply {
+ alpha = 0f
+ background = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
+ this@FastScrollRecyclerView.overlay.add(this)
+ }
+
+ private val thumbWidth = thumbView.background.intrinsicWidth
+ private val thumbHeight = thumbView.background.intrinsicHeight
+ private val thumbPadding = Rect(0, 0, 0, 0)
+ private var thumbOffset = 0
+
+ private var showingThumb = false
+ private val hideThumbRunnable = Runnable {
+ if (!dragging) {
+ hideScrollbar()
+ }
+ }
+
+ // Popup
+ private val popupView =
+ FastScrollPopupView(context).apply {
+ layoutParams =
+ FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ .apply {
+ gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
+ marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
+ }
+
+ this@FastScrollRecyclerView.overlay.add(this)
+ }
+
+ private var showingPopup = false
+
+ // Touch events
+ private var downX = 0f
+ private var downY = 0f
+ private var lastY = 0f
+ private var dragStartY = 0f
+ private var dragStartThumbOffset = 0
+
+ private var dragging = false
+ set(value) {
+ if (field == value) {
+ return
+ }
+
+ field = value
+
+ if (value) {
+ parent.requestDisallowInterceptTouchEvent(true)
+ }
+
+ thumbView.isPressed = value
+
+ if (field) {
+ removeCallbacks(hideThumbRunnable)
+ showScrollbar()
+ showPopup()
+ } else {
+ postAutoHideScrollbar()
+ hidePopup()
+ }
+
+ onDragListener?.invoke(value)
+ }
+
+ private val childRect = Rect()
+
/** Callback to provide a string to be shown on the popup when an item is passed */
var popupProvider: ((Int) -> String)? = null
@@ -86,83 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
*/
var onDragListener: ((Boolean) -> Unit)? = null
- private val minTouchTargetSize: Int = context.getDimenSizeSafe(R.dimen.size_btn_small)
- private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
-
- // Views for the track, thumb, and popup. Note that the track view is mostly vestigial
- // and is only for bounds checking.
- private val trackView: View
- private val thumbView: View
- private val popupView: TextView
-
- // Touch values
- private val thumbWidth: Int
- private val thumbHeight: Int
- private var thumbOffset = 0
- private var downX = 0f
- private var downY = 0f
- private var lastY = 0f
- private var dragStartY = 0f
- private var dragStartThumbOffset = 0
-
- // State
- private var dragging = false
- private var showingScrollbar = false
- private var showingPopup = false
-
- private val childRect = Rect()
-
- private val hideScrollbarRunnable = Runnable {
- if (!dragging) {
- hideScrollbar()
- }
- }
-
- private val scrollerPadding = Rect(0, 0, 0, 0)
-
init {
- val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
-
- trackView = View(context)
- thumbView =
- View(context).apply {
- alpha = 0f
- background = thumbDrawable
- }
-
- popupView =
- AppCompatTextView(context).apply {
- alpha = 0f
- layoutParams =
- FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-
- minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width)
- minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large)
-
- (layoutParams as FrameLayout.LayoutParams).apply {
- gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
- marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
- }
-
- TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
- setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
-
- background = FastScrollPopupDrawable(context)
- elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
- ellipsize = TextUtils.TruncateAt.MIDDLE
- gravity = Gravity.CENTER
- includeFontPadding = false
- isSingleLine = true
- }
-
- thumbWidth = thumbDrawable.intrinsicWidth
- thumbHeight = thumbDrawable.intrinsicHeight
-
- check(thumbWidth >= 0)
- check(thumbHeight >= 0)
-
- overlay.add(trackView)
overlay.add(thumbView)
overlay.add(popupView)
@@ -195,42 +195,33 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private fun onPreDraw() {
updateScrollbarState()
- trackView.layoutDirection = layoutDirection
thumbView.layoutDirection = layoutDirection
popupView.layoutDirection = layoutDirection
- val trackLeft =
- if (isRtl) {
- scrollerPadding.left
- } else {
- width - scrollerPadding.right - thumbWidth
- }
-
- trackView.layout(
- trackLeft, scrollerPadding.top, trackLeft + thumbWidth, height - scrollerPadding.bottom)
-
val thumbLeft =
if (isRtl) {
- scrollerPadding.left
+ thumbPadding.left
} else {
- width - scrollerPadding.right - thumbWidth
+ width - thumbPadding.right - thumbWidth
}
- val thumbTop = scrollerPadding.top + thumbOffset
+ val thumbTop = thumbPadding.top + thumbOffset
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
val firstPos = firstAdapterPos
val popupText =
if (firstPos != NO_POSITION) {
- popupProvider?.invoke(firstPos) ?: ""
+ popupProvider?.invoke(firstPos)?.ifEmpty { null }
} else {
- ""
+ null
}
- popupView.isInvisible = popupText.isEmpty()
+ // Lay out the popup view
- if (popupText.isNotEmpty()) {
+ popupView.isInvisible = popupText == null
+
+ if (popupText != null) {
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
if (popupView.text != popupText) {
@@ -239,8 +230,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val widthMeasureSpec =
ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- scrollerPadding.left +
- scrollerPadding.right +
+ thumbPadding.left +
+ thumbPadding.right +
thumbWidth +
popupLayoutParams.leftMargin +
popupLayoutParams.rightMargin,
@@ -249,8 +240,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val heightMeasureSpec =
ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
- scrollerPadding.top +
- scrollerPadding.bottom +
+ thumbPadding.top +
+ thumbPadding.bottom +
popupLayoutParams.topMargin +
popupLayoutParams.bottomMargin,
popupLayoutParams.height)
@@ -262,39 +253,23 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val popupHeight = popupView.measuredHeight
val popupLeft =
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
- scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
+ thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin
} else {
width -
- scrollerPadding.right -
+ thumbPadding.right -
thumbWidth -
popupLayoutParams.rightMargin -
popupWidth
}
- // We handle RTL separately, so it's okay if Gravity.RIGHT is used here
- @SuppressLint("RtlHardcoded")
- val popupAnchorY =
- when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
- Gravity.CENTER_HORIZONTAL -> popupHeight / 2
- Gravity.RIGHT -> popupHeight
- else -> 0
- }
-
- val thumbAnchorY =
- when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
- Gravity.CENTER_VERTICAL -> {
- thumbView.paddingTop +
- (thumbHeight - thumbView.paddingTop - thumbView.paddingBottom) / 2
- }
- Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom
- else -> thumbView.paddingTop
- }
+ val popupAnchorY = popupHeight / 2
+ val thumbAnchorY = thumbView.paddingTop
val popupTop =
MathUtils.clamp(
thumbTop + thumbAnchorY - popupAnchorY,
- scrollerPadding.top + popupLayoutParams.topMargin,
- height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
+ thumbPadding.top + popupLayoutParams.topMargin,
+ height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
}
@@ -317,7 +292,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
super.onApplyWindowInsets(insets)
val bars = insets.systemBarInsetsCompat
- scrollerPadding.bottom = bars.bottom
+ thumbPadding.bottom = bars.bottom
return insets
}
@@ -345,38 +320,36 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
downX = eventX
downY = eventY
- val scrollX = trackView.scrollX
- val isInScrollbar =
- eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX
-
- if (trackView.alpha > 0 && isInScrollbar) {
+ if (eventX >= thumbView.left && eventX < thumbView.right) {
dragStartY = eventY
- if (isInViewTouchTarget(thumbView, eventX, eventY)) {
+ if (thumbView.isUnder(eventX, eventY, minTouchTargetSize)) {
dragStartThumbOffset = thumbOffset
} else {
dragStartThumbOffset =
- (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
+ (eventY - thumbPadding.top - thumbHeight / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
}
- setDragging(true)
+ dragging = true
}
}
MotionEvent.ACTION_MOVE -> {
if (!dragging &&
- isInViewTouchTarget(trackView, downX, downY) &&
+ thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
abs(eventY - downY) > touchSlop) {
- if (isInViewTouchTarget(thumbView, downX, downY)) {
+
+ if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
dragStartY = lastY
dragStartThumbOffset = thumbOffset
} else {
dragStartY = eventY
dragStartThumbOffset =
- (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
+ (eventY - thumbPadding.top - thumbHeight / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
}
- setDragging(true)
+
+ dragging = true
}
if (dragging) {
@@ -384,49 +357,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
scrollToThumbOffset(thumbOffset)
}
}
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> dragging = false
}
lastY = eventY
return dragging
}
- private fun isInViewTouchTarget(view: View, x: Float, y: Float): Boolean {
- return isInTouchTarget(x, view.left - scrollX, view.right - scrollX, width) &&
- isInTouchTarget(y, view.top - scrollY, view.bottom - scrollY, height)
- }
-
- private fun isInTouchTarget(
- position: Float,
- viewStart: Int,
- viewEnd: Int,
- parentEnd: Int
- ): Boolean {
- val viewSize = viewEnd - viewStart
-
- if (viewSize >= minTouchTargetSize) {
- return position >= viewStart && position < viewEnd
- }
-
- var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
-
- if (touchTargetStart < 0) {
- touchTargetStart = 0
- }
-
- var touchTargetEnd = touchTargetStart + minTouchTargetSize
- if (touchTargetEnd > parentEnd) {
- touchTargetEnd = parentEnd
- touchTargetStart = touchTargetEnd - minTouchTargetSize
-
- if (touchTargetStart < 0) {
- touchTargetStart = 0
- }
- }
-
- return position >= touchTargetStart && position < touchTargetEnd
- }
-
private fun scrollToThumbOffset(thumbOffset: Int) {
val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange)
@@ -464,54 +401,28 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
}
- private fun setDragging(isDragging: Boolean) {
- if (dragging == isDragging) {
- return
- }
-
- dragging = isDragging
-
- if (dragging) {
- parent.requestDisallowInterceptTouchEvent(true)
- }
-
- trackView.isPressed = dragging
- thumbView.isPressed = dragging
-
- if (dragging) {
- removeCallbacks(hideScrollbarRunnable)
- showScrollbar()
- showPopup()
- } else {
- postAutoHideScrollbar()
- hidePopup()
- }
-
- onDragListener?.invoke(isDragging)
- }
-
// --- SCROLLBAR APPEARANCE ---
private fun postAutoHideScrollbar() {
- removeCallbacks(hideScrollbarRunnable)
- postDelayed(hideScrollbarRunnable, AUTO_HIDE_SCROLLBAR_DELAY_MILLIS.toLong())
+ removeCallbacks(hideThumbRunnable)
+ postDelayed(hideThumbRunnable, AUTO_HIDE_SCROLLBAR_DELAY_MILLIS.toLong())
}
private fun showScrollbar() {
- if (showingScrollbar) {
+ if (showingThumb) {
return
}
- showingScrollbar = true
+ showingThumb = true
animateView(thumbView, 1f)
}
private fun hideScrollbar() {
- if (!showingScrollbar) {
+ if (!showingThumb) {
return
}
- showingScrollbar = false
+ showingThumb = false
animateView(thumbView, 0f)
}
@@ -539,12 +450,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// --- LAYOUT STATE ---
- private val isRtl: Boolean
- get() = layoutDirection == LAYOUT_DIRECTION_RTL
-
private val thumbOffsetRange: Int
get() {
- return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
+ return height - thumbPadding.top - thumbPadding.bottom - thumbHeight
}
private val scrollRange: Int
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt
index 2956284b1..889fee6f4 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt
@@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class PlaybackFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
- private var mLastBinding: FragmentPlaybackBinding? = null
+ private var lastBinding: FragmentPlaybackBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
@@ -55,7 +55,7 @@ class PlaybackFragment : Fragment() {
val queueItem: MenuItem
// See onDestroyView for why we do this
- mLastBinding = binding
+ lastBinding = binding
// --- UI SETUP ---
@@ -157,8 +157,8 @@ class PlaybackFragment : Fragment() {
// playbackSong will leak if we don't disable marquee, keep the binding around
// so that we can turn it off when we destroy the view.
- mLastBinding?.playbackSong?.isSelected = false
- mLastBinding = null
+ lastBinding?.playbackSong?.isSelected = false
+ lastBinding = null
}
private fun navigateUp() {
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt
index 5314ace65..8cb2d2f1d 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt
@@ -47,6 +47,7 @@ import org.oxycblt.auxio.util.disableDropShadowCompat
import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getDimenSafe
import org.oxycblt.auxio.util.getDrawableSafe
+import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.pxOfDp
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
@@ -458,27 +459,25 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
return super.onInterceptTouchEvent(ev)
}
- val adx = abs(ev.x - initMotionX)
- val ady = abs(ev.y - initMotionY)
- val dragSlop = dragHelper.touchSlop
-
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
initMotionX = ev.x
initMotionY = ev.y
- if (!playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt())) {
+ if (!playbackContainerView.isUnder(ev.x, ev.y)) {
// Pointer is not on our view, do not intercept this event
dragHelper.cancel()
return false
}
}
MotionEvent.ACTION_MOVE -> {
- val pointerUnder = playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt())
- val motionUnder =
- playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt())
+ val adx = abs(ev.x - initMotionX)
+ val ady = abs(ev.y - initMotionY)
- if (!(pointerUnder || motionUnder) || ady > dragSlop && adx > ady) {
+ val pointerUnder = playbackContainerView.isUnder(ev.x, ev.y)
+ val motionUnder = playbackContainerView.isUnder(initMotionX, initMotionY)
+
+ if (!(pointerUnder || motionUnder) || ady > dragHelper.touchSlop && adx > ady) {
// Pointer has moved beyond our control, do not intercept this event
dragHelper.cancel()
return false
@@ -502,22 +501,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
}
}
- private fun View.isUnder(x: Int, y: Int): Boolean {
- val viewLocation = IntArray(2)
- getLocationOnScreen(viewLocation)
-
- val parentLocation = IntArray(2)
- (parent as View).getLocationOnScreen(parentLocation)
-
- val screenX = parentLocation[0] + x
- val screenY = parentLocation[1] + y
-
- val inX = screenX >= viewLocation[0] && screenX < viewLocation[0] + width
- val inY = screenY >= viewLocation[1] && screenY < viewLocation[1] + height
-
- return inX && inY
- }
-
private val ViewDragHelper.isDragging: Boolean
get() {
// We can't grab the drag state outside of a callback, but that's stupid and I don't
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index b700790d6..3e4662d21 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -441,6 +441,7 @@ class PlaybackService :
// Technically the MediaSession seems to handle bluetooth events on their
// own, but keep this around as a fallback in the case that the former fails
// for whatever reason.
+ // TODO: Remove this since the headset hook KeyEvent should be fine enough.
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pauseFromPlug()
@@ -459,7 +460,7 @@ class PlaybackService :
initialHeadsetPlugEventHandled = true
}
- // I have never seen this ever happen but it might be useful
+ // I have never seen this happen but it might be useful
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
// --- AUXIO EVENTS ---
@@ -484,9 +485,7 @@ class PlaybackService :
* that friendly
* 2. There is a bug where playback will always start when this service starts, mostly due
* to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but I fear
- * that it may not work on OEM skins that for whatever reason don't make this action fire.
- * TODO: Figure out how players like Retro are able to get autoplay working with bluetooth
- * headsets
+ * that it may not work on OEM skins that for whatever reason don't make this action fire.\
*/
private fun maybeResumeFromPlug() {
if (playbackManager.song != null &&
@@ -497,12 +496,7 @@ class PlaybackService :
}
}
- /**
- * Pause from a headset plug.
- *
- * TODO: Find a way to centralize this stuff into a single BroadcastReceiver instead of the
- * weird disjointed arrangement between MediaSession and this.
- */
+ /** Pause from a headset plug. */
private fun pauseFromPlug() {
if (playbackManager.song != null) {
logD("Device disconnected, pausing")
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt
index 73e6f33ca..fd0780274 100644
--- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt
+++ b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt
@@ -56,8 +56,11 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE)
* @throws IllegalStateException When there is no menu for this specific datatype/flag
* @author OxygenCobalt
*
- * TODO: Stop scrolling when a menu is open TODO: Prevent duplicate menus from showing up TODO:
- * Maybe replace this with a bottom sheet?
+ * TODO: Stop scrolling when a menu is open
+ *
+ * TODO: Prevent duplicate menus from showing up
+ *
+ * TODO: Maybe replace this with a bottom sheet?
*/
class ActionMenu(
activity: AppCompatActivity,
diff --git a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
index ea7fae6c7..834e62e63 100644
--- a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt
@@ -20,7 +20,9 @@ package org.oxycblt.auxio.util
import android.content.res.ColorStateList
import android.graphics.Insets
import android.graphics.Rect
+import android.graphics.drawable.Drawable
import android.os.Build
+import android.util.Log
import android.view.View
import android.view.WindowInsets
import androidx.annotation.ColorRes
@@ -73,6 +75,50 @@ fun View.disableDropShadowCompat() {
}
}
+fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0): Boolean {
+ return isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
+ isUnderImpl(y, top, bottom, (parent as View).height, minTouchTargetSize)
+}
+
+private fun isUnderImpl(
+ position: Float,
+ viewStart: Int,
+ viewEnd: Int,
+ parentEnd: Int,
+ minTouchTargetSize: Int
+): Boolean {
+ val viewSize = viewEnd - viewStart
+
+ if (viewSize >= minTouchTargetSize) {
+ return position >= viewStart && position < viewEnd
+ }
+
+ Log.d("Auxio.ViewUtil", "isInTouchTarget: $minTouchTargetSize")
+
+ var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
+
+ if (touchTargetStart < 0) {
+ touchTargetStart = 0
+ }
+
+ var touchTargetEnd = touchTargetStart + minTouchTargetSize
+ if (touchTargetEnd > parentEnd) {
+ touchTargetEnd = parentEnd
+ touchTargetStart = touchTargetEnd - minTouchTargetSize
+
+ if (touchTargetStart < 0) {
+ touchTargetStart = 0
+ }
+ }
+
+ return position >= touchTargetStart && position < touchTargetEnd
+}
+
+val View.isRtl: Boolean
+ get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
+val Drawable.isRtl: Boolean
+ get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
+
/**
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the frustrating changes that were made between 8-11.
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 270cf530d..f6d0f55dd 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -30,8 +30,11 @@
2dp
4dp
- 78dp
- 28dp
+ 80dp
+ 64dp
+ @dimen/spacing_medium
+ 28dp
+ 16dp
6dp
12dp
diff --git a/prebuild.py b/prebuild.py
index f95023c0e..9fd3a3d0b 100755
--- a/prebuild.py
+++ b/prebuild.py
@@ -19,7 +19,7 @@ import re
# WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND
# THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
-EXO_VERSION = "2.17.0"
+EXO_VERSION = "2.17.1"
FLAC_VERSION = "1.3.2"
FATAL="\033[1;31m"