Refactor and tune FastScroller

This commit is contained in:
Koitharu
2022-07-09 13:21:17 +03:00
parent 4743f40154
commit e2ed7f0d77
27 changed files with 464 additions and 331 deletions

View File

@@ -0,0 +1,82 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.measureWidth
import kotlin.math.hypot
class BubbleAnimator(
private val bubble: View,
) {
private val animationDuration = (bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
bubble.context.animatorDurationScale).toLong()
private var animator: Animator? = null
private var isHiding = false
fun show() {
if (bubble.isVisible && !isHiding) {
return
}
isHiding = false
animator?.cancel()
animator = ViewAnimationUtils.createCircularReveal(
bubble,
bubble.measureWidth(),
bubble.measuredHeight,
0f,
hypot(bubble.width.toDouble(), bubble.height.toDouble()).toFloat(),
).apply {
bubble.isVisible = true
duration = animationDuration
interpolator = DecelerateInterpolator()
start()
}
}
fun hide() {
if (!bubble.isVisible || isHiding) {
return
}
animator?.cancel()
isHiding = true
animator = ViewAnimationUtils.createCircularReveal(
bubble,
bubble.width,
bubble.height,
hypot(bubble.width.toDouble(), bubble.height.toDouble()).toFloat(),
0f,
).apply {
duration = animationDuration
interpolator = AccelerateInterpolator()
addListener(HideListener())
start()
}
}
private inner class HideListener : AnimatorListenerAdapter() {
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@BubbleAnimator.animator) {
bubble.isInvisible = true
isHiding = false
this@BubbleAnimator.animator = null
}
}
}
}

View File

@@ -0,0 +1,79 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.parents
class FastScrollRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = androidx.recyclerview.R.attr.recyclerViewStyle,
) : RecyclerView(context, attrs, defStyleAttr) {
val fastScroller = FastScroller(context, attrs)
init {
fastScroller.id = R.id.fast_scroller
fastScroller.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(adapter)
fastScroller.setSectionIndexer(adapter as? FastScroller.SectionIndexer)
}
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
fastScroller.visibility = visibility
}
fun setFastScrollListener(fastScrollListener: FastScroller.FastScrollListener?) =
fastScroller.setFastScrollListener(fastScrollListener)
fun setFastScrollEnabled(enabled: Boolean) {
fastScroller.isEnabled = enabled
}
fun setHideScrollbar(hideScrollbar: Boolean) = fastScroller.setHideScrollbar(hideScrollbar)
fun setTrackVisible(visible: Boolean) = fastScroller.setTrackVisible(visible)
fun setTrackColor(@ColorInt color: Int) = fastScroller.setTrackColor(color)
fun setHandleColor(@ColorInt color: Int) = fastScroller.setHandleColor(color)
@JvmOverloads
fun setBubbleVisible(visible: Boolean, always: Boolean = false) = fastScroller.setBubbleVisible(visible, always)
fun setBubbleColor(@ColorInt color: Int) = fastScroller.setBubbleColor(color)
fun setBubbleTextColor(@ColorInt color: Int) = fastScroller.setBubbleTextColor(color)
fun setBubbleTextSize(size: Int) = fastScroller.setBubbleTextSize(size)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastScroller.attachRecyclerView(this)
for (p in parents) {
if (p is SwipeRefreshLayout) {
fastScroller.setSwipeRefreshLayout(p)
return
}
}
}
override fun onDetachedFromWindow() {
fastScroller.detachRecyclerView()
fastScroller.setSwipeRefreshLayout(null)
super.onDetachedFromWindow()
}
}

View File

@@ -1,23 +1,5 @@
/* package org.koitharu.kotatsu.base.ui.list.fastscroll
* Copyright 2022 Randy Webster. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.koitharu.kotatsu.base.ui.widgets
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
@@ -25,54 +7,43 @@ import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.MotionEvent import android.view.*
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.* import android.widget.*
import android.widget.RelativeLayout.* import androidx.annotation.*
import androidx.annotation.ColorInt
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.annotation.StyleableRes
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.FastScrollerBinding
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
import com.google.android.material.R as materialR
private const val BUBBLE_ANIM_DURATION = 100L
private const val SCROLLBAR_HIDE_DELAY = 1000L private const val SCROLLBAR_HIDE_DELAY = 1000L
private const val TRACK_SNAP_RANGE = 5 private const val TRACK_SNAP_RANGE = 5
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
class FastScroller : LinearLayout { class FastScroller @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = R.attr.fastScrollerStyle,
) : LinearLayout(context, attrs, defStyleAttr) {
enum class Size(@DrawableRes val drawableId: Int, @DimenRes val textSizeId: Int) { enum class BubbleSize(@DrawableRes val drawableId: Int, @DimenRes val textSizeId: Int) {
NORMAL(R.drawable.fastscroll_bubble, R.dimen.fastscroll_bubble_text_size), NORMAL(R.drawable.fastscroll_bubble, R.dimen.fastscroll_bubble_text_size),
SMALL(R.drawable.fastscroll_bubble_small, R.dimen.fastscroll_bubble_text_size_small) SMALL(R.drawable.fastscroll_bubble_small, R.dimen.fastscroll_bubble_text_size_small)
} }
private val Size.textSize get() = resources.getDimension(textSizeId) private val binding = FastScrollerBinding.inflate(LayoutInflater.from(context), this)
private val animationDuration = (context.resources.getInteger(R.integer.config_defaultAnimTime) * private val scrollbarPaddingEnd = context.resources.getDimension(R.dimen.fastscroll_scrollbar_padding_end)
context.animatorDurationScale).toLong()
private val bubbleView: TextView by lazy { findViewById(R.id.fastscroll_bubble) }
private val handleView: ImageView by lazy { findViewById(R.id.fastscroll_handle) }
private val trackView: ImageView by lazy { findViewById(R.id.fastscroll_track) }
private val scrollbar: View by lazy { findViewById(R.id.fastscroll_scrollbar) }
private val scrollbarPaddingEnd by lazy {
resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat()
}
@ColorInt @ColorInt
private var bubbleColor = 0 private var bubbleColor = 0
@@ -86,14 +57,14 @@ class FastScroller : LinearLayout {
private var hideScrollbar = true private var hideScrollbar = true
private var showBubble = true private var showBubble = true
private var showBubbleAlways = false private var showBubbleAlways = false
private var bubbleSize = Size.NORMAL private var bubbleSize = BubbleSize.NORMAL
private var bubbleImage: Drawable? = null private var bubbleImage: Drawable? = null
private var handleImage: Drawable? = null private var handleImage: Drawable? = null
private var trackImage: Drawable? = null private var trackImage: Drawable? = null
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
private var swipeRefreshLayout: SwipeRefreshLayout? = null private var swipeRefreshLayout: SwipeRefreshLayout? = null
private var scrollbarAnimator: ViewPropertyAnimator? = null private val scrollbarAnimator = ScrollbarAnimator(binding.scrollbar, scrollbarPaddingEnd)
private var bubbleAnimator: ViewPropertyAnimator? = null private val bubbleAnimator = BubbleAnimator(binding.bubble)
private var fastScrollListener: FastScrollListener? = null private var fastScrollListener: FastScrollListener? = null
private var sectionIndexer: SectionIndexer? = null private var sectionIndexer: SectionIndexer? = null
@@ -103,19 +74,15 @@ class FastScroller : LinearLayout {
hideScrollbar() hideScrollbar()
} }
private val alphaAnimatorListener = object : AnimatorListenerAdapter() {
/* adapter required for new alpha value to stick */
}
private val scrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() { private val scrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!handleView.isSelected && isEnabled) { if (!binding.thumb.isSelected && isEnabled) {
val y = recyclerView.scrollProportion val y = recyclerView.scrollProportion
setViewPositions(y) setViewPositions(y)
if (showBubbleAlways) { if (showBubbleAlways) {
val targetPos = getRecyclerViewTargetPosition(y) val targetPos = getRecyclerViewTargetPosition(y)
sectionIndexer?.let { bubbleView.text = it.getSectionText(targetPos) } sectionIndexer?.let { binding.bubble.text = it.getSectionText(recyclerView.context, targetPos) }
} }
} }
@@ -133,12 +100,10 @@ class FastScroller : LinearLayout {
when (newState) { when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> { RecyclerView.SCROLL_STATE_DRAGGING -> {
handler.removeCallbacks(scrollbarHider) handler.removeCallbacks(scrollbarHider)
scrollbarAnimator?.cancel() showScrollbar()
if (!scrollbar.isVisible) showScrollbar()
if (showBubbleAlways && sectionIndexer != null) showBubble() if (showBubbleAlways && sectionIndexer != null) showBubble()
} }
RecyclerView.SCROLL_STATE_IDLE -> if (hideScrollbar && !handleView.isSelected) { RecyclerView.SCROLL_STATE_IDLE -> if (hideScrollbar && !binding.thumb.isSelected) {
handler.postDelayed(scrollbarHider, SCROLLBAR_HIDE_DELAY) handler.postDelayed(scrollbarHider, SCROLLBAR_HIDE_DELAY)
} }
} }
@@ -153,22 +118,42 @@ class FastScroller : LinearLayout {
return viewHeight * proportion return viewHeight * proportion
} }
@JvmOverloads init {
constructor(context: Context, size: Size = Size.NORMAL) : super(context) { clipChildren = false
context.layout(size = size) orientation = HORIZONTAL
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
@ColorInt var bubbleColor = context.getThemeColor(materialR.attr.colorControlNormal, Color.DKGRAY)
@ColorInt var handleColor = bubbleColor
@ColorInt var trackColor = context.getThemeColor(materialR.attr.colorOutline, Color.LTGRAY)
@ColorInt var textColor = context.getThemeColor(android.R.attr.textColorPrimaryInverse, Color.WHITE)
var showTrack = false
context.withStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr) {
bubbleColor = getColor(R.styleable.FastScroller_bubbleColor, bubbleColor)
handleColor = getColor(R.styleable.FastScroller_thumbColor, handleColor)
trackColor = getColor(R.styleable.FastScroller_trackColor, trackColor)
textColor = getColor(R.styleable.FastScroller_bubbleTextColor, textColor)
hideScrollbar = getBoolean(R.styleable.FastScroller_hideScrollbar, hideScrollbar)
showBubble = getBoolean(R.styleable.FastScroller_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScroller_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScroller_showTrack, showTrack)
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
}
setTrackColor(trackColor)
setHandleColor(handleColor)
setBubbleColor(bubbleColor)
setBubbleTextColor(textColor)
setHideScrollbar(hideScrollbar)
setBubbleVisible(showBubble, showBubbleAlways)
setTrackVisible(showTrack)
} }
@JvmOverloads override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) { super.onSizeChanged(w, h, oldW, oldH)
context.layout(attrs)
layoutParams = attrs?.let { generateLayoutParams(it) } ?: LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT
)
}
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) = super.onSizeChanged(w, h, oldW, oldH).also {
viewHeight = h viewHeight = h
} }
@@ -180,18 +165,15 @@ class FastScroller : LinearLayout {
setRecyclerViewPosition(y) setRecyclerViewPosition(y)
} }
when (event.action) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
if (event.x < handleView.x - scrollbar.compatPaddingStart) return false if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false
requestDisallowInterceptTouchEvent(true) requestDisallowInterceptTouchEvent(true)
setHandleSelected(true) setHandleSelected(true)
handler.removeCallbacks(scrollbarHider) handler.removeCallbacks(scrollbarHider)
scrollbarAnimator?.cancel() showScrollbar()
bubbleAnimator?.cancel()
if (!scrollbar.isVisible) showScrollbar()
if (showBubble && sectionIndexer != null) showBubble() if (showBubble && sectionIndexer != null) showBubble()
fastScrollListener?.onFastScrollStart(this) fastScrollListener?.onFastScrollStart(this)
@@ -224,7 +206,8 @@ class FastScroller : LinearLayout {
* *
* @param enabled True if this view is enabled, false otherwise * @param enabled True if this view is enabled, false otherwise
*/ */
override fun setEnabled(enabled: Boolean) = super.setEnabled(enabled).also { override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
isVisible = enabled isVisible = enabled
} }
@@ -283,9 +266,9 @@ class FastScroller : LinearLayout {
} }
is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply { is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply {
height = 0 height = 0
addRule(ALIGN_TOP, recyclerViewId) addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
addRule(ALIGN_BOTTOM, recyclerViewId) addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
addRule(ALIGN_END, recyclerViewId) addRule(RelativeLayout.ALIGN_END, recyclerViewId)
setMargins(0, marginTop, 0, marginBottom) setMargins(0, marginTop, 0, marginBottom)
} }
else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout")
@@ -302,6 +285,9 @@ class FastScroller : LinearLayout {
* @see detachRecyclerView * @see detachRecyclerView
*/ */
fun attachRecyclerView(recyclerView: RecyclerView) { fun attachRecyclerView(recyclerView: RecyclerView) {
if (this.recyclerView != null) {
detachRecyclerView()
}
this.recyclerView = recyclerView this.recyclerView = recyclerView
if (parent is ViewGroup) { if (parent is ViewGroup) {
@@ -314,7 +300,7 @@ class FastScroller : LinearLayout {
recyclerView.addOnScrollListener(scrollListener) recyclerView.addOnScrollListener(scrollListener)
// set initial positions for bubble and handle // set initial positions for bubble and thumb
post { setViewPositions(this.recyclerView?.scrollProportion ?: 0f) } post { setViewPositions(this.recyclerView?.scrollProportion ?: 0f) }
} }
@@ -365,7 +351,8 @@ class FastScroller : LinearLayout {
*/ */
fun setHideScrollbar(hideScrollbar: Boolean) { fun setHideScrollbar(hideScrollbar: Boolean) {
if (this.hideScrollbar != hideScrollbar) { if (this.hideScrollbar != hideScrollbar) {
scrollbar.isVisible = !hideScrollbar.also { this.hideScrollbar = it } this.hideScrollbar = hideScrollbar
binding.scrollbar.isGone = hideScrollbar
} }
} }
@@ -375,7 +362,7 @@ class FastScroller : LinearLayout {
* @param visible True to show scroll track, false to hide * @param visible True to show scroll track, false to hide
*/ */
fun setTrackVisible(visible: Boolean) { fun setTrackVisible(visible: Boolean) {
trackView.isVisible = visible binding.track.isVisible = visible
} }
/** /**
@@ -390,14 +377,14 @@ class FastScroller : LinearLayout {
trackImage?.let { trackImage?.let {
it.setTint(color) it.setTint(color)
trackView.setImageDrawable(it) binding.track.setImageDrawable(it)
} }
} }
/** /**
* Set the color of the scroll handle. * Set the color of the scroll thumb.
* *
* @param color The color for the scroll handle * @param color The color for the scroll thumb
*/ */
fun setHandleColor(@ColorInt color: Int) { fun setHandleColor(@ColorInt color: Int) {
handleColor = color handleColor = color
@@ -408,7 +395,7 @@ class FastScroller : LinearLayout {
handleImage?.let { handleImage?.let {
it.setTint(handleColor) it.setTint(handleColor)
handleView.setImageDrawable(it) binding.thumb.setImageDrawable(it)
} }
} }
@@ -416,7 +403,7 @@ class FastScroller : LinearLayout {
* Show the section bubble while scrolling. * Show the section bubble while scrolling.
* *
* @param visible True to show the bubble, false to hide * @param visible True to show the bubble, false to hide
* @param always True to always show the bubble, false to only show on handle touch * @param always True to always show the bubble, false to only show on thumb touch
*/ */
@JvmOverloads @JvmOverloads
fun setBubbleVisible(visible: Boolean, always: Boolean = false) { fun setBubbleVisible(visible: Boolean, always: Boolean = false) {
@@ -438,7 +425,7 @@ class FastScroller : LinearLayout {
bubbleImage?.let { bubbleImage?.let {
it.setTint(bubbleColor) it.setTint(bubbleColor)
bubbleView.background = it binding.bubble.background = it
} }
} }
@@ -447,7 +434,7 @@ class FastScroller : LinearLayout {
* *
* @param color The text color for the section bubble * @param color The text color for the section bubble
*/ */
fun setBubbleTextColor(@ColorInt color: Int) = bubbleView.setTextColor(color) fun setBubbleTextColor(@ColorInt color: Int) = binding.bubble.setTextColor(color)
/** /**
* Set the scaled pixel text size of the section bubble. * Set the scaled pixel text size of the section bubble.
@@ -455,15 +442,15 @@ class FastScroller : LinearLayout {
* @param size The scaled pixel text size for the section bubble * @param size The scaled pixel text size for the section bubble
*/ */
fun setBubbleTextSize(size: Int) { fun setBubbleTextSize(size: Int) {
bubbleView.textSize = size.toFloat() binding.bubble.textSize = size.toFloat()
} }
private fun getRecyclerViewTargetPosition(y: Float) = recyclerView?.let { recyclerView -> private fun getRecyclerViewTargetPosition(y: Float) = recyclerView?.let { recyclerView ->
val itemCount = recyclerView.adapter?.itemCount ?: 0 val itemCount = recyclerView.adapter?.itemCount ?: 0
val proportion = when { val proportion = when {
handleView.y == 0f -> 0f binding.thumb.y == 0f -> 0f
handleView.y + handleHeight >= viewHeight - TRACK_SNAP_RANGE -> 1f binding.thumb.y + handleHeight >= viewHeight - TRACK_SNAP_RANGE -> 1f
else -> y / viewHeight.toFloat() else -> y / viewHeight.toFloat()
} }
@@ -477,139 +464,65 @@ class FastScroller : LinearLayout {
} ?: 0 } ?: 0
private fun setRecyclerViewPosition(y: Float) { private fun setRecyclerViewPosition(y: Float) {
recyclerView?.layoutManager?.let { layoutManager -> val layoutManager = recyclerView?.layoutManager ?: return
val targetPos = getRecyclerViewTargetPosition(y) val targetPos = getRecyclerViewTargetPosition(y)
layoutManager.scrollToPosition(targetPos) layoutManager.scrollToPosition(targetPos)
if (showBubble) sectionIndexer?.let { bubbleView.text = it.getSectionText(targetPos) } if (showBubble) sectionIndexer?.let { binding.bubble.text = it.getSectionText(context, targetPos) }
}
} }
private fun setViewPositions(y: Float) { private fun setViewPositions(y: Float) {
bubbleHeight = bubbleView.measuredHeight bubbleHeight = binding.bubble.measuredHeight
handleHeight = handleView.measuredHeight handleHeight = binding.thumb.measuredHeight
val bubbleHandleHeight = bubbleHeight + handleHeight / 2f val bubbleHandleHeight = bubbleHeight + handleHeight / 2f
if (showBubble && viewHeight >= bubbleHandleHeight) { if (showBubble && viewHeight >= bubbleHandleHeight) {
bubbleView.y = (y - bubbleHeight).coerceIn(0f, viewHeight - bubbleHandleHeight) binding.bubble.y = (y - bubbleHeight).coerceIn(0f, viewHeight - bubbleHandleHeight)
} }
if (viewHeight >= handleHeight) { if (viewHeight >= handleHeight) {
handleView.y = (y - handleHeight / 2).coerceIn(0f, viewHeight - handleHeight.toFloat()) binding.thumb.y = (y - handleHeight / 2).coerceIn(0f, viewHeight - handleHeight.toFloat())
} }
} }
private fun updateViewHeights() { private fun updateViewHeights() {
val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
bubbleView.measure(measureSpec, measureSpec) binding.bubble.measure(measureSpec, measureSpec)
bubbleHeight = bubbleView.measuredHeight bubbleHeight = binding.bubble.measuredHeight
handleView.measure(measureSpec, measureSpec) binding.thumb.measure(measureSpec, measureSpec)
handleHeight = handleView.measuredHeight handleHeight = binding.thumb.measuredHeight
} }
private fun showBubble() { private fun showBubble() {
if (!bubbleView.isVisible) { bubbleAnimator.show()
bubbleView.isVisible = true
bubbleAnimator = bubbleView.animate().alpha(1f)
.setDuration(BUBBLE_ANIM_DURATION)
.setListener(alphaAnimatorListener)
}
} }
private fun hideBubble() { private fun hideBubble() {
if (bubbleView.isVisible) { bubbleAnimator.hide()
bubbleAnimator = bubbleView.animate().alpha(0f)
.setDuration(BUBBLE_ANIM_DURATION)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
bubbleView.isVisible = false
bubbleAnimator = null
}
override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
bubbleView.isVisible = false
bubbleAnimator = null
}
})
}
} }
private fun showScrollbar() { private fun showScrollbar() {
if ((recyclerView?.computeVerticalScrollRange() ?: (0 - viewHeight)) > 0) { if ((recyclerView?.computeVerticalScrollRange() ?: (0 - viewHeight)) > 0) {
scrollbar.translationX = scrollbarPaddingEnd scrollbarAnimator.show()
scrollbar.isVisible = true
scrollbarAnimator = scrollbar.animate().translationX(0f).alpha(1f)
.setDuration(animationDuration)
.setListener(alphaAnimatorListener)
} }
} }
private fun hideScrollbar() { private fun hideScrollbar() {
scrollbarAnimator = scrollbar.animate().translationX(scrollbarPaddingEnd).alpha(0f) scrollbarAnimator.hide()
.setDuration(animationDuration)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
scrollbar.isVisible = false
scrollbarAnimator = null
}
override fun onAnimationCancel(animation: Animator) {
super.onAnimationCancel(animation)
scrollbar.isVisible = false
scrollbarAnimator = null
}
})
} }
private fun setHandleSelected(selected: Boolean) { private fun setHandleSelected(selected: Boolean) {
handleView.isSelected = selected binding.thumb.isSelected = selected
handleImage?.setTint(if (selected) bubbleColor else handleColor) handleImage?.setTint(if (selected) bubbleColor else handleColor)
} }
private fun TypedArray.getSize(@StyleableRes index: Int, defValue: Int) = getInt(index, defValue).let { ordinal -> private fun TypedArray.getBubbleSize(@StyleableRes index: Int, defaultValue: BubbleSize): BubbleSize {
Size.values().find { it.ordinal == ordinal } ?: Size.NORMAL val ordinal = getInt(index, -1)
return BubbleSize.values().getOrNull(ordinal) ?: defaultValue
} }
private fun Context.layout(attrs: AttributeSet? = null, size: Size = Size.NORMAL) { private val BubbleSize.textSize
inflate(this, R.layout.fast_scroller, this@FastScroller) @Px get() = resources.getDimension(textSizeId)
clipChildren = false
orientation = HORIZONTAL
@ColorInt var bubbleColor = Color.GRAY
@ColorInt var handleColor = Color.DKGRAY
@ColorInt var trackColor = Color.LTGRAY
@ColorInt var textColor = Color.WHITE
var showTrack = false
var textSize = size.textSize
withStyledAttributes(attrs, R.styleable.FastScroller) {
bubbleColor = getColor(R.styleable.FastScroller_bubbleColor, bubbleColor)
handleColor = getColor(R.styleable.FastScroller_handleColor, handleColor)
trackColor = getColor(R.styleable.FastScroller_trackColor, trackColor)
textColor = getColor(R.styleable.FastScroller_bubbleTextColor, textColor)
hideScrollbar = getBoolean(R.styleable.FastScroller_hideScrollbar, hideScrollbar)
showBubble = getBoolean(R.styleable.FastScroller_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScroller_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScroller_showTrack, showTrack)
bubbleSize = getSize(R.styleable.FastScroller_bubbleSize, size.ordinal)
textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
}
setTrackColor(trackColor)
setHandleColor(handleColor)
setBubbleColor(bubbleColor)
setBubbleTextColor(textColor)
setHideScrollbar(hideScrollbar)
setBubbleVisible(showBubble, showBubbleAlways)
setTrackVisible(showTrack)
bubbleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
}
interface FastScrollListener { interface FastScrollListener {
@@ -620,6 +533,6 @@ class FastScroller : LinearLayout {
interface SectionIndexer { interface SectionIndexer {
fun getSectionText(position: Int): CharSequence fun getSectionText(context: Context, position: Int): CharSequence
} }
} }

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewPropertyAnimator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
class ScrollbarAnimator(
private val scrollbar: View,
private val scrollbarPaddingEnd: Float,
) {
private val animationDuration = (scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
scrollbar.context.animatorDurationScale).toLong()
private var animator: ViewPropertyAnimator? = null
private var isHiding = false
fun show() {
if (scrollbar.isVisible && !isHiding) {
return
}
isHiding = false
animator?.cancel()
scrollbar.translationX = scrollbarPaddingEnd
scrollbar.isVisible = true
animator = scrollbar
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationDuration)
}
fun hide() {
if (!scrollbar.isVisible || isHiding) {
return
}
animator?.cancel()
isHiding = true
animator = scrollbar
.animate()
.translationX(scrollbarPaddingEnd)
.alpha(0f)
.setDuration(animationDuration)
.setListener(HideListener())
}
private inner class HideListener : AnimatorListenerAdapter() {
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@ScrollbarAnimator.animator) {
scrollbar.isInvisible = true
isHiding = false
this@ScrollbarAnimator.animator = null
}
}
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright 2022 Randy Webster. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
class FastScrollRecyclerView : RecyclerView {
private val fastScroller: FastScroller
constructor(context: Context) : super(context) {
fastScroller = context.layout()
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
@JvmOverloads
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) {
fastScroller = context.layout(attrs)
}
override fun setAdapter(adapter: Adapter<*>?) = super.setAdapter(adapter).also {
when (adapter) {
is FastScroller.SectionIndexer -> fastScroller.setSectionIndexer(adapter)
null -> fastScroller.setSectionIndexer(null)
}
}
override fun setVisibility(visibility: Int) = super.setVisibility(visibility).also {
fastScroller.visibility = visibility
}
fun setFastScrollListener(fastScrollListener: FastScroller.FastScrollListener?) =
fastScroller.setFastScrollListener(fastScrollListener)
fun setFastScrollEnabled(enabled: Boolean) {
fastScroller.isEnabled = enabled
}
fun setHideScrollbar(hideScrollbar: Boolean) = fastScroller.setHideScrollbar(hideScrollbar)
fun setTrackVisible(visible: Boolean) = fastScroller.setTrackVisible(visible)
fun setTrackColor(@ColorInt color: Int) = fastScroller.setTrackColor(color)
fun setHandleColor(@ColorInt color: Int) = fastScroller.setHandleColor(color)
@JvmOverloads
fun setBubbleVisible(visible: Boolean, always: Boolean = false) = fastScroller.setBubbleVisible(visible, always)
fun setBubbleColor(@ColorInt color: Int) = fastScroller.setBubbleColor(color)
fun setBubbleTextColor(@ColorInt color: Int) = fastScroller.setBubbleTextColor(color)
fun setBubbleTextSize(size: Int) = fastScroller.setBubbleTextSize(size)
override fun onAttachedToWindow() = super.onAttachedToWindow().also {
fastScroller.attachRecyclerView(this)
}
override fun onDetachedFromWindow() {
fastScroller.detachRecyclerView()
super.onDetachedFromWindow()
}
private fun Context.layout(attrs: AttributeSet? = null) =
FastScroller(this, attrs).apply { id = R.id.fast_scroller }
}

View File

@@ -10,6 +10,7 @@ import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@@ -29,6 +30,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.end
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ChaptersFragment : class ChaptersFragment :
@@ -192,6 +194,9 @@ class ChaptersFragment :
binding.recyclerViewChapters.updatePadding( binding.recyclerViewChapters.updatePadding(
bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0), bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0),
) )
binding.recyclerViewChapters.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
} }
private fun initSpinner(spinner: Spinner) { private fun initSpinner(spinner: Spinner) {

View File

@@ -1,15 +1,16 @@
package org.koitharu.kotatsu.details.ui.adapter package org.koitharu.kotatsu.details.ui.adapter
import android.content.Context
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.widgets.FastScroller import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
class ChaptersAdapter( class ChaptersAdapter(
onItemClickListener: OnListItemClickListener<ChapterListItem>, onItemClickListener: OnListItemClickListener<ChapterListItem>,
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()), FastScroller.SectionIndexer { ) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()), FastScroller.SectionIndexer {
init { init {
setHasStableIds(true) setHasStableIds(true)
@@ -41,7 +42,7 @@ class ChaptersAdapter(
} }
} }
override fun getSectionText(position: Int): CharSequence { override fun getSectionText(context: Context, position: Int): CharSequence {
return items[position].chapter.number.toString() return items[position].chapter.number.toString()
} }
} }

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
class HistoryListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer {
override fun getSectionText(context: Context, position: Int): CharSequence {
val list = items
for (i in (0..position).reversed()) {
val item = list[i]
if (item is DateTimeAgo) {
return item.format(context.resources)
}
}
return ""
}
}

View File

@@ -6,6 +6,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.ReversibleHandle
@@ -53,6 +54,8 @@ class HistoryListFragment : MangaListFragment() {
} }
} }
override fun onCreateAdapter() = HistoryListAdapter(get(), viewLifecycleOwner, this)
private fun onItemsRemoved(reversibleHandle: ReversibleHandle) { private fun onItemsRemoved(reversibleHandle: ReversibleHandle) {
Snackbar.make(binding.recyclerView, R.string.removed_from_history, Snackbar.LENGTH_LONG) Snackbar.make(binding.recyclerView, R.string.removed_from_history, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { reversibleHandle.reverseAsync() } .setAction(R.string.undo) { reversibleHandle.reverseAsync() }

View File

@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.list.ui
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.view.ViewGroup.MarginLayoutParams
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isNotEmpty import androidx.core.view.isNotEmpty
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -73,11 +75,7 @@ abstract class MangaListFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
listAdapter = MangaListAdapter( listAdapter = onCreateAdapter()
coil = get(),
lifecycleOwner = viewLifecycleOwner,
listener = this,
)
selectionController = ListSelectionController( selectionController = ListSelectionController(
activity = requireActivity(), activity = requireActivity(),
decoration = MangaSelectionDecoration(view.context), decoration = MangaSelectionDecoration(view.context),
@@ -167,6 +165,14 @@ abstract class MangaListFragment :
} }
} }
protected open fun onCreateAdapter(): MangaListAdapter {
return MangaListAdapter(
coil = get(),
lifecycleOwner = viewLifecycleOwner,
listener = this,
)
}
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left, left = insets.left,
@@ -175,6 +181,10 @@ abstract class MangaListFragment :
binding.recyclerView.updatePadding( binding.recyclerView.updatePadding(
bottom = insets.bottom, bottom = insets.bottom,
) )
binding.recyclerView.fastScroller.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom
marginEnd = insets.end(binding.recyclerView)
}
if (activity is MainActivity) { if (activity is MainActivity) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.swipeRefreshLayout.setProgressViewOffset( binding.swipeRefreshLayout.setProgressViewOffset(

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
class MangaListAdapter( open class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener, listener: MangaListListener,

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.utils.ext
import android.view.View
import androidx.core.graphics.Insets
fun Insets.end(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) left else right
}
fun Insets.start(view: View): Int {
return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) right else left
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewParent
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children import androidx.core.view.children
@@ -147,4 +148,13 @@ fun RecyclerView.invalidateNestedItemDecorations() {
} }
} }
internal val View.compatPaddingStart get() = ViewCompat.getPaddingStart(this) internal val View.compatPaddingStart get() = ViewCompat.getPaddingStart(this)
val View.parents: Sequence<ViewParent>
get() = sequence {
var p: ViewParent? = parent
while (p != null) {
yield(p)
p = p.parent
}
}

View File

@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape <shape
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle"> android:shape="rectangle">
<tools:solid android:color="#777777" />
<corners <corners
android:topLeftRadius="@dimen/fastscroll_bubble_radius" android:topLeftRadius="@dimen/fastscroll_bubble_radius"
android:topRightRadius="@dimen/fastscroll_bubble_radius" android:topRightRadius="@dimen/fastscroll_bubble_radius"

View File

@@ -12,6 +12,7 @@
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:elevation="0dp" app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View File

@@ -6,13 +6,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters" android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:handleColor="?attr/colorTertiary"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_chapter" /> tools:listitem="@layout/item_chapter" />

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ Copyright 2022 Randy Webster. All rights reserved. ~ Copyright 2022 Randy Webster. All rights reserved.
~ ~
~ Licensed under the Apache License, Version 2.0 (the "License"); ~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,45 +16,54 @@
<merge <merge
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
tools:clipChildren="false"
tools:layout_gravity="end"
tools:layout_height="match_parent"
tools:layout_width="wrap_content"
tools:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<TextView <TextView
android:id="@+id/fastscroll_bubble" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:singleLine="true"
android:visibility="gone" android:visibility="invisible"
tools:background="@drawable/fastscroll_bubble" tools:background="@drawable/fastscroll_bubble"
tools:backgroundTint="@color/blue_primary"
tools:text="A" tools:text="A"
tools:textColor="#ffffff" tools:textColor="#ffffff"
tools:visibility="visible" /> tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/fastscroll_scrollbar" android:id="@+id/scrollbar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingEnd="@dimen/fastscroll_scrollbar_padding_end"
android:paddingStart="@dimen/fastscroll_scrollbar_padding_start" android:paddingStart="@dimen/fastscroll_scrollbar_padding_start"
android:paddingEnd="@dimen/fastscroll_scrollbar_padding_end"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/fastscroll_track" android:id="@+id/track"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/fastscroll_track" /> tools:src="@drawable/fastscroll_track"
tools:tint="@color/kotatsu_outline" />
<ImageView <ImageView
android:id="@+id/fastscroll_handle" android:id="@+id/thumb"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/fastscroll_handle" /> tools:src="@drawable/fastscroll_handle"
tools:tint="@color/kotatsu_primary" />
</FrameLayout> </FrameLayout>

View File

@@ -17,7 +17,7 @@
tools:listitem="@layout/item_branch" tools:listitem="@layout/item_branch"
tools:visibility="visible" /> tools:visibility="visible" />
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters" android:id="@+id/recyclerView_chapters"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
@@ -28,11 +28,7 @@
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:bubbleColor="?attr/colorTertiary"
app:bubbleTextColor="?attr/colorOnTertiary"
app:handleColor="?attr/colorTertiary"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:showBubble="true"
tools:listitem="@layout/item_chapter" /> tools:listitem="@layout/item_chapter" />
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.progressindicator.CircularProgressIndicator

View File

@@ -11,7 +11,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -21,8 +21,8 @@
android:paddingTop="@dimen/grid_spacing_outer" android:paddingTop="@dimen/grid_spacing_outer"
android:paddingRight="@dimen/list_spacing" android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer" android:paddingBottom="@dimen/grid_spacing_outer"
app:trackColor="?attr/colorOutline"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:trackColor="?attr/colorOutline"
tools:listitem="@layout/item_feed" /> tools:listitem="@layout/item_feed" />
</FrameLayout> </FrameLayout>

View File

@@ -3,7 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
@@ -12,13 +11,12 @@
android:paddingRight="@dimen/list_spacing" android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer"> android:paddingBottom="@dimen/grid_spacing_outer">
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:handleColor="?attr/colorTertiary"
tools:listitem="@layout/item_feed" /> tools:listitem="@layout/item_feed" />
</FrameLayout> </FrameLayout>

View File

@@ -11,17 +11,17 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="@dimen/list_spacing" android:paddingLeft="@dimen/list_spacing"
android:paddingRight="@dimen/list_spacing"
android:paddingTop="@dimen/grid_spacing_outer" android:paddingTop="@dimen/grid_spacing_outer"
android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer" android:paddingBottom="@dimen/grid_spacing_outer"
app:handleColor="?attr/colorTertiary" app:bubbleSize="small"
tools:layoutManager="org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager" tools:layoutManager="org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager"
tools:listitem="@layout/item_manga_list" /> tools:listitem="@layout/item_manga_list" />

View File

@@ -12,13 +12,12 @@
android:paddingRight="@dimen/list_spacing" android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer"> android:paddingBottom="@dimen/grid_spacing_outer">
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:handleColor="?attr/colorTertiary"
tools:listitem="@layout/item_feed" /> tools:listitem="@layout/item_feed" />
</FrameLayout> </FrameLayout>

View File

@@ -22,13 +22,12 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<org.koitharu.kotatsu.base.ui.widgets.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:handleColor="?attr/colorOutline"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_chapter" /> tools:listitem="@layout/item_chapter" />

View File

@@ -5,6 +5,7 @@
<attr name="multiAutoCompleteTextViewPreferenceStyle" /> <attr name="multiAutoCompleteTextViewPreferenceStyle" />
<attr name="autoCompleteTextViewPreferenceStyle" /> <attr name="autoCompleteTextViewPreferenceStyle" />
<attr name="listItemTextViewStyle" /> <attr name="listItemTextViewStyle" />
<attr name="fastScrollerStyle" />
<declare-styleable name="Theme"> <declare-styleable name="Theme">
<attr name="navigationBarDividerColor" format="color" /> <attr name="navigationBarDividerColor" format="color" />
@@ -53,16 +54,16 @@
</declare-styleable> </declare-styleable>
<declare-styleable name="FastScroller"> <declare-styleable name="FastScroller">
<attr format="boolean" name="hideScrollbar" /> <attr name="hideScrollbar" format="boolean" />
<attr format="boolean" name="showBubble" /> <attr name="showBubble" format="boolean" />
<attr format="boolean" name="showBubbleAlways" /> <attr name="showBubbleAlways" format="boolean" />
<attr format="boolean" name="showTrack" /> <attr name="showTrack" format="boolean" />
<attr format="color" name="bubbleColor" /> <attr name="bubbleColor" format="color" />
<attr format="color" name="bubbleTextColor" /> <attr name="bubbleTextColor" format="color" />
<attr format="color" name="handleColor" /> <attr name="thumbColor" format="color" />
<attr format="color" name="trackColor" /> <attr name="trackColor" format="color" />
<attr format="dimension" name="bubbleTextSize" /> <attr name="bubbleTextSize" format="dimension" />
<attr format="enum" name="bubbleSize"> <attr name="bubbleSize" format="enum">
<enum name="normal" value="0" /> <enum name="normal" value="0" />
<enum name="small" value="1" /> <enum name="small" value="1" />
</attr> </attr>

View File

@@ -44,8 +44,8 @@
<dimen name="fastscroll_bubble_padding">16dp</dimen> <dimen name="fastscroll_bubble_padding">16dp</dimen>
<dimen name="fastscroll_bubble_radius_small">32dp</dimen> <dimen name="fastscroll_bubble_radius_small">32dp</dimen>
<dimen name="fastscroll_bubble_size_small">64dp</dimen> <dimen name="fastscroll_bubble_size_small">36dp</dimen>
<dimen name="fastscroll_bubble_text_size_small">36sp</dimen> <dimen name="fastscroll_bubble_text_size_small">24sp</dimen>
<dimen name="fastscroll_bubble_padding_small">12dp</dimen> <dimen name="fastscroll_bubble_padding_small">12dp</dimen>
<dimen name="fastscroll_handle_height">58dp</dimen> <dimen name="fastscroll_handle_height">58dp</dimen>

View File

@@ -115,6 +115,14 @@
<item name="android:scrollbarStyle">outsideOverlay</item> <item name="android:scrollbarStyle">outsideOverlay</item>
</style> </style>
<style name="Widget.Kotatsu.FastScroller" parent="">
<item name="thumbColor">?colorTertiary</item>
<item name="bubbleColor">?colorTertiary</item>
<item name="bubbleTextColor">?colorOnTertiary</item>
<item name="trackColor">?colorOutline</item>
<item name="bubbleSize">normal</item>
</style>
<style name="Widget.Kotatsu.ListItemTextView" parent=""> <style name="Widget.Kotatsu.ListItemTextView" parent="">
<item name="android:textColor">@color/list_item_text_color</item> <item name="android:textColor">@color/list_item_text_color</item>
<item name="backgroundFillColor">@color/list_item_background_color</item> <item name="backgroundFillColor">@color/list_item_background_color</item>

View File

@@ -71,6 +71,7 @@
<item name="tabStyle">@style/Widget.Kotatsu.Tabs</item> <item name="tabStyle">@style/Widget.Kotatsu.Tabs</item>
<item name="materialCardViewStyle">@style/Widget.Material3.CardView.Filled</item> <item name="materialCardViewStyle">@style/Widget.Material3.CardView.Filled</item>
<item name="recyclerViewStyle">@style/Widget.Kotatsu.RecyclerView</item> <item name="recyclerViewStyle">@style/Widget.Kotatsu.RecyclerView</item>
<item name="fastScrollerStyle">@style/Widget.Kotatsu.FastScroller</item>
<item name="listItemTextViewStyle">@style/Widget.Kotatsu.ListItemTextView</item> <item name="listItemTextViewStyle">@style/Widget.Kotatsu.ListItemTextView</item>
<item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item> <item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item>
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat.M3</item> <item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat.M3</item>