Added animation to scrolling sideways in library list mode

This commit is contained in:
Jay 2020-03-09 21:09:02 -07:00
parent 27932e9c06
commit 1966dd594f
7 changed files with 272 additions and 18 deletions

View File

@ -183,5 +183,6 @@ class LibraryCategoryAdapter(val libraryListener: LibraryListener) :
fun selectAll(position: Int)
fun allSelected(position: Int): Boolean
fun showCategories(position: Int, view: View)
fun recyclerIsScrolling(): Boolean
}
}

View File

@ -399,4 +399,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun selectAll(position: Int) { }
override fun allSelected(position: Int): Boolean = false
override fun showCategories(position: Int, view: View) { }
override fun recyclerIsScrolling() = false
}

View File

@ -144,6 +144,7 @@ class LibraryHeaderItem(private val categoryF: (Int) -> Category, val catId: Int
}
}
private fun showCatSortOptions() {
if (adapter.libraryListener.recyclerIsScrolling()) return
val category =
(adapter.getItem(adapterPosition) as? LibraryHeaderItem)?.category ?: return
// Create a PopupMenu, giving it the clicked view for an anchor

View File

@ -50,4 +50,9 @@ abstract class LibraryHolder(
super.onItemReleased(position)
(adapter as? LibraryCategoryAdapter)?.libraryListener?.onItemReleased(position)
}
override fun onLongClick(view: View?): Boolean {
super.onLongClick(view)
return !adapter.libraryListener.recyclerIsScrolling()
}
}

View File

@ -1,14 +1,20 @@
package eu.kanade.tachiyomi.ui.library
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.math.MathUtils.clamp
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
@ -26,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.main.OnTouchEventInterface
import eu.kanade.tachiyomi.ui.main.SpinnerTitleInterface
import eu.kanade.tachiyomi.ui.main.SwipeGestureInterface
import eu.kanade.tachiyomi.util.system.dpToPx
@ -33,6 +40,7 @@ import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
import kotlinx.android.synthetic.main.filter_bottom_sheet.*
import kotlinx.android.synthetic.main.library_grid_recycler.*
@ -40,8 +48,13 @@ import kotlinx.android.synthetic.main.library_list_controller.*
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.spinner_title.view.*
import kotlinx.coroutines.delay
import timber.log.Timber
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
FlexibleAdapter.OnItemClickListener,
@ -49,6 +62,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
FlexibleAdapter.OnItemMoveListener,
LibraryCategoryAdapter.LibraryListener,
SpinnerTitleInterface,
OnTouchEventInterface,
SwipeGestureInterface {
private lateinit var adapter: LibraryCategoryAdapter
@ -66,6 +80,17 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
private var switchingCategories = false
var startPosX:Float? = null
var startPosY:Float? = null
var moved = false
var lockedRecycler = false
var lockedY = false
var nextCategory:Int? = null
var ogCategory:Int? = null
var prevCategory:Int? = null
private val swipeDistance = 300f
var flinging = false
/**
* Recycler view of the list of manga.
*/
@ -73,7 +98,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
override fun contentView():View = recycler_layout
/* override fun getTitle(): String? {
override fun getTitle(): String? {
return if (::customTitleSpinner.isInitialized) customTitleSpinner.category_title.text.toString()
else super.getTitle()
// when {
@ -81,7 +106,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
// spinnerAdapter?.array?.size == 1 -> return spinnerAdapter?.array?.firstOrNull()
// else -> return super.getTitle()
// }
}*/
}
private var scrollListener = object : RecyclerView.OnScrollListener () {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -115,6 +140,156 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
}
}
override fun onTouchEvent(event: MotionEvent?) {
if (event == null) {
resetScrollingValues()
resetRecyclerY()
return
}
if (flinging) return
val sheetRect = Rect()
val recyclerRect = Rect()
bottom_sheet.getGlobalVisibleRect(sheetRect)
view?.getGlobalVisibleRect(recyclerRect)
if (sheetRect.contains(event.x.toInt(), event.y.toInt()) ||
!recyclerRect.contains(event.x.toInt(), event.y.toInt())) {
return
}
if (startPosX == null) {
startPosX = event.rawX
startPosY = event.rawY
val position =
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val order = when (val item = adapter.getItem(position)) {
is LibraryHeaderItem -> item.category.order
is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order
else -> null
}
if (order != null) {
ogCategory = order
var newOffsetN = order + 1
while (adapter.indexOf(newOffsetN) == -1 && presenter.categories.any { it.order == newOffsetN }) {
newOffsetN += 1
}
if (adapter.indexOf(newOffsetN) != -1)
nextCategory = newOffsetN
if (position == 0) prevCategory = null
else {
var newOffsetP = order - 1
while (adapter.indexOf(newOffsetP) == -1 && presenter.categories.any { it.order == newOffsetP }) {
newOffsetP -= 1
}
if (adapter.indexOf(newOffsetP) != -1)
prevCategory = newOffsetP
}
}
}
else if (event.actionMasked != MotionEvent.ACTION_UP && startPosX != null) {
val distance = abs(event.rawX - startPosX!!)
val sign = sign(event.rawX - startPosX!!)
if (lockedY) return
if (distance > 60 && abs(event.rawY - startPosY!!) <= 30 &&
!lockedRecycler) {
lockedRecycler = true
switchingCategories = true
if ((prevCategory == null && sign > 0) || (nextCategory == null && sign < 0)) {
recycler_layout.x = 0f
recycler_layout.alpha = 1f
return
}
else
recycler.suppressLayout(true)
}
else if (!lockedRecycler && abs(event.rawY - startPosY!!) > 30) {
lockedY = true
resetRecyclerY()
return
}
if ((prevCategory == null && sign > 0) || (nextCategory == null && sign < 0)) {
resetRecyclerY()
return
}
if (abs(event.rawY - startPosY!!) <= 30 || recycler.isLayoutSuppressed
|| lockedRecycler) {
if (distance <= swipeDistance * 1.1f) {
recycler_layout.x = (max(0f, distance - 50f) * sign) / 3
recycler_layout.alpha =
(1f - (distance - (swipeDistance * 0.1f)) / swipeDistance)
if (moved) {
scrollToHeader(ogCategory ?: -1)
moved = false
}
} else {
if (!moved) {
scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1)
moved = true
}
recycler_layout.x = ((distance - swipeDistance * 2) * sign) / 3
recycler_layout.alpha = ((distance - swipeDistance * 1.1f) / swipeDistance)
if (sign > 0) {
recycler_layout.x = min(0f, recycler_layout.x)
} else {
recycler_layout.x = max(0f, recycler_layout.x)
}
recycler_layout.alpha = min(1f, recycler_layout.alpha)
}
}
}
else if (event.actionMasked == MotionEvent.ACTION_UP) {
recycler_layout.post {
if (!flinging) {
resetScrollingValues()
resetRecyclerY(true)
}
}
}
}
private fun resetScrollingValues() {
startPosX = null
startPosY = null
nextCategory = null
prevCategory = null
ogCategory = null
lockedY = false
}
private fun resetRecyclerY(animated: Boolean = false, time: Long = 100) {
moved = false
lockedRecycler = false
if (animated) {
val set = AnimatorSet()
val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, 0f)
translationXAnimator.duration = time
translationXAnimator.addUpdateListener {
animation -> recycler_layout.x = animation.animatedValue as Float
}
val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 1f)
translationAlphaAnimator.duration = time
translationAlphaAnimator.addUpdateListener {
animation -> recycler_layout.alpha = animation.animatedValue as Float
}
set.playTogether(translationXAnimator, translationAlphaAnimator)
set.start()
launchUI {
delay(time)
if (!lockedRecycler) switchingCategories = false
}
}
else {
recycler_layout.x = 0f
recycler_layout.alpha = 1f
switchingCategories = false
}
recycler.suppressLayout(false)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.library_list_controller, container, false)
}
@ -133,7 +308,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
})
recycler.setHasFixedSize(true)
recycler.adapter = adapter
adapter.fastScroller = fast_scroller
//adapter.fastScroller = fast_scroller
recycler.addOnScrollListener(scrollListener)
val tv = TypedValue()
@ -258,14 +433,26 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
}
}
private fun scrollToHeader(pos: Int, fade:Boolean = false) {
private fun scrollToHeader(pos: Int) {
val headerPosition = adapter.indexOf(pos)
switchingCategories = true
if (headerPosition > -1) {
activity?.appbar?.y = 0f
val appbar = activity?.appbar
//if (headerPosition == 0)
//activity?.appbar?.y = 0f
recycler.suppressLayout(true)
val appbarOffset =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (appbar?.y ?: 0f > -20) 0 else
(appbar?.y?.plus(view?.rootWindowInsets?.systemWindowInsetTop ?: 0)
?: 0f).roundToInt() + 10.dpToPx
}
else {
0
}
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
headerPosition, if (headerPosition == 0) 0 else (-30).dpToPx
headerPosition, (if (headerPosition == 0) 0 else (-28).dpToPx)
+ appbarOffset
)
recycler.suppressLayout(false)
}
@ -349,6 +536,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
}
override fun startReading(position: Int) {
if (recyclerIsScrolling()) return
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(position)
return
@ -381,7 +569,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(view: View?, position: Int): Boolean {
if (switchingCategories) return false
if (recyclerIsScrolling()) return false
val item = adapter.getItem(position) as? LibraryItem ?: return false
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
lastClickPosition = position
@ -399,6 +587,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
if (recyclerIsScrolling()) return
createActionModeIfNeeded()
when {
lastClickPosition == -1 -> setSelection(position)
@ -586,6 +775,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
override fun onSwipeRight(x: Float, y: Float) = goToNextCategory(x, y,1)
private fun goToNextCategory(x: Float, y: Float, offset: Int) {
/*
val sheetRect = Rect()
val recyclerRect = Rect()
bottom_sheet.getGlobalVisibleRect(sheetRect)
@ -594,13 +784,58 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
if (sheetRect.contains(x.toInt(), y.toInt()) ||
!recyclerRect.contains(x.toInt(), y.toInt())) {
return
}*/
//jumpToCategory(offset)
if (lockedRecycler && abs(x) > 1000f) {
val sign = sign(x).roundToInt()
val distance = recycler_layout.alpha
val speed = max(3000f / abs(x), 0.75f)
Timber.d("Flinged $distance, velo ${abs(x)}, speed $speed")
if (sign(recycler_layout.x) == sign(x)) {
flinging = true
val duration = (distance * 100 * speed).toLong()
val set = AnimatorSet()
val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, sign * 100f)
translationXAnimator.duration = duration
translationXAnimator.addUpdateListener { animation ->
recycler_layout.x = animation.animatedValue as Float
}
val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 0f)
translationAlphaAnimator.duration = duration
translationAlphaAnimator.addUpdateListener { animation ->
recycler_layout.alpha = animation.animatedValue as Float
}
set.playTogether(translationXAnimator, translationAlphaAnimator)
set.start()
set.addListener(object : Animator.AnimatorListener {
override fun onAnimationEnd(animation: Animator?) {
recycler_layout.x = -sign * 100f
recycler_layout.alpha = 0f
scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1)
resetScrollingValues()
resetRecyclerY(true, (100 * speed).toLong())
flinging = false
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
}
}
}
private fun jumpToCategory(offset: Int) {
val position =
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val order = when (val item = adapter.getItem(position)) {
is LibraryHeaderItem -> item.category.order
is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order
?.plus(if (offset < 0) 1 else 0)
else -> null
}
if (order != null) {
@ -608,9 +843,11 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
while (adapter.indexOf(newOffset) == -1 && presenter.categories.any { it.order == newOffset }) {
newOffset += offset
}
scrollToHeader (newOffset, true)
scrollToHeader(newOffset)
}
}
override fun popUpMenu(): PopupMenu = titlePopupMenu
override fun recyclerIsScrolling() = switchingCategories || lockedRecycler || lockedY
}

View File

@ -11,6 +11,7 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.GestureDetector
import android.view.MenuItem
import android.view.MotionEvent
@ -383,7 +384,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
return super.startSupportActionMode(callback)
}
/* override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) {
override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) launchUI {
val scale = Settings.Global.getFloat(
contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
@ -391,10 +392,11 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
val duration = resources.getInteger(android.R.integer.config_mediumAnimTime) * scale
delay(duration.toLong())
delay(100)
window?.statusBarColor = getResourceColor(android.R.attr.statusBarColor)
window?.statusBarColor = ColorUtils.setAlphaComponent(getResourceColor(android.R.attr
.colorBackground), 175)
}
super.onSupportActionModeFinished(mode)
}*/
}
private fun setExtensionsBadge() {
val updates = preferences.extensionUpdatesCount().getOrDefault()
@ -553,6 +555,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
gestureDetector.onTouchEvent(ev)
val controller = router.backstack.lastOrNull()?.controller()
if (controller is OnTouchEventInterface)
controller.onTouchEvent(ev)
if (ev?.action == MotionEvent.ACTION_DOWN) {
if (snackBar != null && snackBar!!.isShown) {
val sRect = Rect()
@ -687,9 +692,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
&& abs(diffY) <= Companion.SWIPE_THRESHOLD * 0.75f
) {
if (diffX > 0) {
currentGestureDelegate?.onSwipeRight(e1.x, e1.y)
currentGestureDelegate?.onSwipeRight(velocityX, e1.y)
} else {
currentGestureDelegate?.onSwipeLeft(e1.x, e1.y)
currentGestureDelegate?.onSwipeLeft(velocityX, e1.y)
}
result = true
}
@ -739,6 +744,10 @@ interface BottomNavBarInterface {
interface RootSearchInterface
interface OnTouchEventInterface {
fun onTouchEvent(event: MotionEvent?)
}
interface SpinnerTitleInterface {
fun popUpMenu(): PopupMenu
}

View File

@ -317,7 +317,7 @@ fun Controller.scrollViewWith(recycler: RecyclerView,
swipeRefreshLayout: SwipeRefreshLayout? = null,
f: ((WindowInsets) -> Unit)? = null) {
var statusBarHeight = -1
activity!!.appbar.y = 0f
activity?.appbar?.y = 0f
recycler.doOnApplyWindowInsets { view, insets, _ ->
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
val array = view.context.obtainStyledAttributes(attrsArray)
@ -335,13 +335,13 @@ fun Controller.scrollViewWith(recycler: RecyclerView,
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith &&
if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith &&
statusBarHeight > -1 &&
activity!!.appbar.height > 0) {
activity!!.appbar.y -= dy
activity!!.appbar.y = clamp(
activity!!.appbar.y,
-activity!!.appbar.height.toFloat(),// + statusBarHeight,
-activity!!.appbar.height.toFloat(),
0f
)
}
@ -350,7 +350,7 @@ fun Controller.scrollViewWith(recycler: RecyclerView,
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith &&
if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith &&
statusBarHeight > -1 &&
activity!!.appbar.height > 0) {
val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2)