all: document custom stuff
Document and clean up PlaybackBarLayout and the fast scroll views to an extent.
This commit is contained in:
parent
8b8d36cf22
commit
1b79eb11e0
11 changed files with 342 additions and 267 deletions
|
@ -83,7 +83,7 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||||
if (excludedModel.isModified()) {
|
if (excludedModel.isModified) {
|
||||||
saveAndRestart()
|
saveAndRestart()
|
||||||
} else {
|
} else {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|
|
@ -33,13 +33,17 @@ import kotlinx.coroutines.withContext
|
||||||
* of paths. Use [Factory] to instantiate this.
|
* of paths. Use [Factory] to instantiate this.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedViewModel(context: Context) : ViewModel() {
|
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
||||||
private val mPaths = MutableLiveData(mutableListOf<String>())
|
private val mPaths = MutableLiveData(mutableListOf<String>())
|
||||||
val paths: LiveData<MutableList<String>> get() = mPaths
|
val paths: LiveData<MutableList<String>> get() = mPaths
|
||||||
|
|
||||||
private val excludedDatabase = ExcludedDatabase.getInstance(context)
|
|
||||||
private var dbPaths = listOf<String>()
|
private var dbPaths = listOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if changes have been made to the ViewModel's paths.
|
||||||
|
*/
|
||||||
|
val isModified: Boolean get() = dbPaths != paths.value
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadDatabasePaths()
|
loadDatabasePaths()
|
||||||
}
|
}
|
||||||
|
@ -89,11 +93,6 @@ class ExcludedViewModel(context: Context) : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if changes have been made to the ViewModel's paths.
|
|
||||||
*/
|
|
||||||
fun isModified() = dbPaths != paths.value
|
|
||||||
|
|
||||||
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
check(modelClass.isAssignableFrom(ExcludedViewModel::class.java)) {
|
check(modelClass.isAssignableFrom(ExcludedViewModel::class.java)) {
|
||||||
|
@ -101,7 +100,7 @@ class ExcludedViewModel(context: Context) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return ExcludedViewModel(context) as T
|
return ExcludedViewModel(ExcludedDatabase.getInstance(context)) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,112 +46,114 @@ import kotlin.math.sqrt
|
||||||
* !!! MODIFICATIONS !!!:
|
* !!! MODIFICATIONS !!!:
|
||||||
* - Use modified Auxio resources instead of AFS resources
|
* - Use modified Auxio resources instead of AFS resources
|
||||||
* - Variable names are no longer prefixed with m
|
* - Variable names are no longer prefixed with m
|
||||||
* - Suppressed deprecation warning when dealing with convexness
|
* - Made path management compat-friendly
|
||||||
*/
|
*/
|
||||||
class Md2PopupBackground(context: Context) : Drawable() {
|
class FastScrollPopupDrawable(context: Context) : Drawable() {
|
||||||
private val paint: Paint = Paint()
|
private val paint: Paint = Paint().apply {
|
||||||
private val paddingStart: Int
|
isAntiAlias = true
|
||||||
private val paddingEnd: Int
|
color = R.attr.colorControlActivated.resolveAttr(context)
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
private val path = Path()
|
private val path = Path()
|
||||||
private val tempMatrix = Matrix()
|
private val matrix = Matrix()
|
||||||
|
|
||||||
|
private val paddingStart = context.resources.getDimensionPixelOffset(R.dimen.spacing_medium)
|
||||||
|
private val paddingEnd = context.resources.getDimensionPixelOffset(R.dimen.popup_padding_end)
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
override fun draw(canvas: Canvas) {
|
||||||
canvas.drawPath(path, paint)
|
canvas.drawPath(path, paint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
updatePath()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
|
override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
|
||||||
updatePath()
|
updatePath()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setAlpha(alpha: Int) {}
|
@Suppress("DEPRECATION")
|
||||||
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
override fun getOutline(outline: Outline) {
|
||||||
override fun isAutoMirrored(): Boolean {
|
when {
|
||||||
return true
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> outline.setPath(path)
|
||||||
}
|
|
||||||
|
|
||||||
private fun needMirroring(): Boolean {
|
// Paths don't need to be convex on android Q, but the API was mislabeled and so
|
||||||
return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
// we still have to use this method.
|
||||||
}
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path)
|
||||||
|
|
||||||
override fun getOpacity(): Int {
|
else -> if (!path.isConvex) {
|
||||||
return PixelFormat.TRANSLUCENT
|
// The outline path must be convex before Q, but we may run into floating point
|
||||||
}
|
// error caused by calculations involving sqrt(2) or OEM implementation differences,
|
||||||
|
// so in this case we just omit the shadow instead of crashing.
|
||||||
override fun onBoundsChange(bounds: Rect) {
|
super.getOutline(outline)
|
||||||
updatePath()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePath() {
|
|
||||||
path.reset()
|
|
||||||
val bounds = bounds
|
|
||||||
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 (needMirroring()) {
|
|
||||||
tempMatrix.setScale(-1f, 1f, width / 2, 0f)
|
|
||||||
} else {
|
|
||||||
tempMatrix.reset()
|
|
||||||
}
|
}
|
||||||
tempMatrix.postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
|
|
||||||
path.transform(tempMatrix)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPadding(padding: Rect): Boolean {
|
override fun getPadding(padding: Rect): Boolean {
|
||||||
if (needMirroring()) {
|
if (isRtl) {
|
||||||
padding[paddingEnd, 0, paddingStart] = 0
|
padding[paddingEnd, 0, paddingStart] = 0
|
||||||
} else {
|
} else {
|
||||||
padding[paddingStart, 0, paddingEnd] = 0
|
padding[paddingStart, 0, paddingEnd] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
override fun isAutoMirrored(): Boolean = true
|
||||||
override fun getOutline(outline: Outline) {
|
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !path.isConvex) {
|
override fun setAlpha(alpha: Int) {}
|
||||||
// The outline path must be convex before Q, but we may run into floating point error
|
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
||||||
// caused by calculation involving sqrt(2) or OEM implementation difference, so in this
|
|
||||||
// case we just omit the shadow instead of crashing.
|
private fun updatePath() {
|
||||||
super.getOutline(outline)
|
path.reset()
|
||||||
return
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
outline.setConvexPath(path)
|
matrix.postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
|
||||||
|
|
||||||
|
path.transform(matrix)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun pathArcTo(
|
||||||
private fun pathArcTo(
|
path: Path,
|
||||||
path: Path,
|
centerX: Float,
|
||||||
centerX: Float,
|
centerY: Float,
|
||||||
centerY: Float,
|
radius: Float,
|
||||||
radius: Float,
|
startAngle: Float,
|
||||||
startAngle: Float,
|
sweepAngle: Float
|
||||||
sweepAngle: Float
|
) {
|
||||||
) {
|
path.arcTo(
|
||||||
path.arcTo(
|
centerX - radius, centerY - radius, centerX + radius, centerY + radius,
|
||||||
centerX - radius, centerY - radius, centerX + radius, centerY + radius,
|
startAngle, sweepAngle, false
|
||||||
startAngle, sweepAngle, false
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
private val isRtl: Boolean get() =
|
||||||
paint.isAntiAlias = true
|
DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
||||||
paint.color = R.attr.colorControlActivated.resolveAttr(context)
|
|
||||||
paint.style = Paint.Style.FILL
|
|
||||||
val resources = context.resources
|
|
||||||
paddingStart = resources.getDimensionPixelOffset(R.dimen.spacing_medium)
|
|
||||||
paddingEnd = resources.getDimensionPixelOffset(R.dimen.popup_padding_end)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -142,7 +142,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
|
|
||||||
setLayoutParams(layoutParams)
|
setLayoutParams(layoutParams)
|
||||||
|
|
||||||
background = Md2PopupBackground(context)
|
background = FastScrollPopupDrawable(context)
|
||||||
elevation = resources.getDimensionPixelOffset(R.dimen.elevation_normal).toFloat()
|
elevation = resources.getDimensionPixelOffset(R.dimen.elevation_normal).toFloat()
|
||||||
ellipsize = TextUtils.TruncateAt.MIDDLE
|
ellipsize = TextUtils.TruncateAt.MIDDLE
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
|
@ -335,7 +335,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
getDecoratedBoundsWithMargins(getChildAt(0), childRect)
|
getDecoratedBoundsWithMargins(getChildAt(0), childRect)
|
||||||
val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - childRect.top
|
val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - childRect.top
|
||||||
|
|
||||||
// Finally, we can calculate the thumb position, which is just:
|
// Then calculate the thumb position, which is just:
|
||||||
// [proportion of scroll position to scroll range] * [total thumb range]
|
// [proportion of scroll position to scroll range] * [total thumb range]
|
||||||
thumbOffset = (thumbOffsetRange.toLong() * scrollOffset / scrollOffsetRange).toInt()
|
thumbOffset = (thumbOffsetRange.toLong() * scrollOffset / scrollOffsetRange).toInt()
|
||||||
}
|
}
|
||||||
|
@ -396,25 +396,14 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isInViewTouchTarget(view: View, x: Float, y: Float): Boolean {
|
private fun isInViewTouchTarget(view: View, x: Float, y: Float): Boolean {
|
||||||
val scrollX = scrollX
|
return isInTouchTarget(x, view.left - scrollX, view.right - scrollX, width) &&
|
||||||
val scrollY = scrollY
|
isInTouchTarget(y, view.top - scrollY, view.bottom - scrollY, height)
|
||||||
return (
|
|
||||||
isInTouchTarget(
|
|
||||||
x, view.left - scrollX, view.right - scrollX, 0,
|
|
||||||
width
|
|
||||||
) &&
|
|
||||||
isInTouchTarget(
|
|
||||||
y, view.top - scrollY, view.bottom - scrollY, 0,
|
|
||||||
height
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isInTouchTarget(
|
private fun isInTouchTarget(
|
||||||
position: Float,
|
position: Float,
|
||||||
viewStart: Int,
|
viewStart: Int,
|
||||||
viewEnd: Int,
|
viewEnd: Int,
|
||||||
parentStart: Int,
|
|
||||||
parentEnd: Int
|
parentEnd: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val viewSize = viewEnd - viewStart
|
val viewSize = viewEnd - viewStart
|
||||||
|
@ -424,16 +413,18 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
|
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
|
||||||
if (touchTargetStart < parentStart) {
|
|
||||||
touchTargetStart = parentStart
|
if (touchTargetStart < 0) {
|
||||||
|
touchTargetStart = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var touchTargetEnd = touchTargetStart + minTouchTargetSize
|
var touchTargetEnd = touchTargetStart + minTouchTargetSize
|
||||||
if (touchTargetEnd > parentEnd) {
|
if (touchTargetEnd > parentEnd) {
|
||||||
touchTargetEnd = parentEnd
|
touchTargetEnd = parentEnd
|
||||||
touchTargetStart = touchTargetEnd - minTouchTargetSize
|
touchTargetStart = touchTargetEnd - minTouchTargetSize
|
||||||
if (touchTargetStart < parentStart) {
|
|
||||||
touchTargetStart = parentStart
|
if (touchTargetStart < 0) {
|
||||||
|
touchTargetStart = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,10 +432,11 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollToThumbOffset(thumbOffset: Int) {
|
private fun scrollToThumbOffset(thumbOffset: Int) {
|
||||||
var newThumbOffset = thumbOffset
|
val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange)
|
||||||
newThumbOffset = MathUtils.clamp(newThumbOffset, 0, thumbOffsetRange)
|
|
||||||
var scrollOffset = (scrollOffsetRange.toLong() * newThumbOffset / thumbOffsetRange).toInt()
|
val scrollOffset = (
|
||||||
scrollOffset -= paddingTop
|
scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange
|
||||||
|
).toInt() - paddingTop
|
||||||
|
|
||||||
scrollTo(scrollOffset)
|
scrollTo(scrollOffset)
|
||||||
}
|
}
|
||||||
|
@ -454,6 +446,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
|
|
||||||
val trueOffset = offset - paddingTop
|
val trueOffset = offset - paddingTop
|
||||||
val itemHeight = itemHeight
|
val itemHeight = itemHeight
|
||||||
|
|
||||||
val firstItemPosition = 0.coerceAtLeast(trueOffset / itemHeight)
|
val firstItemPosition = 0.coerceAtLeast(trueOffset / itemHeight)
|
||||||
val firstItemTop = firstItemPosition * itemHeight - trueOffset
|
val firstItemTop = firstItemPosition * itemHeight - trueOffset
|
||||||
|
|
||||||
|
@ -557,32 +550,11 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
private val isRtl: Boolean
|
private val isRtl: Boolean
|
||||||
get() = layoutDirection == LAYOUT_DIRECTION_RTL
|
get() = layoutDirection == LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
private val scrollOffsetRange: Int
|
|
||||||
get() = scrollRange - height
|
|
||||||
|
|
||||||
private val thumbOffsetRange: Int
|
private val thumbOffsetRange: Int
|
||||||
get() {
|
get() {
|
||||||
return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
return height - scrollerPadding.top - scrollerPadding.bottom - thumbHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
private val itemCount: Int
|
|
||||||
get() = when (val mgr = layoutManager) {
|
|
||||||
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
|
|
||||||
is LinearLayoutManager -> mgr.itemCount
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private val itemHeight: Int
|
|
||||||
get() {
|
|
||||||
if (childCount == 0) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val itemView = getChildAt(0)
|
|
||||||
getDecoratedBoundsWithMargins(itemView, childRect)
|
|
||||||
return childRect.height()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scrollRange: Int
|
private val scrollRange: Int
|
||||||
get() {
|
get() {
|
||||||
val itemCount = itemCount
|
val itemCount = itemCount
|
||||||
|
@ -600,6 +572,9 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val scrollOffsetRange: Int
|
||||||
|
get() = scrollRange - height
|
||||||
|
|
||||||
private val firstAdapterPos: Int
|
private val firstAdapterPos: Int
|
||||||
get() {
|
get() {
|
||||||
if (childCount == 0) {
|
if (childCount == 0) {
|
||||||
|
@ -615,6 +590,24 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val itemHeight: Int
|
||||||
|
get() {
|
||||||
|
if (childCount == 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val itemView = getChildAt(0)
|
||||||
|
getDecoratedBoundsWithMargins(itemView, childRect)
|
||||||
|
return childRect.height()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val itemCount: Int
|
||||||
|
get() = when (val mgr = layoutManager) {
|
||||||
|
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
|
||||||
|
is LinearLayoutManager -> mgr.itemCount
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ANIM_MILLIS = 150L
|
private const val ANIM_MILLIS = 150L
|
||||||
private const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500
|
private const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500
|
||||||
|
|
|
@ -44,10 +44,10 @@ sealed class Music : BaseModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [BaseModel] variant that denotes that this object is a parent of other data objects, such
|
* [Music] variant that denotes that this object is a parent of other data objects, such
|
||||||
* as an [Album] or [Artist]
|
* as an [Album] or [Artist]
|
||||||
* @property resolvedName A name resolved from it's raw form to a form suitable to be shown in
|
* @property resolvedName A name resolved from it's raw form to a form suitable to be shown in
|
||||||
* a ui. Ex. unknown would become Unknown Artist, (124) would become its proper genre name, etc.
|
* a ui. Ex. "unknown" would become Unknown Artist, (124) would become its proper genre name, etc.
|
||||||
*/
|
*/
|
||||||
sealed class MusicParent : Music() {
|
sealed class MusicParent : Music() {
|
||||||
abstract val resolvedName: String
|
abstract val resolvedName: String
|
||||||
|
@ -200,10 +200,55 @@ data class Genre(
|
||||||
override val hash = name.hashCode().toLong()
|
override val hash = name.hashCode().toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data object used solely for the "Header" UI element.
|
||||||
|
* @see HeaderString
|
||||||
|
*/
|
||||||
|
data class Header(
|
||||||
|
override val id: Long,
|
||||||
|
val string: HeaderString
|
||||||
|
) : BaseModel()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data object used for an action header. Like [Header], but with a button.
|
||||||
|
* @see Header
|
||||||
|
*/
|
||||||
|
data class ActionHeader(
|
||||||
|
override val id: Long,
|
||||||
|
val string: HeaderString,
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
|
@StringRes val desc: Int,
|
||||||
|
val onClick: (View) -> Unit,
|
||||||
|
) : BaseModel() {
|
||||||
|
// JVM can't into comparing lambdas, so we override equals/hashCode and exclude them.
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is ActionHeader) return false
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (string != other.string) return false
|
||||||
|
if (icon != other.icon) return false
|
||||||
|
if (desc != other.desc) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + string.hashCode()
|
||||||
|
result = 31 * result + icon
|
||||||
|
result = 31 * result + desc
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The string used for a header instance. This class is a bit complex, mostly because it revolves
|
* The string used for a header instance. This class is a bit complex, mostly because it revolves
|
||||||
* around passing string resources that are then resolved by the view instead of passing a context
|
* around passing string resources that are then resolved by the view. This is because ViewModel
|
||||||
* directly.
|
* instance should preferably not have access to a Context but should still generate data,
|
||||||
|
* which at times can include [Header] instances that require string resources.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
sealed class HeaderString {
|
sealed class HeaderString {
|
||||||
|
@ -272,47 +317,3 @@ sealed class HeaderString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A data object used solely for the "Header" UI element.
|
|
||||||
* @see HeaderString
|
|
||||||
*/
|
|
||||||
data class Header(
|
|
||||||
override val id: Long,
|
|
||||||
val string: HeaderString
|
|
||||||
) : BaseModel()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data object used for an action header. Like [Header], but with a button.
|
|
||||||
* @see HeaderString
|
|
||||||
*/
|
|
||||||
data class ActionHeader(
|
|
||||||
override val id: Long,
|
|
||||||
val string: HeaderString,
|
|
||||||
@DrawableRes val icon: Int,
|
|
||||||
@StringRes val desc: Int,
|
|
||||||
val onClick: (View) -> Unit,
|
|
||||||
) : BaseModel() {
|
|
||||||
// JVM can't into comparing lambdas, so we override equals/hashCode and exclude them.
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is ActionHeader) return false
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
|
||||||
if (string != other.string) return false
|
|
||||||
if (icon != other.icon) return false
|
|
||||||
if (desc != other.desc) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + string.hashCode()
|
|
||||||
result = 31 * result + icon
|
|
||||||
result = 31 * result + desc
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
* have to query for each genre, query all the songs in each genre, and then iterate through those
|
* have to query for each genre, query all the songs in each genre, and then iterate through those
|
||||||
* songs to link every song with their genre. This is not documented anywhere, and the
|
* songs to link every song with their genre. This is not documented anywhere, and the
|
||||||
* O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's
|
* O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's
|
||||||
* loading times. At no point have the devs considered that this column is absolutely busted, and
|
* loading times. At no point have the devs considered that this column is absolutely insane, and
|
||||||
* instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their
|
* instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their
|
||||||
* own Google Play Music, and we all know how great that worked out!
|
* own Google Play Music, and we all know how great that worked out!
|
||||||
*
|
*
|
||||||
|
@ -221,7 +221,7 @@ class MusicLoader(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
songs = songs.distinctBy {
|
songs = songs.distinctBy {
|
||||||
it.name to it.albumId to it.artistName to it.track to it.duration
|
it.name to it.albumName to it.artistName to it.track to it.duration
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
logD("Song search finished with ${songs.size} found")
|
logD("Song search finished with ${songs.size} found")
|
||||||
|
|
|
@ -22,7 +22,9 @@ import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.drawable.RippleDrawable
|
import android.graphics.drawable.RippleDrawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.WindowInsets
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ViewCompactPlaybackBinding
|
import org.oxycblt.auxio.databinding.ViewCompactPlaybackBinding
|
||||||
|
@ -30,6 +32,7 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.resolveAttr
|
import org.oxycblt.auxio.util.resolveAttr
|
||||||
import org.oxycblt.auxio.util.resolveDrawable
|
import org.oxycblt.auxio.util.resolveDrawable
|
||||||
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view displaying the playback state in a compact manner. This is only meant to be used
|
* A view displaying the playback state in a compact manner. This is only meant to be used
|
||||||
|
@ -82,6 +85,11 @@ class CompactPlaybackView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
updatePadding(bottom = insets.systemBarsCompat.bottom)
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
fun setSong(song: Song) {
|
fun setSong(song: Song) {
|
||||||
binding.song = song
|
binding.song = song
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
|
|
|
@ -28,7 +28,6 @@ import android.view.WindowInsets
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.customview.widget.ViewDragHelper
|
import androidx.customview.widget.ViewDragHelper
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.systemBarsCompat
|
import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
|
@ -36,11 +35,12 @@ import org.oxycblt.auxio.util.systemBarsCompat
|
||||||
/**
|
/**
|
||||||
* A layout that manages the bottom playback bar while still enabling edge-to-edge to work
|
* A layout that manages the bottom playback bar while still enabling edge-to-edge to work
|
||||||
* properly. The mechanism is mostly inspired by Material Files' PersistentBarLayout, however
|
* properly. The mechanism is mostly inspired by Material Files' PersistentBarLayout, however
|
||||||
* this class was primarily written by me and I plan to expand this layout to become part of
|
* this class was primarily written by me.
|
||||||
* the playback navigation process.
|
|
||||||
*
|
*
|
||||||
* TODO: Explain how this thing works so that others can be spared the pain of deciphering
|
* TODO: Add a swipe-up behavior a la Phonograph. I think that would improve UX.
|
||||||
* this custom viewgroup
|
* TODO: Leverage this layout to make more tablet-friendly UIs
|
||||||
|
*
|
||||||
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackBarLayout @JvmOverloads constructor(
|
class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -49,12 +49,14 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
@StyleRes defStyleRes: Int = 0
|
@StyleRes defStyleRes: Int = 0
|
||||||
) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
|
) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
private val playbackView = CompactPlaybackView(context)
|
private val playbackView = CompactPlaybackView(context)
|
||||||
private var barDragHelper = ViewDragHelper.create(this, ViewDragCallback())
|
private var barDragHelper = ViewDragHelper.create(this, BarDragCallback())
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addView(playbackView)
|
addView(playbackView)
|
||||||
|
|
||||||
|
// playbackView is special as it's the view we treat as a bottom bar.
|
||||||
|
// Mark it as such.
|
||||||
(playbackView.layoutParams as LayoutParams).apply {
|
(playbackView.layoutParams as LayoutParams).apply {
|
||||||
width = ViewGroup.LayoutParams.MATCH_PARENT
|
width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
@ -70,16 +72,21 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
|
|
||||||
setMeasuredDimension(widthSize, heightSize)
|
setMeasuredDimension(widthSize, heightSize)
|
||||||
|
|
||||||
|
// Measure the bar view so that it fills the whole screen and takes up the bottom views.
|
||||||
val barParams = playbackView.layoutParams as LayoutParams
|
val barParams = playbackView.layoutParams as LayoutParams
|
||||||
|
|
||||||
val barWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, barParams.width)
|
val barWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, barParams.width)
|
||||||
val barHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, barParams.height)
|
val barHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, barParams.height)
|
||||||
playbackView.measure(barWidthSpec, barHeightSpec)
|
playbackView.measure(barWidthSpec, barHeightSpec)
|
||||||
|
|
||||||
updateWindowInsets()
|
applyContentWindowInsets()
|
||||||
measureContent()
|
measureContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure the content views in this layout. This is done separately as at times we want
|
||||||
|
* to relayout the content views but not relayout the bar view.
|
||||||
|
*/
|
||||||
private fun measureContent() {
|
private fun measureContent() {
|
||||||
val barParams = playbackView.layoutParams as LayoutParams
|
val barParams = playbackView.layoutParams as LayoutParams
|
||||||
val barHeightAdjusted = (playbackView.measuredHeight * barParams.offset).toInt()
|
val barHeightAdjusted = (playbackView.measuredHeight * barParams.offset).toInt()
|
||||||
|
@ -103,24 +110,22 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
|
|
||||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||||
val barHeight = playbackView.measuredHeight
|
val barHeight = playbackView.measuredHeight
|
||||||
val barHeightAdjusted = (barHeight * (playbackView.layoutParams as LayoutParams).offset).toInt()
|
val barParams = (playbackView.layoutParams as LayoutParams)
|
||||||
|
val barHeightAdjusted = (barHeight * barParams.offset).toInt()
|
||||||
|
|
||||||
for (child in children) {
|
// Again, lay out our view like we measured it.
|
||||||
if (child.visibility == View.GONE) continue
|
playbackView.layout(
|
||||||
|
0, height - barHeightAdjusted,
|
||||||
val childParams = child.layoutParams as LayoutParams
|
width, height + (barHeight - barHeightAdjusted)
|
||||||
|
)
|
||||||
if (childParams.isBar) {
|
|
||||||
child.layout(
|
|
||||||
0, height - barHeightAdjusted,
|
|
||||||
width, height + (barHeight - barHeightAdjusted)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutContent()
|
layoutContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout the content views in this layout. This is done separately as at times we want
|
||||||
|
* to relayout the content views but not relayout the bar view.
|
||||||
|
*/
|
||||||
private fun layoutContent() {
|
private fun layoutContent() {
|
||||||
for (child in children) {
|
for (child in children) {
|
||||||
val childParams = child.layoutParams as LayoutParams
|
val childParams = child.layoutParams as LayoutParams
|
||||||
|
@ -132,15 +137,79 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
playbackView.updatePadding(bottom = insets.systemBarsCompat.bottom)
|
// Applying window insets is the real special sauce of this layout. The problem with
|
||||||
|
// having a bottom bar is that if you support edge-to-edge, applying insets to views
|
||||||
|
// will result in spacing being incorrect whenever the bar is shown. If you cleverly
|
||||||
|
// modify the insets however, you can make all content views remove their spacing as
|
||||||
|
// the bar enters. This function itself is unimportant, so you should probably take
|
||||||
|
// a look at applyContentWindowInsets and adjustInsets instead.
|
||||||
|
playbackView.onApplyWindowInsets(insets)
|
||||||
|
|
||||||
lastInsets = insets
|
lastInsets = insets
|
||||||
updateWindowInsets()
|
applyContentWindowInsets()
|
||||||
|
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply window insets to the content views in this layouts. This is done separately as at
|
||||||
|
* times we want to relayout the content views but not relayout the bar view.
|
||||||
|
*/
|
||||||
|
private fun applyContentWindowInsets() {
|
||||||
|
val insets = lastInsets
|
||||||
|
|
||||||
|
if (insets != null) {
|
||||||
|
val adjustedInsets = adjustInsets(insets)
|
||||||
|
|
||||||
|
for (child in children) {
|
||||||
|
val childParams = child.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
if (!childParams.isBar) {
|
||||||
|
child.dispatchApplyWindowInsets(adjustedInsets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust window insets to line up with the playback bar
|
||||||
|
*/
|
||||||
|
private fun adjustInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
// Find how much space the bar is consuming right now. We use this to modify
|
||||||
|
// the bottom window inset so that the spacing checks out, 0 if the bar is fully
|
||||||
|
// shown and the original value if the bar is hidden.
|
||||||
|
val barParams = playbackView.layoutParams as LayoutParams
|
||||||
|
val barConsumedInset = (playbackView.measuredHeight * barParams.offset).toInt()
|
||||||
|
|
||||||
|
val bars = insets.systemBarsCompat
|
||||||
|
val adjustedBottomInset = (bars.bottom - barConsumedInset).coerceAtLeast(0)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// Android R. Modify insets using their new method that exists for no reason
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
|
WindowInsets.Builder(insets)
|
||||||
|
.setInsets(
|
||||||
|
WindowInsets.Type.systemBars(),
|
||||||
|
Insets.of(bars.left, bars.top, bars.right, adjustedBottomInset)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android O. Modify insets using the original method
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
insets.replaceSystemWindowInsets(
|
||||||
|
bars.left, bars.top, bars.right, adjustedBottomInset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun computeScroll() {
|
override fun computeScroll() {
|
||||||
|
// Copied this from MaterialFiles.
|
||||||
|
// Don't know what this does, but it seems important so I just keep it around.
|
||||||
if (barDragHelper.continueSettling(true)) {
|
if (barDragHelper.continueSettling(true)) {
|
||||||
postInvalidateOnAnimation()
|
postInvalidateOnAnimation()
|
||||||
}
|
}
|
||||||
|
@ -149,52 +218,15 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
// Prevent memory leaks
|
||||||
playbackView.clearCallback()
|
playbackView.clearCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWindowInsets() {
|
/**
|
||||||
val insets = lastInsets
|
* Update the song that this layout is showing. This will be reflected in the compact view
|
||||||
|
* at the bottom of the screen.
|
||||||
if (insets != null) {
|
* @param animate Whether to animate bar showing/hiding events.
|
||||||
val adjustedInsets = adjustInsets(insets)
|
*/
|
||||||
|
|
||||||
for (child in children) {
|
|
||||||
child.dispatchApplyWindowInsets(adjustedInsets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustInsets(insets: WindowInsets): WindowInsets {
|
|
||||||
val barParams = playbackView.layoutParams as LayoutParams
|
|
||||||
val childConsumedInset = (playbackView.measuredHeight * barParams.offset).toInt()
|
|
||||||
|
|
||||||
val bars = insets.systemBarsCompat
|
|
||||||
|
|
||||||
return when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
|
||||||
WindowInsets.Builder(insets)
|
|
||||||
.setInsets(
|
|
||||||
WindowInsets.Type.systemBars(),
|
|
||||||
Insets.of(
|
|
||||||
bars.left, bars.top,
|
|
||||||
bars.right, (bars.bottom - childConsumedInset).coerceAtLeast(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
insets.replaceSystemWindowInsets(
|
|
||||||
bars.left, bars.top,
|
|
||||||
bars.right, (bars.bottom - childConsumedInset).coerceAtLeast(0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSong(song: Song?, animate: Boolean = false) {
|
fun setSong(song: Song?, animate: Boolean = false) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
showBar(animate)
|
showBar(animate)
|
||||||
|
@ -204,14 +236,25 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the playing status on this layout. This will be reflected in the compact view
|
||||||
|
* at the bottom of the screen.
|
||||||
|
*/
|
||||||
fun setPlaying(isPlaying: Boolean) {
|
fun setPlaying(isPlaying: Boolean) {
|
||||||
playbackView.setPlaying(isPlaying)
|
playbackView.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the playback positon on this layout. This will be reflected in the compact view
|
||||||
|
* at the bottom of the screen.
|
||||||
|
*/
|
||||||
fun setPosition(position: Long) {
|
fun setPosition(position: Long) {
|
||||||
playbackView.setPosition(position)
|
playbackView.setPosition(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a callback for actions from the compact playback view in this layout.
|
||||||
|
*/
|
||||||
fun setActionCallback(callback: ActionCallback) {
|
fun setActionCallback(callback: ActionCallback) {
|
||||||
playbackView.setCallback(callback)
|
playbackView.setCallback(callback)
|
||||||
}
|
}
|
||||||
|
@ -220,20 +263,25 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
val barParams = playbackView.layoutParams as LayoutParams
|
val barParams = playbackView.layoutParams as LayoutParams
|
||||||
|
|
||||||
if (barParams.shown || barParams.offset == 1f) {
|
if (barParams.shown || barParams.offset == 1f) {
|
||||||
|
// Already showed the bar, don't do it again.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
barParams.shown = true
|
barParams.shown = true
|
||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
|
// Animate, use our drag helper to slide the view upwards. All invalidation is done
|
||||||
|
// in the callback.
|
||||||
barDragHelper.smoothSlideViewTo(
|
barDragHelper.smoothSlideViewTo(
|
||||||
playbackView, playbackView.left, height - playbackView.height
|
playbackView, playbackView.left, height - playbackView.height
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// Don't animate, snap the view and invalidate the content views if we are already
|
||||||
|
// laid out. Otherwise we will do it later so don't waste time now.
|
||||||
barParams.offset = 1f
|
barParams.offset = 1f
|
||||||
|
|
||||||
if (isLaidOut) {
|
if (isLaidOut) {
|
||||||
updateWindowInsets()
|
applyContentWindowInsets()
|
||||||
measureContent()
|
measureContent()
|
||||||
layoutContent()
|
layoutContent()
|
||||||
}
|
}
|
||||||
|
@ -246,20 +294,25 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
val barParams = playbackView.layoutParams as LayoutParams
|
val barParams = playbackView.layoutParams as LayoutParams
|
||||||
|
|
||||||
if (barParams.shown || barParams.offset == 0f) {
|
if (barParams.shown || barParams.offset == 0f) {
|
||||||
|
// Already hid the bar, don't do it again.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
barParams.shown = false
|
barParams.shown = false
|
||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
|
// Animate, use our drag helper to slide the view upwards. All invalidation is done
|
||||||
|
// in the callback.
|
||||||
barDragHelper.smoothSlideViewTo(
|
barDragHelper.smoothSlideViewTo(
|
||||||
playbackView, playbackView.left, height
|
playbackView, playbackView.left, height
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// Don't animate, snap the view and invalidate the content views if we are already
|
||||||
|
// laid out. Otherwise we will do it later so don't waste time now.
|
||||||
barParams.offset = 0f
|
barParams.offset = 0f
|
||||||
|
|
||||||
if (isLaidOut) {
|
if (isLaidOut) {
|
||||||
updateWindowInsets()
|
applyContentWindowInsets()
|
||||||
measureContent()
|
measureContent()
|
||||||
layoutContent()
|
layoutContent()
|
||||||
}
|
}
|
||||||
|
@ -289,7 +342,25 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
override fun checkLayoutParams(layoutParams: ViewGroup.LayoutParams): Boolean =
|
override fun checkLayoutParams(layoutParams: ViewGroup.LayoutParams): Boolean =
|
||||||
layoutParams is LayoutParams && super.checkLayoutParams(layoutParams)
|
layoutParams is LayoutParams && super.checkLayoutParams(layoutParams)
|
||||||
|
|
||||||
class LayoutParams : ViewGroup.LayoutParams {
|
/**
|
||||||
|
* A callback for actions done from this view. This fragment can inherit this and recieve
|
||||||
|
* updates from the compact playback view in this layout that can then be sent to the
|
||||||
|
* internal playback engine.
|
||||||
|
*
|
||||||
|
* There is no need to clear this callback when done, the view clears it itself when the
|
||||||
|
* view is detached.
|
||||||
|
*/
|
||||||
|
interface ActionCallback {
|
||||||
|
fun onPlayPauseClick()
|
||||||
|
fun onNavToItem()
|
||||||
|
fun onNavToPlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout parameters for this layout. This layout is meant to be a black box with only two
|
||||||
|
* types of views, so this implementation is kept private.
|
||||||
|
*/
|
||||||
|
private class LayoutParams : ViewGroup.LayoutParams {
|
||||||
var isBar = false
|
var isBar = false
|
||||||
var shown = false
|
var shown = false
|
||||||
var offset = 0f
|
var offset = 0f
|
||||||
|
@ -303,13 +374,11 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
constructor(source: ViewGroup.LayoutParams) : super(source)
|
constructor(source: ViewGroup.LayoutParams) : super(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionCallback {
|
/**
|
||||||
fun onPlayPauseClick()
|
* Internal drag callback for animating the bar view showing/hiding.
|
||||||
fun onNavToItem()
|
*/
|
||||||
fun onNavToPlayback()
|
private inner class BarDragCallback : ViewDragHelper.Callback() {
|
||||||
}
|
// We aren't actually dragging things. Ignore this.
|
||||||
|
|
||||||
private inner class ViewDragCallback : ViewDragHelper.Callback() {
|
|
||||||
override fun tryCaptureView(child: View, pointerId: Int): Boolean = false
|
override fun tryCaptureView(child: View, pointerId: Int): Boolean = false
|
||||||
|
|
||||||
override fun onViewPositionChanged(
|
override fun onViewPositionChanged(
|
||||||
|
@ -320,12 +389,13 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
dy: Int
|
dy: Int
|
||||||
) {
|
) {
|
||||||
val childRange = getViewVerticalDragRange(changedView)
|
val childRange = getViewVerticalDragRange(changedView)
|
||||||
val childLayoutParams = changedView.layoutParams as LayoutParams
|
val childParams = changedView.layoutParams as LayoutParams
|
||||||
|
|
||||||
val height = height
|
// Find the new offset that this view takes up after an animation frame.
|
||||||
childLayoutParams.offset = (height - top).toFloat() / childRange
|
childParams.offset = (height - top).toFloat() / childRange
|
||||||
|
|
||||||
updateWindowInsets()
|
// Invalidate our content views so that they accurately reflect the bar now.
|
||||||
|
applyContentWindowInsets()
|
||||||
measureContent()
|
measureContent()
|
||||||
layoutContent()
|
layoutContent()
|
||||||
}
|
}
|
||||||
|
@ -333,13 +403,14 @@ class PlaybackBarLayout @JvmOverloads constructor(
|
||||||
override fun getViewVerticalDragRange(child: View): Int {
|
override fun getViewVerticalDragRange(child: View): Int {
|
||||||
val childParams = child.layoutParams as LayoutParams
|
val childParams = child.layoutParams as LayoutParams
|
||||||
|
|
||||||
return if (childParams.isBar) {
|
// Sanity check
|
||||||
child.height
|
check(childParams.isBar) { "This drag helper is only meant for content views" }
|
||||||
} else {
|
|
||||||
0
|
return child.height
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't really know what these do but they're needed
|
||||||
|
|
||||||
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = child.left
|
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = child.left
|
||||||
|
|
||||||
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
|
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.annotation.SuppressLint
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -157,6 +158,8 @@ class QueueAdapter(
|
||||||
override fun onBind(data: Song) {
|
override fun onBind(data: Song) {
|
||||||
binding.song = data
|
binding.song = data
|
||||||
|
|
||||||
|
binding.background.isInvisible = true
|
||||||
|
|
||||||
binding.songName.requestLayout()
|
binding.songName.requestLayout()
|
||||||
binding.songInfo.requestLayout()
|
binding.songInfo.requestLayout()
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,4 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEUE DATA ---
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve window insets in a version-aware manner. This can be used to apply padding to
|
* Resolve window insets in a version-aware manner. This can be used to apply padding to
|
||||||
* a view that properly
|
* a view that properly follows all the frustrating changes that were made between 8-11.
|
||||||
*/
|
*/
|
||||||
val WindowInsets.systemBarsCompat: Rect get() {
|
val WindowInsets.systemBarsCompat: Rect get() {
|
||||||
return when {
|
return when {
|
||||||
|
|
Loading…
Reference in a new issue