From 90f10f2a84c681308cd0f5df3150ccd681b68342 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sat, 19 Mar 2022 10:59:13 -0600 Subject: [PATCH] home: clean up fast scroller Clean up the fast scroller implementation to remove dead code and be more coherent in general. --- app/build.gradle | 1 - .../java/org/oxycblt/auxio/MainActivity.kt | 7 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 4 +- .../fastscroll/FastScrollPopupDrawable.kt | 168 --------- .../home/fastscroll/FastScrollPopupView.kt | 173 +++++++++ .../home/fastscroll/FastScrollRecyclerView.kt | 338 +++++++----------- .../auxio/playback/PlaybackFragment.kt | 8 +- .../oxycblt/auxio/playback/PlaybackLayout.kt | 33 +- .../auxio/playback/system/PlaybackService.kt | 14 +- .../java/org/oxycblt/auxio/ui/ActionMenu.kt | 7 +- .../java/org/oxycblt/auxio/util/ViewUtil.kt | 46 +++ app/src/main/res/values/dimens.xml | 7 +- prebuild.py | 2 +- 13 files changed, 377 insertions(+), 431 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt 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"