From d872044252e7f42c88b831116b2b4dd8ca977776 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 14 Sep 2023 11:20:22 +0300 Subject: [PATCH] Improve mouse interaction --- .../core/ui/sheet/AdaptiveSheetBehavior.kt | 12 ++-- .../core/ui/sheet/AdaptiveSheetHeaderBar.kt | 27 +++++++- .../koitharu/kotatsu/core/util/ext/View.kt | 7 +++ .../details/ui/ChaptersBottomSheetMediator.kt | 18 +++++- .../kotatsu/details/ui/DetailsActivity.kt | 47 +++++++++++--- .../ui/adapter/ExploreAdapterDelegates.kt | 26 +++++++- .../list/ui/adapter/MangaGridItemAD.kt | 13 ++-- .../ui/adapter/MangaListDetailedItemAD.kt | 2 + .../list/ui/adapter/MangaListItemAD.kt | 13 ++-- .../koitharu/kotatsu/main/ui/MainActivity.kt | 13 ++-- .../pager/reversed/ReversedReaderFragment.kt | 23 ++++++- .../reader/ui/pager/standard/PageHolder.kt | 1 + .../ui/pager/standard/PagerReaderFragment.kt | 61 +++++++++++++------ .../ui/pager/standard/SsivZoomListener.kt | 32 ++++++++++ .../ui/pager/webtoon/WebtoonScalingFrame.kt | 46 +++++++++++--- app/src/main/res/drawable/list_selector.xml | 11 ++++ .../res/layout-w600dp-land/activity_main.xml | 1 + app/src/main/res/layout/activity_main.xml | 1 + .../res/layout/fragment_reader_webtoon.xml | 1 + .../main/res/layout/item_source_config.xml | 7 ++- app/src/main/res/menu/opt_main.xml | 3 +- build.gradle | 2 +- 22 files changed, 305 insertions(+), 62 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/SsivZoomListener.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt index bedd65148..077eca144 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt @@ -104,6 +104,7 @@ sealed class AdaptiveSheetBehavior { companion object { const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED + const val STATE_COLLAPSED = BottomSheetBehavior.STATE_COLLAPSED const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN @@ -114,10 +115,11 @@ sealed class AdaptiveSheetBehavior { else -> null } - fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) { - is BottomSheetBehavior<*> -> Bottom(behavior) - is SideSheetBehavior<*> -> Side(behavior) - else -> null - } + fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = + when (val behavior = lp.behavior) { + is BottomSheetBehavior<*> -> Bottom(behavior) + is SideSheetBehavior<*> -> Side(behavior) + else -> null + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt index 055454628..cf70fbc0a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt @@ -2,7 +2,9 @@ package org.koitharu.kotatsu.core.ui.sheet import android.content.Context import android.util.AttributeSet +import android.view.InputDevice import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.widget.LinearLayout import androidx.annotation.AttrRes @@ -21,7 +23,8 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor( @AttrRes defStyleAttr: Int = 0, ) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback { - private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this) + private val binding = + LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this) private var sheetBehavior: AdaptiveSheetBehavior? = null var title: CharSequence? @@ -60,6 +63,28 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor( super.onDetachedFromWindow() } + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + val behavior = sheetBehavior ?: return super.onGenericMotionEvent(event) + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) { + behavior.state = if ( + behavior is AdaptiveSheetBehavior.Bottom + && behavior.state == AdaptiveSheetBehavior.STATE_EXPANDED + ) { + AdaptiveSheetBehavior.STATE_COLLAPSED + } else { + AdaptiveSheetBehavior.STATE_HIDDEN + } + } else { + behavior.state = AdaptiveSheetBehavior.STATE_EXPANDED + } + return true + } + } + return super.onGenericMotionEvent(event) + } + override fun onStateChanged(sheet: View, newState: Int) { } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt index 41048c7e1..0577ad383 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.util.ext import android.app.Activity import android.graphics.Rect +import android.os.Build import android.view.View import android.view.View.MeasureSpec import android.view.ViewGroup @@ -140,3 +141,9 @@ fun BaseProgressIndicator<*>.showOrHide(value: Boolean) { if (isVisible) hide() } } + +fun View.setOnContextClickListenerCompat(listener: View.OnLongClickListener) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnContextClickListener(listener::onLongClick) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt index 8187b7fdf..f74d65393 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.details.ui +import android.view.InputDevice +import android.view.MotionEvent import android.view.View import android.view.View.OnLayoutChangeListener import androidx.activity.OnBackPressedCallback @@ -12,7 +14,7 @@ class ChaptersBottomSheetMediator( private val behavior: BottomSheetBehavior<*>, ) : OnBackPressedCallback(false), ActionModeListener, - OnLayoutChangeListener { + OnLayoutChangeListener, View.OnGenericMotionListener { private var lockCounter = 0 @@ -55,6 +57,20 @@ class ChaptersBottomSheetMediator( } } + override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) { + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + return true + } + } + return false + } + fun lock() { lockCounter++ behavior.isDraggable = lockCounter <= 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 86b351120..95e5af30b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -49,6 +49,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.service.MangaPrefetchService @@ -89,6 +90,7 @@ class DetailsActivity : } viewBinding.buttonRead.setOnClickListener(this) viewBinding.buttonRead.setOnLongClickListener(this) + viewBinding.buttonRead.setOnContextClickListenerCompat(this) viewBinding.buttonDropdown.setOnClickListener(this) viewBadge = ViewBadge(viewBinding.buttonRead, this) @@ -103,6 +105,7 @@ class DetailsActivity : viewBinding.toolbarChapters?.setNavigationOnClickListener { behavior.state = BottomSheetBehavior.STATE_COLLAPSED } + viewBinding.toolbarChapters?.setOnGenericMotionListener(bsMediator) } else { chaptersMenuProvider = ChaptersMenuProvider(viewModel, null) addMenuProvider(chaptersMenuProvider) @@ -134,13 +137,19 @@ class DetailsActivity : viewBinding.toolbarChapters?.subtitle = it viewBinding.textViewSubtitle?.textAndVisible = it } - viewModel.isChaptersReversed.observe(this, MenuInvalidator(viewBinding.toolbarChapters ?: this)) + viewModel.isChaptersReversed.observe( + this, + MenuInvalidator(viewBinding.toolbarChapters ?: this) + ) viewModel.favouriteCategories.observe(this, MenuInvalidator(this)) viewModel.branches.observe(this) { viewBinding.buttonDropdown.isVisible = it.size > 1 } viewModel.chapters.observe(this, PrefetchObserver(this)) - viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails)) + viewModel.onDownloadStarted.observeEvent( + this, + DownloadStartedObserver(viewBinding.containerDetails) + ) addMenuProvider( DetailsMenuProvider( @@ -243,7 +252,11 @@ class DetailsActivity : right = insets.right, ) if (insets.bottom > 0) { - window.setNavigationBarTransparentCompat(this, viewBinding.layoutBottom?.elevation ?: 0f, 0.9f) + window.setNavigationBarTransparentCompat( + this, + viewBinding.layoutBottom?.elevation ?: 0f, + 0.9f + ) } viewBinding.cardChapters?.updateLayoutParams { bottomMargin = insets.bottom + marginEnd @@ -265,9 +278,18 @@ class DetailsActivity : } val text = when { !info.isValid -> getString(R.string.loading_) - info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) + info.currentChapter >= 0 -> getString( + R.string.chapter_d_of_d, + info.currentChapter + 1, + info.totalChapters + ) + info.totalChapters == 0 -> getString(R.string.no_chapters) - else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) + else -> resources.getQuantityString( + R.plurals.chapters, + info.totalChapters, + info.totalChapters + ) } viewBinding.toolbarChapters?.title = text viewBinding.textViewTitle?.text = text @@ -286,7 +308,12 @@ class DetailsActivity : append(' ') append(' ') inSpans( - ForegroundColorSpan(v.context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY)), + ForegroundColorSpan( + v.context.getThemeColor( + android.R.attr.textColorSecondary, + Color.LTGRAY + ) + ), RelativeSizeSpan(0.74f), ) { append(branch.count.toString()) @@ -305,7 +332,8 @@ class DetailsActivity : val manga = viewModel.manga.value ?: return val chapterId = viewModel.historyInfo.value.history?.chapterId if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { - val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) + val snackbar = + makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) snackbar.show() } else { startActivity( @@ -331,7 +359,10 @@ class DetailsActivity : view.isVisible = isVisible } - private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar { + private fun makeSnackbar( + text: CharSequence, + @BaseTransientBottomBar.Duration duration: Int, + ): Snackbar { val sb = Snackbar.make(viewBinding.containerDetails, text, duration) if (viewBinding.layoutBottom?.isVisible == true) { sb.anchorView = viewBinding.toolbarChapters diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt index 08f10ba29..83d40985d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt @@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding @@ -44,7 +45,12 @@ fun exploreButtonsAD( if (item.isRandomLoading) { val icon = CircularProgressDrawable(context) icon.strokeWidth = context.resources.resolveDp(2f) - icon.setColorSchemeColors(context.getThemeColor(materialR.attr.colorPrimary, Color.DKGRAY)) + icon.setColorSchemeColors( + context.getThemeColor( + materialR.attr.colorPrimary, + Color.DKGRAY + ) + ) binding.buttonRandom.icon = icon icon.start() } else { @@ -88,7 +94,13 @@ fun exploreSourceListItemAD( listener: OnListItemClickListener, lifecycleOwner: LifecycleOwner, ) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) }, + { layoutInflater, parent -> + ItemExploreSourceListBinding.inflate( + layoutInflater, + parent, + false + ) + }, on = { item, _, _ -> item is MangaSourceItem && !item.isGrid }, ) { @@ -96,6 +108,7 @@ fun exploreSourceListItemAD( binding.root.setOnClickListener(eventListener) binding.root.setOnLongClickListener(eventListener) + binding.root.setOnContextClickListenerCompat(eventListener) bind { binding.textViewTitle.text = item.source.title @@ -115,7 +128,13 @@ fun exploreSourceGridItemAD( listener: OnListItemClickListener, lifecycleOwner: LifecycleOwner, ) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) }, + { layoutInflater, parent -> + ItemExploreSourceGridBinding.inflate( + layoutInflater, + parent, + false + ) + }, on = { item, _, _ -> item is MangaSourceItem && item.isGrid }, ) { @@ -123,6 +142,7 @@ fun exploreSourceGridItemAD( binding.root.setOnClickListener(eventListener) binding.root.setOnLongClickListener(eventListener) + binding.root.setOnContextClickListenerCompat(eventListener) bind { binding.textViewTitle.text = item.source.title diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index caaeb78c4..0840404e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.list.ui.adapter +import android.view.View import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.google.android.material.badge.BadgeDrawable @@ -10,6 +11,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.list.ui.ListModelDiffCallback @@ -28,12 +30,13 @@ fun mangaGridItemAD( ) { var badge: BadgeDrawable? = null - itemView.setOnClickListener { - clickListener.onItemClick(item.manga, it) - } - itemView.setOnLongClickListener { - clickListener.onItemLongClick(item.manga, it) + val eventListener = object : View.OnClickListener, View.OnLongClickListener { + override fun onClick(v: View) = clickListener.onItemClick(item.manga, v) + override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v) } + itemView.setOnClickListener(eventListener) + itemView.setOnLongClickListener(eventListener) + itemView.setOnContextClickListenerCompat(eventListener) sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView) bind { payloads -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 08f7e3f14..77a197d14 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding @@ -45,6 +46,7 @@ fun mangaListDetailedItemAD( } itemView.setOnClickListener(listenerAdapter) itemView.setOnLongClickListener(listenerAdapter) + itemView.setOnContextClickListenerCompat(listenerAdapter) binding.buttonRead.setOnClickListener(listenerAdapter) binding.chipsTags.onChipClickListener = listenerAdapter diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 46b1974b4..c15805d59 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.list.ui.adapter +import android.view.View import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.google.android.material.badge.BadgeDrawable @@ -9,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListBinding @@ -25,12 +27,13 @@ fun mangaListItemAD( ) { var badge: BadgeDrawable? = null - itemView.setOnClickListener { - clickListener.onItemClick(item.manga, it) - } - itemView.setOnLongClickListener { - clickListener.onItemLongClick(item.manga, it) + val eventListener = object : View.OnClickListener, View.OnLongClickListener { + override fun onClick(v: View) = clickListener.onItemClick(item.manga, v) + override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v) } + itemView.setOnClickListener(eventListener) + itemView.setOnLongClickListener(eventListener) + itemView.setOnContextClickListenerCompat(eventListener) bind { binding.textViewTitle.text = item.title diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index a3e01a2ae..95fa26f61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -376,9 +376,14 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal) viewBinding.appbar.updatePadding(left = padding, right = padding) adjustFabVisibility(isSearchOpened = isOpened) - supportActionBar?.setHomeAsUpIndicator( - if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, - ) + supportActionBar?.apply { + setHomeAsUpIndicator( + if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, + ) + setHomeActionContentDescription( + if (isOpened) R.string.back else R.string.search, + ) + } viewBinding.searchView.setHintCompat( if (isOpened) R.string.search_hint else R.string.search_manga, ) @@ -394,7 +399,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), - 1 + 1, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt index 73a7f1107..7238d99a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt @@ -1,7 +1,11 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import androidx.core.view.children import com.google.android.material.snackbar.Snackbar @@ -26,9 +30,11 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import javax.inject.Inject import kotlin.math.absoluteValue +import kotlin.math.sign @AndroidEntryPoint -class ReversedReaderFragment : BaseReaderFragment() { +class ReversedReaderFragment : BaseReaderFragment(), + View.OnGenericMotionListener { @Inject lateinit var networkState: NetworkState @@ -47,6 +53,7 @@ class ReversedReaderFragment : BaseReaderFragment adapter = readerAdapter offscreenPageLimit = 2 doOnPageChanged(::notifyPageChanged) + setOnGenericMotionListener(this@ReversedReaderFragment) } viewModel.pageAnimation.observe(viewLifecycleOwner) { @@ -69,6 +76,20 @@ class ReversedReaderFragment : BaseReaderFragment super.onDestroyView() } + override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0 + if (!withCtrl) { + switchPageBy(-axisValue.sign.toInt()) + return true + } + } + } + return false + } + override fun onCreateAdapter() = ReversedPagesAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index 9c568fad6..6842401c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -35,6 +35,7 @@ open class PageHolder( binding.ssiv.bindToLifecycle(owner) binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice() binding.ssiv.addOnImageEventListener(delegate) + binding.ssiv.setOnGenericMotionListener(SsivZoomListener()) @Suppress("LeakingThis") bindingInfo.buttonRetry.setOnClickListener(this) @Suppress("LeakingThis") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt index 5d5c7f6a5..a92979509 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt @@ -1,7 +1,11 @@ package org.koitharu.kotatsu.reader.ui.pager.standard import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import androidx.core.view.children import com.google.android.material.snackbar.Snackbar @@ -24,9 +28,11 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject import kotlin.math.absoluteValue +import kotlin.math.sign @AndroidEntryPoint -class PagerReaderFragment : BaseReaderFragment() { +class PagerReaderFragment : BaseReaderFragment(), + View.OnGenericMotionListener { @Inject lateinit var networkState: NetworkState @@ -39,12 +45,16 @@ class PagerReaderFragment : BaseReaderFragment() container: ViewGroup?, ) = FragmentReaderStandardBinding.inflate(inflater, container, false) - override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { + override fun onViewBindingCreated( + binding: FragmentReaderStandardBinding, + savedInstanceState: Bundle?, + ) { super.onViewBindingCreated(binding, savedInstanceState) with(binding.pager) { adapter = readerAdapter offscreenPageLimit = 2 doOnPageChanged(::notifyPageChanged) + setOnGenericMotionListener(this@PagerReaderFragment) } viewModel.pageAnimation.observe(viewLifecycleOwner) { @@ -67,27 +77,42 @@ class PagerReaderFragment : BaseReaderFragment() super.onDestroyView() } - override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = coroutineScope { - val items = async { - requireAdapter().setItems(pages) - yield() + override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0 + if (!withCtrl) { + switchPageBy(-axisValue.sign.toInt()) + return true + } + } } - if (pendingState != null) { - val position = pages.indexOfFirst { - it.chapterId == pendingState.chapterId && it.index == pendingState.page + return false + } + + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = + coroutineScope { + val items = async { + requireAdapter().setItems(pages) + yield() } - items.await() - if (position != -1) { - requireViewBinding().pager.setCurrentItem(position, false) - notifyPageChanged(position) + if (pendingState != null) { + val position = pages.indexOfFirst { + it.chapterId == pendingState.chapterId && it.index == pendingState.page + } + items.await() + if (position != -1) { + requireViewBinding().pager.setCurrentItem(position, false) + notifyPageChanged(position) + } else { + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() + } } else { - Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) - .show() + items.await() } - } else { - items.await() } - } override fun onCreateAdapter() = PagesAdapter( lifecycleOwner = viewLifecycleOwner, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/SsivZoomListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/SsivZoomListener.kt new file mode 100644 index 000000000..fc69d32ad --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/SsivZoomListener.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.reader.ui.pager.standard + +import android.graphics.PointF +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.View.OnGenericMotionListener +import android.view.animation.DecelerateInterpolator +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView + +class SsivZoomListener : OnGenericMotionListener { + + override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { + val ssiv = v as? SubsamplingScaleImageView ?: return false + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0 + if (withCtrl || ssiv.scale > ssiv.minScale) { + val center = PointF(event.x, event.y) + val scale = ssiv.scale + axisValue * 1.6f + (ssiv.animateScaleAndCenter(scale, center) ?: return false) + .withInterpolator(DecelerateInterpolator()) + .start() + return true + } + } + } + return false + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt index d871b3320..ca8a38585 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt @@ -7,6 +7,8 @@ import android.graphics.Rect import android.graphics.RectF import android.util.AttributeSet import android.view.GestureDetector +import android.view.InputDevice +import android.view.KeyEvent import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.animation.AccelerateDecelerateInterpolator @@ -16,6 +18,7 @@ import androidx.core.view.GestureDetectorCompat private const val MAX_SCALE = 2.5f private const val MIN_SCALE = 0.5f +private const val WHEEL_SCALE_FACTOR = 0.2f class WebtoonScalingFrame @JvmOverloads constructor( context: Context, @@ -77,10 +80,26 @@ class WebtoonScalingFrame @JvmOverloads constructor( return super.dispatchTouchEvent(ev) } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - halfWidth = measuredWidth / 2f - halfHeight = measuredHeight / 2f + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0 + if (withCtrl) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + val newScale = + (scale + axisValue * WHEEL_SCALE_FACTOR).coerceIn(MIN_SCALE, MAX_SCALE) + scaleChild(newScale, event.x, event.y) + return true + } + } + } + return super.onGenericMotionEvent(event) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + halfWidth = w / 2f + halfHeight = h / 2f } private fun invalidateTarget() { @@ -161,7 +180,12 @@ class WebtoonScalingFrame @JvmOverloads constructor( private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable { - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float, + ): Boolean { if (scale <= 1f) return false transformMatrix.postTranslate(-distanceX, -distanceY) invalidateTarget() @@ -181,7 +205,12 @@ class WebtoonScalingFrame @JvmOverloads constructor( return true } - override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float, + ): Boolean { if (scale <= 1) return false overScroller.fling( @@ -200,7 +229,10 @@ class WebtoonScalingFrame @JvmOverloads constructor( override fun run() { if (overScroller.computeScrollOffset()) { - transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY) + transformMatrix.postTranslate( + overScroller.currX - transX, + overScroller.currY - transY + ) invalidateTarget() postOnAnimation(this) } diff --git a/app/src/main/res/drawable/list_selector.xml b/app/src/main/res/drawable/list_selector.xml index 4299c0e1e..5142ab10e 100644 --- a/app/src/main/res/drawable/list_selector.xml +++ b/app/src/main/res/drawable/list_selector.xml @@ -26,6 +26,17 @@ + + + + + + + + android:src="@drawable/ic_add" + android:tooltipText="@string/add" /> + android:src="@drawable/ic_delete" + android:tooltipText="@string/remove" /> diff --git a/app/src/main/res/menu/opt_main.xml b/app/src/main/res/menu/opt_main.xml index ef76a5e4d..167abbe1d 100644 --- a/app/src/main/res/menu/opt_main.xml +++ b/app/src/main/res/menu/opt_main.xml @@ -7,7 +7,8 @@ android:id="@+id/action_app_update" android:icon="@drawable/ic_app_update" android:orderInCategory="1" - android:title="@string/update" + android:title="@string/app_update_available" + android:titleCondensed="@string/update" android:visible="false" app:showAsAction="ifRoom" /> diff --git a/build.gradle b/build.gradle index 8385c1e4a..fb01f1a2a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.1.1' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48' - classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.0-1.0.13' + classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.10-1.0.13' } }