home: clean up fast scroller

Clean up the fast scroller implementation to remove dead code and
be more coherent in general.
This commit is contained in:
OxygenCobalt 2022-03-19 10:59:13 -06:00
parent f1bd6b1c8c
commit 90f10f2a84
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 377 additions and 431 deletions

View file

@ -106,7 +106,6 @@ dependencies {
spotless {
kotlin {
target "src/**/*.kt"
ktfmt('0.30').dropboxStyle()
licenseHeaderFile("NOTICE")
}

View file

@ -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()

View file

@ -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()

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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

View file

@ -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() {

View file

@ -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

View file

@ -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")

View file

@ -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,

View file

@ -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.

View file

@ -30,8 +30,11 @@
<dimen name="elevation_small">2dp</dimen>
<dimen name="elevation_normal">4dp</dimen>
<dimen name="popup_min_width">78dp</dimen>
<dimen name="popup_padding_end">28dp</dimen>
<dimen name="fast_scroll_popup_min_width">80dp</dimen>
<dimen name="fast_scroll_popup_min_height">64dp</dimen>
<dimen name="fast_scroll_popup_padding_start">@dimen/spacing_medium</dimen>
<dimen name="fast_scroll_popup_padding_end">28dp</dimen>
<dimen name="fast_scroll_thumb_touch_target_size">16dp</dimen>
<dimen name="slider_thumb_radius">6dp</dimen>
<dimen name="slider_halo_radius">12dp</dimen>

View file

@ -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"