recycler: redesign fast scroller

- Use new "bump" design
- Base off fundamental RV primitives over custom item
calculations
- Make possible to use by non-home views
This commit is contained in:
Alexander Capehart 2024-11-07 20:52:48 -07:00
parent 8ec61c9388
commit fe6c07a342
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 115 additions and 443 deletions

View file

@ -1,185 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* FastScrollPopupView.kt is part of Auxio.
*
* 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.R as MR
import com.google.android.material.textview.MaterialTextView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.isRtl
/**
* A [MaterialTextView] that displays the popup indicator used in FastScrollRecyclerView
*
* @author Alexander Capehart (OxygenCobalt), Hai Zhang
*/
class FastScrollPopupView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :
MaterialTextView(context, attrs, defStyleRes) {
init {
minimumWidth = context.getDimenPixels(R.dimen.size_touchable_mid_huge)
minimumHeight = context.getDimenPixels(R.dimen.size_touchable_large)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
setTextColor(context.getAttrColorCompat(MR.attr.colorOnSecondary))
ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER
includeFontPadding = false
alpha = 0f
elevation = context.getDimenPixels(MR.dimen.m3_sys_elevation_level2).toFloat()
background = FastScrollPopupDrawable(context)
}
private class FastScrollPopupDrawable(context: Context) : Drawable() {
private val paint: Paint =
Paint().apply {
isAntiAlias = true
color =
context
.getAttrColorCompat(com.google.android.material.R.attr.colorSecondary)
.defaultColor
style = Paint.Style.FILL
}
private val path = Path()
private val matrix = Matrix()
private val paddingStart = context.getDimenPixels(R.dimen.spacing_medium)
private val paddingEnd = context.getDimenPixels(R.dimen.spacing_mid_huge)
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 setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
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
) {
arcTo(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
startAngle,
sweepAngle,
false)
}
}
private companion object {
// Pre-calculate sqrt(2)
const val SQRT2 = 1.4142135f
}
}

View file

@ -29,12 +29,12 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music

View file

@ -27,12 +27,12 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music

View file

@ -27,11 +27,11 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.GenreViewHolder
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Genre

View file

@ -26,11 +26,11 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.PlaylistViewHolder
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Music

View file

@ -28,11 +28,11 @@ import java.util.Formatter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.sort.Sort
import org.oxycblt.auxio.music.Music

View file

@ -16,31 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home.fastscroll
package org.oxycblt.auxio.list.recycler
import android.animation.Animator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
import org.oxycblt.auxio.ui.MaterialFader
import org.oxycblt.auxio.ui.MaterialSlider
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.isRtl
import org.oxycblt.auxio.util.isUnder
import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -67,6 +61,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* - Variable names are no longer prefixed with m
* - Added drag listener
* - Added documentation
* - Completely new design
*
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
*
@ -78,14 +73,12 @@ class FastScrollRecyclerView
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AuxioRecyclerView(context, attrs, defStyleAttr) {
// Thumb
private val thumbView =
View(context).apply {
scaleX = 0f
background = context.getDrawableCompat(R.drawable.ui_scroll_thumb)
}
private val thumbSize = context.getDimenPixels(R.dimen.size_touchable_small)
private val slider = MaterialSlider(context, thumbSize)
private var thumbAnimator: Animator? = null
private val thumbWidth = thumbView.background.intrinsicWidth
private val thumbHeight = thumbView.background.intrinsicHeight
private val thumbView =
context.inflater.inflate(R.layout.view_scroll_thumb, null).apply { slider.jumpOut(this) }
private val thumbPadding = Rect(0, 0, 0, 0)
private var thumbOffset = 0
@ -96,27 +89,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
}
// Popup
private val popupView =
FastScrollPopupView(context).apply {
scaleX = 0f
scaleY = 0f
alpha = 0f
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
.apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
marginEnd = context.getDimenPixels(R.dimen.spacing_small)
}
}
private val fader = MaterialFader.quickLopsided(context)
private var thumbAnimator: Animator? = null
private var popupAnimator: Animator? = null
private var showingPopup = false
// Touch
private val minTouchTargetSize = context.getDimenPixels(R.dimen.size_touchable_small)
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
@ -144,23 +116,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (field) {
removeCallbacks(hideThumbRunnable)
showScrollbar()
showPopup()
} else {
postAutoHideScrollbar()
hidePopup()
}
listener?.onFastScrollingChanged(field)
}
private val tRect = Rect()
var popupProvider: PopupProvider? = null
var listener: Listener? = null
init {
overlay.add(thumbView)
overlay.add(popupView)
addItemDecoration(
object : ItemDecoration() {
@ -192,85 +159,17 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
updateScrollbarState()
thumbView.layoutDirection = layoutDirection
popupView.layoutDirection = layoutDirection
thumbView.measure(
MeasureSpec.makeMeasureSpec(thumbSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(thumbSize, MeasureSpec.EXACTLY))
val thumbTop = thumbPadding.top + thumbOffset
val thumbLeft =
if (isRtl) {
thumbPadding.left
} else {
width - thumbPadding.right - thumbWidth
width - thumbPadding.right - thumbSize
}
val thumbTop = thumbPadding.top + thumbOffset
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
val child = getChildAt(0)
val firstAdapterPos =
if (child != null) {
layoutManager?.getPosition(child) ?: NO_POSITION
} else {
NO_POSITION
}
val popupText: String
val provider = popupProvider
if (firstAdapterPos != NO_POSITION && provider != null) {
popupView.isInvisible = false
// Get the popup text. If there is none, we default to "?".
popupText = provider.getPopup(firstAdapterPos) ?: "?"
} else {
// No valid position or provider, do not show the popup.
popupView.isInvisible = true
popupText = ""
}
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
if (popupView.text != popupText) {
popupView.text = popupText
val widthMeasureSpec =
ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
thumbPadding.left +
thumbPadding.right +
thumbWidth +
popupLayoutParams.leftMargin +
popupLayoutParams.rightMargin,
popupLayoutParams.width)
val heightMeasureSpec =
ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
thumbPadding.top +
thumbPadding.bottom +
popupLayoutParams.topMargin +
popupLayoutParams.bottomMargin,
popupLayoutParams.height)
popupView.measure(widthMeasureSpec, heightMeasureSpec)
}
val popupWidth = popupView.measuredWidth
val popupHeight = popupView.measuredHeight
val popupLeft =
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin
} else {
width - thumbPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth
}
val popupAnchorY = popupHeight / 2
val thumbAnchorY = thumbView.paddingTop
val popupTop =
(thumbTop + thumbAnchorY - popupAnchorY)
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
.coerceAtMost(
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbSize, thumbTop + thumbSize)
}
override fun onScrolled(dx: Int, dy: Int) {
@ -295,26 +194,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private fun updateScrollbarState() {
if (scrollRange <= height || childCount == 0) {
return
}
// Combine the previous item dimensions with the current item top to find our scroll
// position
getDecoratedBoundsWithMargins(getChildAt(0), tRect)
val child = getChildAt(0)
val firstAdapterPos =
when (val mgr = layoutManager) {
is GridLayoutManager -> mgr.getPosition(child) / mgr.spanCount
is LinearLayoutManager -> mgr.getPosition(child)
else -> 0
}
val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - tRect.top
// Then calculate the thumb position, which is just:
// [proportion of scroll position to scroll range] * [total thumb range]
thumbOffset = (thumbOffsetRange.toLong() * scrollOffset / scrollOffsetRange).toInt()
val offsetY = computeVerticalScrollOffset()
if (computeVerticalScrollRange() < height || childCount == 0) {
return
}
val extentY = computeVerticalScrollExtent()
val fraction = (offsetY).toFloat() / (computeVerticalScrollRange() - extentY)
thumbOffset = (thumbOffsetRange * fraction).toInt()
}
private fun onItemTouch(event: MotionEvent): Boolean {
@ -331,10 +219,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (thumbView.isUnder(eventX, eventY, minTouchTargetSize)) {
dragStartThumbOffset = thumbOffset
} else {
dragStartThumbOffset =
(eventY - thumbPadding.top - thumbHeight / 2f).toInt()
} else if (eventX > thumbView.right - thumbSize / 4) {
dragStartThumbOffset = (eventY - thumbPadding.top - thumbSize / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
} else {
return false
}
dragging = true
@ -349,8 +238,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
dragStartThumbOffset = thumbOffset
} else {
dragStartY = eventY
dragStartThumbOffset =
(eventY - thumbPadding.top - thumbHeight / 2f).toInt()
dragStartThumbOffset = (eventY - thumbPadding.top - thumbSize / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
}
@ -371,44 +259,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private fun scrollToThumbOffset(thumbOffset: Int) {
val clampedThumbOffset = thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange)
val scrollOffset =
(scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() -
paddingTop
scrollTo(scrollOffset)
}
private fun scrollTo(offset: Int) {
if (childCount == 0) {
val rangeY = computeVerticalScrollRange() - computeVerticalScrollExtent()
val previousThumbOffset = this.thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange)
val previousOffsetY = rangeY * (previousThumbOffset / thumbOffsetRange.toFloat())
val newThumbOffset = thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange)
val newOffsetY = rangeY * (newThumbOffset / thumbOffsetRange.toFloat())
if (newOffsetY == 0f) {
// Hacky workaround to drift in vertical scroll offset where we just snap
// to the top if the thumb offset hit zero.
scrollToPosition(0)
return
}
stopScroll()
val trueOffset = offset - paddingTop
val itemHeight = itemHeight
val firstItemPosition = 0.coerceAtLeast(trueOffset / itemHeight)
val firstItemTop = firstItemPosition * itemHeight - trueOffset
scrollToPositionWithOffset(firstItemPosition, firstItemTop)
}
private fun scrollToPositionWithOffset(position: Int, offset: Int) {
var targetPosition = position
val trueOffset = offset - paddingTop
when (val mgr = layoutManager) {
is GridLayoutManager -> {
targetPosition *= mgr.spanCount
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
}
is LinearLayoutManager -> {
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
}
}
val dy = newOffsetY - previousOffsetY
scrollBy(0, max(dy.roundToInt(), -computeVerticalScrollOffset()))
}
// --- SCROLLBAR APPEARANCE ---
@ -425,7 +288,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
showingThumb = true
thumbAnimator?.cancel()
thumbAnimator = fader.fadeIn(thumbView).also { it.start() }
thumbAnimator = slider.slideIn(thumbView).also { it.start() }
}
private fun hideScrollbar() {
@ -435,77 +298,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
showingThumb = false
thumbAnimator?.cancel()
thumbAnimator = fader.fadeOut(thumbView).also { it.start() }
}
private fun showPopup() {
if (showingPopup) {
return
}
popupView.scaleX = 0f
popupView.scaleY = 0f
popupView.alpha = 1f
showingPopup = true
popupAnimator?.cancel()
popupAnimator = fader.fadeIn(popupView).also { it.start() }
}
private fun hidePopup() {
if (!showingPopup) {
return
}
showingPopup = false
popupAnimator?.cancel()
popupAnimator = fader.fadeOut(popupView).also { it.start() }
thumbAnimator = slider.slideOut(thumbView).also { it.start() }
}
// --- LAYOUT STATE ---
private val thumbOffsetRange: Int
get() {
return height - thumbPadding.top - thumbPadding.bottom - thumbHeight
}
private val scrollRange: Int
get() {
val itemCount = itemCount
if (itemCount == 0) {
return 0
}
val itemHeight = itemHeight
return if (itemHeight != 0) {
paddingTop + itemCount * itemHeight + paddingBottom
} else {
0
}
}
private val scrollOffsetRange: Int
get() = scrollRange - height
private val itemHeight: Int
get() {
if (childCount == 0) {
return 0
}
val itemView = getChildAt(0)
getDecoratedBoundsWithMargins(itemView, tRect)
return tRect.height()
}
private val itemCount: Int
get() =
when (val mgr = layoutManager) {
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
is LinearLayoutManager -> mgr.itemCount
else -> 0
return height - thumbPadding.top - thumbPadding.bottom - thumbSize
}
/** An interface to provide text to use in the popup when fast-scrolling. */
@ -531,6 +331,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
}
private companion object {
const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500
const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 500
}
}

View file

@ -200,3 +200,25 @@ class MaterialFlipper(context: Context) {
return AnimatorSet().apply { playTogether(outAnimator, inAnimator) }
}
}
class MaterialSlider(context: Context, private val x: Int) {
private val outConfig =
AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3)
private val inConfig =
AnimConfig.of(context, AnimConfig.EMPHASIZED_DECELERATE, AnimConfig.MEDIUM1)
fun jumpOut(view: View) {
view.translationX = x.toFloat()
}
fun slideOut(view: View): Animator {
val animator =
outConfig.genericFloat(view.translationX, x.toFloat()) { view.translationX = it }
return animator
}
fun slideIn(view: View): Animator {
val animator = inConfig.genericFloat(view.translationX, 0f) { view.translationX = it }
return animator
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,840L300,660L358,602L480,724L602,602L660,660L480,840ZM358,362L300,304L480,124L660,304L602,362L480,240L358,362Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:tint="?attr/colorSecondary">
<corners android:radius="16dp" />
<size
android:width="56dp"
android:height="56dp" />
<solid android:color="@android:color/white" />
</shape>

View file

@ -3,14 +3,9 @@
android:shape="rectangle"
android:tint="?attr/colorSecondary">
<corners android:radius="8dp" />
<padding
android:bottom="4dp"
android:left="2dp"
android:right="2dp"
android:top="4dp" />
<corners android:topLeftRadius="24dp" android:bottomLeftRadius="24dp" />
<size
android:width="8dp"
android:height="52dp" />
android:width="48dp"
android:height="48dp" />
<solid android:color="@android:color/white" />
</shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.oxycblt.auxio.list.recycler.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/home_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/ui_scroll_thumb">
<ImageView
android:layout_width="@dimen/size_touchable_small"
android:layout_height="@dimen/size_touchable_small"
app:tint="?attr/colorOnSecondary"
android:scaleType="centerInside"
android:src="@drawable/ic_scroll_24" />
</FrameLayout>

View file

@ -25,6 +25,9 @@
<dimen name="size_icon_large">40dp</dimen>
<dimen name="size_icon_huge">48dp</dimen>
<dimen name="width_scroll_thumb">48dp</dimen>
<dimen name="height_scroll_thumb">48dp</dimen>
<!-- Misc -->
<dimen name="m3_shape_corners_large">16dp</dimen>
<dimen name="m3_shape_corners_full">128dp</dimen>