From 1966dd594f8d70245bdd1de646f97c2f0fdc22d0 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 9 Mar 2020 21:09:02 -0700 Subject: [PATCH] Added animation to scrolling sideways in library list mode --- .../ui/library/LibraryCategoryAdapter.kt | 1 + .../ui/library/LibraryCategoryView.kt | 1 + .../tachiyomi/ui/library/LibraryHeaderItem.kt | 1 + .../tachiyomi/ui/library/LibraryHolder.kt | 5 + .../ui/library/LibraryListController.kt | 255 +++++++++++++++++- .../kanade/tachiyomi/ui/main/MainActivity.kt | 19 +- .../tachiyomi/util/view/ViewExtensions.kt | 8 +- 7 files changed, 272 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 1975ab8037..fca03ba4c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index b46ec61726..114cf40908 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt index b8063c7e07..b676f946a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 3e294a3605..b519c33bef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -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() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt index 2d40139e2e..5f7e5b44b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index fcd7a9b6c9..f21ff5ad90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 8feefae628..855ec9c7ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -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)