Vertical reader mode
This commit is contained in:
@@ -4,7 +4,9 @@ enum class ReaderMode(val id: Int) {
|
||||
|
||||
STANDARD(1),
|
||||
REVERSED(3),
|
||||
WEBTOON(2);
|
||||
VERTICAL(4),
|
||||
WEBTOON(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.vertical.VerticalReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
||||
import java.util.EnumMap
|
||||
|
||||
@@ -21,6 +22,7 @@ class ReaderManager(
|
||||
modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
|
||||
modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java
|
||||
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
|
||||
modeMap[ReaderMode.VERTICAL] = VerticalReaderFragment::class.java
|
||||
}
|
||||
|
||||
val currentReader: BaseReaderFragment<*>?
|
||||
|
||||
@@ -79,6 +79,7 @@ class ReaderConfigSheet :
|
||||
binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD
|
||||
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
|
||||
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
|
||||
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
||||
|
||||
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||
binding.buttonSavePage.setOnClickListener(this)
|
||||
@@ -153,6 +154,7 @@ class ReaderConfigSheet :
|
||||
R.id.button_standard -> ReaderMode.STANDARD
|
||||
R.id.button_webtoon -> ReaderMode.WEBTOON
|
||||
R.id.button_reversed -> ReaderMode.REVERSED
|
||||
R.id.button_vertical -> ReaderMode.VERTICAL
|
||||
else -> return
|
||||
}
|
||||
if (newMode == mode) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.vertical
|
||||
|
||||
import android.view.View
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
|
||||
class VerticalPageAnimTransformer : ViewPager2.PageTransformer {
|
||||
|
||||
override fun transformPage(page: View, position: Float) = with(page) {
|
||||
translationY = -position * height
|
||||
pivotX = width / 2f
|
||||
pivotY = height * 0.2f
|
||||
cameraDistance = 20000f
|
||||
when {
|
||||
position < -1f || position > 1f -> {
|
||||
alpha = 0f
|
||||
rotationX = 0f
|
||||
translationZ = -1f
|
||||
}
|
||||
position > 0f -> {
|
||||
alpha = 1f
|
||||
rotationX = 0f
|
||||
translationZ = 0f
|
||||
}
|
||||
position <= 0f -> {
|
||||
alpha = 1f
|
||||
rotationX = -120 * position
|
||||
translationZ = 2f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.vertical
|
||||
|
||||
import android.os.Build
|
||||
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 androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
|
||||
import org.koitharu.kotatsu.core.ui.list.lifecycle.PagerLifecycleDispatcher
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.findCurrentViewHolder
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagesAdapter
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
@AndroidEntryPoint
|
||||
class VerticalReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
|
||||
View.OnGenericMotionListener {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
|
||||
@Inject
|
||||
lateinit var pageLoader: PageLoader
|
||||
|
||||
private var pagerLifecycleDispatcher: PagerLifecycleDispatcher? = null
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(
|
||||
binding: FragmentReaderStandardBinding,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
with(binding.pager) {
|
||||
orientation = ViewPager2.ORIENTATION_VERTICAL
|
||||
adapter = readerAdapter
|
||||
offscreenPageLimit = 2
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
setOnGenericMotionListener(this@VerticalReaderFragment)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
recyclerView?.defaultFocusHighlightEnabled = false
|
||||
}
|
||||
PagerEventSupplier(this).attach()
|
||||
pagerLifecycleDispatcher = PagerLifecycleDispatcher(this).also {
|
||||
registerOnPageChangeCallback(it)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
||||
val transformer = when (it) {
|
||||
ReaderAnimation.NONE -> NoAnimPageTransformer()
|
||||
ReaderAnimation.DEFAULT -> null
|
||||
ReaderAnimation.ADVANCED -> VerticalPageAnimTransformer()
|
||||
}
|
||||
binding.pager.setPageTransformer(transformer)
|
||||
if (transformer == null) {
|
||||
binding.pager.recyclerView?.children?.forEach { view ->
|
||||
view.resetTransformations()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
pagerLifecycleDispatcher = null
|
||||
requireViewBinding().pager.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomIn()
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomOut()
|
||||
}
|
||||
|
||||
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 suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) =
|
||||
coroutineScope {
|
||||
val items = launch {
|
||||
requireAdapter().setItems(pages)
|
||||
yield()
|
||||
pagerLifecycleDispatcher?.invalidate()
|
||||
}
|
||||
if (pendingState != null) {
|
||||
val position = pages.indexOfFirst {
|
||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||
}
|
||||
items.join()
|
||||
if (position != -1) {
|
||||
requireViewBinding().pager.setCurrentItem(position, false)
|
||||
notifyPageChanged(position)
|
||||
} else {
|
||||
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
items.join()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAdapter() = PagesAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
with(requireViewBinding().pager) {
|
||||
setCurrentItem(currentItem + delta, isAnimationEnabled())
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||
with(requireViewBinding().pager) {
|
||||
setCurrentItem(
|
||||
position,
|
||||
smooth && isAnimationEnabled() && (currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentState(): ReaderState? = viewBinding?.run {
|
||||
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
||||
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
||||
ReaderState(
|
||||
chapterId = page.chapterId,
|
||||
page = page.index,
|
||||
scroll = 0,
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val SMOOTH_SCROLL_LIMIT = 3
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/drawable/ic_reader_vertical.xml
Normal file
12
app/src/main/res/drawable/ic_reader_vertical.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M18 2H6C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V4C20 2.9 19.11 2 18 2M18 20H6V16H18V20M18 8H6V4H18V8Z" />
|
||||
</vector>
|
||||
@@ -101,6 +101,15 @@
|
||||
android:text="@string/right_to_left"
|
||||
app:icon="@drawable/ic_reader_rtl" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_vertical"
|
||||
style="@style/Widget.Kotatsu.ToggleButton.Vertical"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/vertical"
|
||||
app:icon="@drawable/ic_reader_vertical" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_webtoon"
|
||||
style="@style/Widget.Kotatsu.ToggleButton.Vertical"
|
||||
|
||||
@@ -565,4 +565,5 @@
|
||||
<string name="volume_">Volume %d</string>
|
||||
<string name="volume_unknown">Unknown volume</string>
|
||||
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
|
||||
<string name="vertical">Vertical</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user