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:
parent
f1bd6b1c8c
commit
90f10f2a84
13 changed files with 377 additions and 431 deletions
|
@ -106,7 +106,6 @@ dependencies {
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
target "src/**/*.kt"
|
target "src/**/*.kt"
|
||||||
|
|
||||||
ktfmt('0.30').dropboxStyle()
|
ktfmt('0.30').dropboxStyle()
|
||||||
licenseHeaderFile("NOTICE")
|
licenseHeaderFile("NOTICE")
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,11 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
/**
|
/**
|
||||||
* The single [AppCompatActivity] for Auxio.
|
* The single [AppCompatActivity] for Auxio.
|
||||||
*
|
*
|
||||||
* TODO: Add a new view for crashes with a stack trace TODO: Custom language support TODO: Rework
|
* TODO: Add a new view for crashes with a stack trace
|
||||||
* menus [perhaps add multi-select]
|
*
|
||||||
|
* TODO: Custom language support
|
||||||
|
*
|
||||||
|
* TODO: Rework menus [perhaps add multi-select]
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val playbackModel: PlaybackViewModel by viewModels()
|
private val playbackModel: PlaybackViewModel by viewModels()
|
||||||
|
|
|
@ -55,7 +55,9 @@ import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
* respective item.
|
* respective item.
|
||||||
* @author OxygenCobalt
|
* @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() {
|
class HomeFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,11 +17,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.fastscroll
|
package org.oxycblt.auxio.home.fastscroll
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
@ -30,12 +28,9 @@ import android.view.ViewConfiguration
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -43,19 +38,21 @@ import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.EdgeRecyclerView
|
import org.oxycblt.auxio.ui.EdgeRecyclerView
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
|
||||||
import org.oxycblt.auxio.util.getDimenOffsetSafe
|
import org.oxycblt.auxio.util.getDimenOffsetSafe
|
||||||
import org.oxycblt.auxio.util.getDimenSizeSafe
|
import org.oxycblt.auxio.util.getDimenSizeSafe
|
||||||
import org.oxycblt.auxio.util.getDrawableSafe
|
import org.oxycblt.auxio.util.getDrawableSafe
|
||||||
|
import org.oxycblt.auxio.util.isRtl
|
||||||
|
import org.oxycblt.auxio.util.isUnder
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
* 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.
|
* 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
|
* Attributions as per the Apache 2.0 license:
|
||||||
* [https://github.com/zhanghai] PROJECT: Android Fast Scroll
|
* - ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
|
||||||
* [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/]
|
* - PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
||||||
|
* - MODIFIER: OxygenCobalt [https://github.com/oxygencobalt]
|
||||||
*
|
*
|
||||||
* !!! MODIFICATIONS !!!:
|
* !!! MODIFICATIONS !!!:
|
||||||
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with
|
* - 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
|
* - Added documentation
|
||||||
*
|
*
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @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
|
class FastScrollRecyclerView
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
EdgeRecyclerView(context, attrs, defStyleAttr) {
|
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 */
|
/** Callback to provide a string to be shown on the popup when an item is passed */
|
||||||
var popupProvider: ((Int) -> String)? = null
|
var popupProvider: ((Int) -> String)? = null
|
||||||
|
|
||||||
|
@ -86,83 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
*/
|
*/
|
||||||
var onDragListener: ((Boolean) -> Unit)? = null
|
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 {
|
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(thumbView)
|
||||||
overlay.add(popupView)
|
overlay.add(popupView)
|
||||||
|
|
||||||
|
@ -195,42 +195,33 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
private fun onPreDraw() {
|
private fun onPreDraw() {
|
||||||
updateScrollbarState()
|
updateScrollbarState()
|
||||||
|
|
||||||
trackView.layoutDirection = layoutDirection
|
|
||||||
thumbView.layoutDirection = layoutDirection
|
thumbView.layoutDirection = layoutDirection
|
||||||
popupView.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 =
|
val thumbLeft =
|
||||||
if (isRtl) {
|
if (isRtl) {
|
||||||
scrollerPadding.left
|
thumbPadding.left
|
||||||
} else {
|
} 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)
|
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
|
||||||
|
|
||||||
val firstPos = firstAdapterPos
|
val firstPos = firstAdapterPos
|
||||||
val popupText =
|
val popupText =
|
||||||
if (firstPos != NO_POSITION) {
|
if (firstPos != NO_POSITION) {
|
||||||
popupProvider?.invoke(firstPos) ?: ""
|
popupProvider?.invoke(firstPos)?.ifEmpty { null }
|
||||||
} else {
|
} 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
|
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
|
||||||
|
|
||||||
if (popupView.text != popupText) {
|
if (popupView.text != popupText) {
|
||||||
|
@ -239,8 +230,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val widthMeasureSpec =
|
val widthMeasureSpec =
|
||||||
ViewGroup.getChildMeasureSpec(
|
ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
scrollerPadding.left +
|
thumbPadding.left +
|
||||||
scrollerPadding.right +
|
thumbPadding.right +
|
||||||
thumbWidth +
|
thumbWidth +
|
||||||
popupLayoutParams.leftMargin +
|
popupLayoutParams.leftMargin +
|
||||||
popupLayoutParams.rightMargin,
|
popupLayoutParams.rightMargin,
|
||||||
|
@ -249,8 +240,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val heightMeasureSpec =
|
val heightMeasureSpec =
|
||||||
ViewGroup.getChildMeasureSpec(
|
ViewGroup.getChildMeasureSpec(
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||||
scrollerPadding.top +
|
thumbPadding.top +
|
||||||
scrollerPadding.bottom +
|
thumbPadding.bottom +
|
||||||
popupLayoutParams.topMargin +
|
popupLayoutParams.topMargin +
|
||||||
popupLayoutParams.bottomMargin,
|
popupLayoutParams.bottomMargin,
|
||||||
popupLayoutParams.height)
|
popupLayoutParams.height)
|
||||||
|
@ -262,39 +253,23 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
val popupHeight = popupView.measuredHeight
|
val popupHeight = popupView.measuredHeight
|
||||||
val popupLeft =
|
val popupLeft =
|
||||||
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||||
scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
||||||
} else {
|
} else {
|
||||||
width -
|
width -
|
||||||
scrollerPadding.right -
|
thumbPadding.right -
|
||||||
thumbWidth -
|
thumbWidth -
|
||||||
popupLayoutParams.rightMargin -
|
popupLayoutParams.rightMargin -
|
||||||
popupWidth
|
popupWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
|
val popupAnchorY = popupHeight / 2
|
||||||
@SuppressLint("RtlHardcoded")
|
val thumbAnchorY = thumbView.paddingTop
|
||||||
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 popupTop =
|
val popupTop =
|
||||||
MathUtils.clamp(
|
MathUtils.clamp(
|
||||||
thumbTop + thumbAnchorY - popupAnchorY,
|
thumbTop + thumbAnchorY - popupAnchorY,
|
||||||
scrollerPadding.top + popupLayoutParams.topMargin,
|
thumbPadding.top + popupLayoutParams.topMargin,
|
||||||
height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
|
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
|
||||||
|
|
||||||
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + 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 {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
super.onApplyWindowInsets(insets)
|
super.onApplyWindowInsets(insets)
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
scrollerPadding.bottom = bars.bottom
|
thumbPadding.bottom = bars.bottom
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,38 +320,36 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
downX = eventX
|
downX = eventX
|
||||||
downY = eventY
|
downY = eventY
|
||||||
|
|
||||||
val scrollX = trackView.scrollX
|
if (eventX >= thumbView.left && eventX < thumbView.right) {
|
||||||
val isInScrollbar =
|
|
||||||
eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX
|
|
||||||
|
|
||||||
if (trackView.alpha > 0 && isInScrollbar) {
|
|
||||||
dragStartY = eventY
|
dragStartY = eventY
|
||||||
|
|
||||||
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
if (thumbView.isUnder(eventX, eventY, minTouchTargetSize)) {
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartThumbOffset =
|
dragStartThumbOffset =
|
||||||
(eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
(eventY - thumbPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDragging(true)
|
dragging = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
if (!dragging &&
|
if (!dragging &&
|
||||||
isInViewTouchTarget(trackView, downX, downY) &&
|
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
||||||
abs(eventY - downY) > touchSlop) {
|
abs(eventY - downY) > touchSlop) {
|
||||||
if (isInViewTouchTarget(thumbView, downX, downY)) {
|
|
||||||
|
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
||||||
dragStartY = lastY
|
dragStartY = lastY
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartY = eventY
|
dragStartY = eventY
|
||||||
dragStartThumbOffset =
|
dragStartThumbOffset =
|
||||||
(eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
(eventY - thumbPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
setDragging(true)
|
|
||||||
|
dragging = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
|
@ -384,49 +357,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
scrollToThumbOffset(thumbOffset)
|
scrollToThumbOffset(thumbOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> dragging = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lastY = eventY
|
lastY = eventY
|
||||||
return dragging
|
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) {
|
private fun scrollToThumbOffset(thumbOffset: Int) {
|
||||||
val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange)
|
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 ---
|
// --- SCROLLBAR APPEARANCE ---
|
||||||
|
|
||||||
private fun postAutoHideScrollbar() {
|
private fun postAutoHideScrollbar() {
|
||||||
removeCallbacks(hideScrollbarRunnable)
|
removeCallbacks(hideThumbRunnable)
|
||||||
postDelayed(hideScrollbarRunnable, AUTO_HIDE_SCROLLBAR_DELAY_MILLIS.toLong())
|
postDelayed(hideThumbRunnable, AUTO_HIDE_SCROLLBAR_DELAY_MILLIS.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showScrollbar() {
|
private fun showScrollbar() {
|
||||||
if (showingScrollbar) {
|
if (showingThumb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showingScrollbar = true
|
showingThumb = true
|
||||||
animateView(thumbView, 1f)
|
animateView(thumbView, 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideScrollbar() {
|
private fun hideScrollbar() {
|
||||||
if (!showingScrollbar) {
|
if (!showingThumb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showingScrollbar = false
|
showingThumb = false
|
||||||
animateView(thumbView, 0f)
|
animateView(thumbView, 0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,12 +450,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
// --- LAYOUT STATE ---
|
// --- LAYOUT STATE ---
|
||||||
|
|
||||||
private val isRtl: Boolean
|
|
||||||
get() = layoutDirection == LAYOUT_DIRECTION_RTL
|
|
||||||
|
|
||||||
private val thumbOffsetRange: Int
|
private val thumbOffsetRange: Int
|
||||||
get() {
|
get() {
|
||||||
return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
return height - thumbPadding.top - thumbPadding.bottom - thumbHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scrollRange: Int
|
private val scrollRange: Int
|
||||||
|
|
|
@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
class PlaybackFragment : Fragment() {
|
class PlaybackFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
private var mLastBinding: FragmentPlaybackBinding? = null
|
private var lastBinding: FragmentPlaybackBinding? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -55,7 +55,7 @@ class PlaybackFragment : Fragment() {
|
||||||
val queueItem: MenuItem
|
val queueItem: MenuItem
|
||||||
|
|
||||||
// See onDestroyView for why we do this
|
// See onDestroyView for why we do this
|
||||||
mLastBinding = binding
|
lastBinding = binding
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
@ -157,8 +157,8 @@ class PlaybackFragment : Fragment() {
|
||||||
|
|
||||||
// playbackSong will leak if we don't disable marquee, keep the binding around
|
// 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.
|
// so that we can turn it off when we destroy the view.
|
||||||
mLastBinding?.playbackSong?.isSelected = false
|
lastBinding?.playbackSong?.isSelected = false
|
||||||
mLastBinding = null
|
lastBinding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateUp() {
|
private fun navigateUp() {
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.oxycblt.auxio.util.disableDropShadowCompat
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||||
import org.oxycblt.auxio.util.getDimenSafe
|
import org.oxycblt.auxio.util.getDimenSafe
|
||||||
import org.oxycblt.auxio.util.getDrawableSafe
|
import org.oxycblt.auxio.util.getDrawableSafe
|
||||||
|
import org.oxycblt.auxio.util.isUnder
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.pxOfDp
|
import org.oxycblt.auxio.util.pxOfDp
|
||||||
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
|
@ -458,27 +459,25 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
|
||||||
return super.onInterceptTouchEvent(ev)
|
return super.onInterceptTouchEvent(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
val adx = abs(ev.x - initMotionX)
|
|
||||||
val ady = abs(ev.y - initMotionY)
|
|
||||||
val dragSlop = dragHelper.touchSlop
|
|
||||||
|
|
||||||
when (ev.actionMasked) {
|
when (ev.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
initMotionX = ev.x
|
initMotionX = ev.x
|
||||||
initMotionY = ev.y
|
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
|
// Pointer is not on our view, do not intercept this event
|
||||||
dragHelper.cancel()
|
dragHelper.cancel()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
val pointerUnder = playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt())
|
val adx = abs(ev.x - initMotionX)
|
||||||
val motionUnder =
|
val ady = abs(ev.y - initMotionY)
|
||||||
playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt())
|
|
||||||
|
|
||||||
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
|
// Pointer has moved beyond our control, do not intercept this event
|
||||||
dragHelper.cancel()
|
dragHelper.cancel()
|
||||||
return false
|
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
|
private val ViewDragHelper.isDragging: Boolean
|
||||||
get() {
|
get() {
|
||||||
// We can't grab the drag state outside of a callback, but that's stupid and I don't
|
// We can't grab the drag state outside of a callback, but that's stupid and I don't
|
||||||
|
|
|
@ -441,6 +441,7 @@ class PlaybackService :
|
||||||
// Technically the MediaSession seems to handle bluetooth events on their
|
// 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
|
// own, but keep this around as a fallback in the case that the former fails
|
||||||
// for whatever reason.
|
// for whatever reason.
|
||||||
|
// TODO: Remove this since the headset hook KeyEvent should be fine enough.
|
||||||
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
|
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
|
||||||
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
|
when (intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)) {
|
||||||
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pauseFromPlug()
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> pauseFromPlug()
|
||||||
|
@ -459,7 +460,7 @@ class PlaybackService :
|
||||||
initialHeadsetPlugEventHandled = true
|
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()
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
|
||||||
|
|
||||||
// --- AUXIO EVENTS ---
|
// --- AUXIO EVENTS ---
|
||||||
|
@ -484,9 +485,7 @@ class PlaybackService :
|
||||||
* that friendly
|
* that friendly
|
||||||
* 2. There is a bug where playback will always start when this service starts, mostly due
|
* 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
|
* 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.
|
* 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
|
|
||||||
*/
|
*/
|
||||||
private fun maybeResumeFromPlug() {
|
private fun maybeResumeFromPlug() {
|
||||||
if (playbackManager.song != null &&
|
if (playbackManager.song != null &&
|
||||||
|
@ -497,12 +496,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Pause from a headset plug. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
private fun pauseFromPlug() {
|
private fun pauseFromPlug() {
|
||||||
if (playbackManager.song != null) {
|
if (playbackManager.song != null) {
|
||||||
logD("Device disconnected, pausing")
|
logD("Device disconnected, pausing")
|
||||||
|
|
|
@ -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
|
* @throws IllegalStateException When there is no menu for this specific datatype/flag
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
*
|
||||||
* TODO: Stop scrolling when a menu is open TODO: Prevent duplicate menus from showing up TODO:
|
* TODO: Stop scrolling when a menu is open
|
||||||
* Maybe replace this with a bottom sheet?
|
*
|
||||||
|
* TODO: Prevent duplicate menus from showing up
|
||||||
|
*
|
||||||
|
* TODO: Maybe replace this with a bottom sheet?
|
||||||
*/
|
*/
|
||||||
class ActionMenu(
|
class ActionMenu(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
|
|
|
@ -20,7 +20,9 @@ package org.oxycblt.auxio.util
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Insets
|
import android.graphics.Insets
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.annotation.ColorRes
|
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
|
* 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.
|
* that properly follows all the frustrating changes that were made between 8-11.
|
||||||
|
|
|
@ -30,8 +30,11 @@
|
||||||
<dimen name="elevation_small">2dp</dimen>
|
<dimen name="elevation_small">2dp</dimen>
|
||||||
<dimen name="elevation_normal">4dp</dimen>
|
<dimen name="elevation_normal">4dp</dimen>
|
||||||
|
|
||||||
<dimen name="popup_min_width">78dp</dimen>
|
<dimen name="fast_scroll_popup_min_width">80dp</dimen>
|
||||||
<dimen name="popup_padding_end">28dp</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_thumb_radius">6dp</dimen>
|
||||||
<dimen name="slider_halo_radius">12dp</dimen>
|
<dimen name="slider_halo_radius">12dp</dimen>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import re
|
||||||
|
|
||||||
# WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND
|
# 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.
|
# 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"
|
FLAC_VERSION = "1.3.2"
|
||||||
|
|
||||||
FATAL="\033[1;31m"
|
FATAL="\033[1;31m"
|
||||||
|
|
Loading…
Reference in a new issue