Improve mouse interaction

pull/497/head
Koitharu 3 years ago
parent f4313525c2
commit d872044252
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -104,6 +104,7 @@ sealed class AdaptiveSheetBehavior {
companion object { companion object {
const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED
const val STATE_COLLAPSED = BottomSheetBehavior.STATE_COLLAPSED
const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
@ -114,10 +115,11 @@ sealed class AdaptiveSheetBehavior {
else -> null else -> null
} }
fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) { fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? =
is BottomSheetBehavior<*> -> Bottom(behavior) when (val behavior = lp.behavior) {
is SideSheetBehavior<*> -> Side(behavior) is BottomSheetBehavior<*> -> Bottom(behavior)
else -> null is SideSheetBehavior<*> -> Side(behavior)
} else -> null
}
} }
} }

@ -2,7 +2,9 @@ package org.koitharu.kotatsu.core.ui.sheet
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.InputDevice
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@ -21,7 +23,8 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0, @AttrRes defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback { ) : 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 private var sheetBehavior: AdaptiveSheetBehavior? = null
var title: CharSequence? var title: CharSequence?
@ -60,6 +63,28 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor(
super.onDetachedFromWindow() 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) { override fun onStateChanged(sheet: View, newState: Int) {
} }

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.util.ext
import android.app.Activity import android.app.Activity
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.view.View import android.view.View
import android.view.View.MeasureSpec import android.view.View.MeasureSpec
import android.view.ViewGroup import android.view.ViewGroup
@ -140,3 +141,9 @@ fun BaseProgressIndicator<*>.showOrHide(value: Boolean) {
if (isVisible) hide() if (isVisible) hide()
} }
} }
fun View.setOnContextClickListenerCompat(listener: View.OnLongClickListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnContextClickListener(listener::onLongClick)
}
}

@ -1,5 +1,7 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.view.InputDevice
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.OnLayoutChangeListener import android.view.View.OnLayoutChangeListener
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
@ -12,7 +14,7 @@ class ChaptersBottomSheetMediator(
private val behavior: BottomSheetBehavior<*>, private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(false), ) : OnBackPressedCallback(false),
ActionModeListener, ActionModeListener,
OnLayoutChangeListener { OnLayoutChangeListener, View.OnGenericMotionListener {
private var lockCounter = 0 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() { fun lock() {
lockCounter++ lockCounter++
behavior.isDraggable = lockCounter <= 0 behavior.isDraggable = lockCounter <= 0

@ -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.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe 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.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
@ -89,6 +90,7 @@ class DetailsActivity :
} }
viewBinding.buttonRead.setOnClickListener(this) viewBinding.buttonRead.setOnClickListener(this)
viewBinding.buttonRead.setOnLongClickListener(this) viewBinding.buttonRead.setOnLongClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
viewBinding.buttonDropdown.setOnClickListener(this) viewBinding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(viewBinding.buttonRead, this) viewBadge = ViewBadge(viewBinding.buttonRead, this)
@ -103,6 +105,7 @@ class DetailsActivity :
viewBinding.toolbarChapters?.setNavigationOnClickListener { viewBinding.toolbarChapters?.setNavigationOnClickListener {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED behavior.state = BottomSheetBehavior.STATE_COLLAPSED
} }
viewBinding.toolbarChapters?.setOnGenericMotionListener(bsMediator)
} else { } else {
chaptersMenuProvider = ChaptersMenuProvider(viewModel, null) chaptersMenuProvider = ChaptersMenuProvider(viewModel, null)
addMenuProvider(chaptersMenuProvider) addMenuProvider(chaptersMenuProvider)
@ -134,13 +137,19 @@ class DetailsActivity :
viewBinding.toolbarChapters?.subtitle = it viewBinding.toolbarChapters?.subtitle = it
viewBinding.textViewSubtitle?.textAndVisible = 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.favouriteCategories.observe(this, MenuInvalidator(this))
viewModel.branches.observe(this) { viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1 viewBinding.buttonDropdown.isVisible = it.size > 1
} }
viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails)) viewModel.onDownloadStarted.observeEvent(
this,
DownloadStartedObserver(viewBinding.containerDetails)
)
addMenuProvider( addMenuProvider(
DetailsMenuProvider( DetailsMenuProvider(
@ -243,7 +252,11 @@ class DetailsActivity :
right = insets.right, right = insets.right,
) )
if (insets.bottom > 0) { 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<MarginLayoutParams> { viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom + marginEnd bottomMargin = insets.bottom + marginEnd
@ -265,9 +278,18 @@ class DetailsActivity :
} }
val text = when { val text = when {
!info.isValid -> getString(R.string.loading_) !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) 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.toolbarChapters?.title = text
viewBinding.textViewTitle?.text = text viewBinding.textViewTitle?.text = text
@ -286,7 +308,12 @@ class DetailsActivity :
append(' ') append(' ')
append(' ') append(' ')
inSpans( inSpans(
ForegroundColorSpan(v.context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY)), ForegroundColorSpan(
v.context.getThemeColor(
android.R.attr.textColorSecondary,
Color.LTGRAY
)
),
RelativeSizeSpan(0.74f), RelativeSizeSpan(0.74f),
) { ) {
append(branch.count.toString()) append(branch.count.toString())
@ -305,7 +332,8 @@ class DetailsActivity :
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
val chapterId = viewModel.historyInfo.value.history?.chapterId val chapterId = viewModel.historyInfo.value.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { 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() snackbar.show()
} else { } else {
startActivity( startActivity(
@ -331,7 +359,10 @@ class DetailsActivity :
view.isVisible = isVisible 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) val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
if (viewBinding.layoutBottom?.isVisible == true) { if (viewBinding.layoutBottom?.isVisible == true) {
sb.anchorView = viewBinding.toolbarChapters sb.anchorView = viewBinding.toolbarChapters

@ -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.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.resolveDp 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.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
@ -44,7 +45,12 @@ fun exploreButtonsAD(
if (item.isRandomLoading) { if (item.isRandomLoading) {
val icon = CircularProgressDrawable(context) val icon = CircularProgressDrawable(context)
icon.strokeWidth = context.resources.resolveDp(2f) 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 binding.buttonRandom.icon = icon
icon.start() icon.start()
} else { } else {
@ -88,7 +94,13 @@ fun exploreSourceListItemAD(
listener: OnListItemClickListener<MangaSourceItem>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>(
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent ->
ItemExploreSourceListBinding.inflate(
layoutInflater,
parent,
false
)
},
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
) { ) {
@ -96,6 +108,7 @@ fun exploreSourceListItemAD(
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
binding.root.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
@ -115,7 +128,13 @@ fun exploreSourceGridItemAD(
listener: OnListItemClickListener<MangaSourceItem>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>(
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent ->
ItemExploreSourceGridBinding.inflate(
layoutInflater,
parent,
false
)
},
on = { item, _, _ -> item is MangaSourceItem && item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
) { ) {
@ -123,6 +142,7 @@ fun exploreSourceGridItemAD(
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
binding.root.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable 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.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest 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.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
@ -28,12 +30,13 @@ fun mangaGridItemAD(
) { ) {
var badge: BadgeDrawable? = null var badge: BadgeDrawable? = null
itemView.setOnClickListener { val eventListener = object : View.OnClickListener, View.OnLongClickListener {
clickListener.onItemClick(item.manga, it) override fun onClick(v: View) = clickListener.onItemClick(item.manga, v)
} override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
} }
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
itemView.setOnContextClickListenerCompat(eventListener)
sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView) sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView)
bind { payloads -> bind { payloads ->

@ -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.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest 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.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
@ -45,6 +46,7 @@ fun mangaListDetailedItemAD(
} }
itemView.setOnClickListener(listenerAdapter) itemView.setOnClickListener(listenerAdapter)
itemView.setOnLongClickListener(listenerAdapter) itemView.setOnLongClickListener(listenerAdapter)
itemView.setOnContextClickListenerCompat(listenerAdapter)
binding.buttonRead.setOnClickListener(listenerAdapter) binding.buttonRead.setOnClickListener(listenerAdapter)
binding.chipsTags.onChipClickListener = listenerAdapter binding.chipsTags.onChipClickListener = listenerAdapter

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable 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.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest 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.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.databinding.ItemMangaListBinding
@ -25,12 +27,13 @@ fun mangaListItemAD(
) { ) {
var badge: BadgeDrawable? = null var badge: BadgeDrawable? = null
itemView.setOnClickListener { val eventListener = object : View.OnClickListener, View.OnLongClickListener {
clickListener.onItemClick(item.manga, it) override fun onClick(v: View) = clickListener.onItemClick(item.manga, v)
} override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
} }
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
itemView.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title

@ -376,9 +376,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal) val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal)
viewBinding.appbar.updatePadding(left = padding, right = padding) viewBinding.appbar.updatePadding(left = padding, right = padding)
adjustFabVisibility(isSearchOpened = isOpened) adjustFabVisibility(isSearchOpened = isOpened)
supportActionBar?.setHomeAsUpIndicator( supportActionBar?.apply {
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, 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( viewBinding.searchView.setHintCompat(
if (isOpened) R.string.search_hint else R.string.search_manga, if (isOpened) R.string.search_hint else R.string.search_manga,
) )
@ -394,7 +399,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
this, this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS), arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1 1,
) )
} }
} }

@ -1,7 +1,11 @@
package org.koitharu.kotatsu.reader.ui.pager.reversed package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.os.Bundle import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar 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 org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint @AndroidEntryPoint
class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@ -47,6 +53,7 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
adapter = readerAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@ReversedReaderFragment)
} }
viewModel.pageAnimation.observe(viewLifecycleOwner) { viewModel.pageAnimation.observe(viewLifecycleOwner) {
@ -69,6 +76,20 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
super.onDestroyView() 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( override fun onCreateAdapter() = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
loader = pageLoader, loader = pageLoader,

@ -35,6 +35,7 @@ open class PageHolder(
binding.ssiv.bindToLifecycle(owner) binding.ssiv.bindToLifecycle(owner)
binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice() binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
binding.ssiv.addOnImageEventListener(delegate) binding.ssiv.addOnImageEventListener(delegate)
binding.ssiv.setOnGenericMotionListener(SsivZoomListener())
@Suppress("LeakingThis") @Suppress("LeakingThis")
bindingInfo.buttonRetry.setOnClickListener(this) bindingInfo.buttonRetry.setOnClickListener(this)
@Suppress("LeakingThis") @Suppress("LeakingThis")

@ -1,7 +1,11 @@
package org.koitharu.kotatsu.reader.ui.pager.standard package org.koitharu.kotatsu.reader.ui.pager.standard
import android.os.Bundle import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar 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 org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint @AndroidEntryPoint
class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() { class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@ -39,12 +45,16 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
container: ViewGroup?, container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false) ) = FragmentReaderStandardBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(
binding: FragmentReaderStandardBinding,
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
with(binding.pager) { with(binding.pager) {
adapter = readerAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@PagerReaderFragment)
} }
viewModel.pageAnimation.observe(viewLifecycleOwner) { viewModel.pageAnimation.observe(viewLifecycleOwner) {
@ -67,27 +77,42 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
super.onDestroyView() super.onDestroyView()
} }
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope { override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
val items = async { if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
requireAdapter().setItems(pages) if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
yield() 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) { return false
val position = pages.indexOfFirst { }
it.chapterId == pendingState.chapterId && it.index == pendingState.page
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) =
coroutineScope {
val items = async {
requireAdapter().setItems(pages)
yield()
} }
items.await() if (pendingState != null) {
if (position != -1) { val position = pages.indexOfFirst {
requireViewBinding().pager.setCurrentItem(position, false) it.chapterId == pendingState.chapterId && it.index == pendingState.page
notifyPageChanged(position) }
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 { } else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) items.await()
.show()
} }
} else {
items.await()
} }
}
override fun onCreateAdapter() = PagesAdapter( override fun onCreateAdapter() = PagesAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,

@ -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
}
}

@ -7,6 +7,8 @@ import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector import android.view.GestureDetector
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
@ -16,6 +18,7 @@ import androidx.core.view.GestureDetectorCompat
private const val MAX_SCALE = 2.5f private const val MAX_SCALE = 2.5f
private const val MIN_SCALE = 0.5f private const val MIN_SCALE = 0.5f
private const val WHEEL_SCALE_FACTOR = 0.2f
class WebtoonScalingFrame @JvmOverloads constructor( class WebtoonScalingFrame @JvmOverloads constructor(
context: Context, context: Context,
@ -77,10 +80,26 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return super.dispatchTouchEvent(ev) return super.dispatchTouchEvent(ev)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onGenericMotionEvent(event: MotionEvent): Boolean {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
halfWidth = measuredWidth / 2f if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
halfHeight = measuredHeight / 2f 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() { private fun invalidateTarget() {
@ -161,7 +180,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable { 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 if (scale <= 1f) return false
transformMatrix.postTranslate(-distanceX, -distanceY) transformMatrix.postTranslate(-distanceX, -distanceY)
invalidateTarget() invalidateTarget()
@ -181,7 +205,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true 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 if (scale <= 1) return false
overScroller.fling( overScroller.fling(
@ -200,7 +229,10 @@ class WebtoonScalingFrame @JvmOverloads constructor(
override fun run() { override fun run() {
if (overScroller.computeScrollOffset()) { if (overScroller.computeScrollOffset()) {
transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY) transformMatrix.postTranslate(
overScroller.currX - transX,
overScroller.currY - transY
)
invalidateTarget() invalidateTarget()
postOnAnimation(this) postOnAnimation(this)
} }

@ -26,6 +26,17 @@
<solid android:color="@color/selector_overlay" /> <solid android:color="@color/selector_overlay" />
</shape> </shape>
</item> </item>
<item
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:state_hovered="true"
android:top="2dp">
<shape android:shape="rectangle">
<corners android:radius="@dimen/list_selector_corner" />
<solid android:color="@color/selector_overlay" />
</shape>
</item>
<item <item
android:bottom="2dp" android:bottom="2dp"
android:left="2dp" android:left="2dp"

@ -64,6 +64,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:navigationContentDescription="@string/search"
app:navigationIcon="?attr/actionModeWebSearchDrawable"> app:navigationIcon="?attr/actionModeWebSearchDrawable">
<org.koitharu.kotatsu.search.ui.widget.SearchEditText <org.koitharu.kotatsu.search.ui.widget.SearchEditText

@ -48,6 +48,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:navigationContentDescription="@string/search"
app:navigationIcon="?attr/actionModeWebSearchDrawable"> app:navigationIcon="?attr/actionModeWebSearchDrawable">
<org.koitharu.kotatsu.search.ui.widget.SearchEditText <org.koitharu.kotatsu.search.ui.widget.SearchEditText

@ -5,6 +5,7 @@
android:id="@+id/frame" android:id="@+id/frame"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView <org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -60,6 +60,7 @@
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/abc_ic_menu_overflow_material" android:src="@drawable/abc_ic_menu_overflow_material"
android:tooltipText="@string/more"
app:tint="?colorControlNormal" /> app:tint="?colorControlNormal" />
<ImageView <ImageView
@ -70,7 +71,8 @@
android:contentDescription="@string/add" android:contentDescription="@string/add"
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_add" /> android:src="@drawable/ic_add"
android:tooltipText="@string/add" />
<ImageView <ImageView
android:id="@+id/imageView_remove" android:id="@+id/imageView_remove"
@ -80,6 +82,7 @@
android:contentDescription="@string/remove" android:contentDescription="@string/remove"
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_delete" /> android:src="@drawable/ic_delete"
android:tooltipText="@string/remove" />
</LinearLayout> </LinearLayout>

@ -7,7 +7,8 @@
android:id="@+id/action_app_update" android:id="@+id/action_app_update"
android:icon="@drawable/ic_app_update" android:icon="@drawable/ic_app_update"
android:orderInCategory="1" android:orderInCategory="1"
android:title="@string/update" android:title="@string/app_update_available"
android:titleCondensed="@string/update"
android:visible="false" android:visible="false"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

@ -7,7 +7,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.1.1' classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48' 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'
} }
} }

Loading…
Cancel
Save