Viewer navigation (#3869)

* Viewer navigation

Co-authored-by: Harsh Parekh <h.x.dev@outlook.com>

* Match current reader behavior and add ability to invert it

* A bit of clean up

* Clean up inversion

* Only create navigator when changed

and change tap zone when invertTapping is changed

* Clean up PagerConfig

* Change how Viewer navigation works

* Add Edge Navigation

Co-authored-by: Harsh Parekh <h.x.dev@outlook.com>
This commit is contained in:
Andreas E 2021-01-02 00:41:20 +01:00 committed by GitHub
parent 71ece73d99
commit d69e9034ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 350 additions and 58 deletions

View file

@ -65,6 +65,10 @@ object PreferenceKeys {
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
const val navigationModePager = "reader_navigation_mode_pager"
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"

View file

@ -31,10 +31,10 @@ object PreferenceValues {
LIST,
}
enum class TappingInvertMode {
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
HORIZONTAL(shouldInvertHorizontal = true),
VERTICAL(shouldInvertVertical = true),
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
}
}

View file

@ -138,6 +138,10 @@ class PreferencesHelper(val context: Context) {
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)

View file

@ -86,6 +86,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
binding.webtoonPrefsGroup.isInvisible = true
binding.pagerPrefsGroup.isVisible = true
binding.pagerNav.bindToPreference(preferences.navigationModePager())
binding.scaleType.bindToPreference(preferences.imageScaleType(), 1)
binding.zoomStart.bindToPreference(preferences.zoomStart(), 1)
binding.cropBorders.bindToPreference(preferences.cropBorders())
@ -98,6 +99,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
binding.pagerPrefsGroup.isInvisible = true
binding.webtoonPrefsGroup.isVisible = true
binding.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
}

View file

@ -28,13 +28,18 @@ abstract class ViewerConfig(preferences: PreferencesHelper) {
var volumeKeysInverted = false
var trueColor = false
var alwaysShowChapterTransition = true
var navigationMode = 0
protected set
abstract var navigator: ViewerNavigation
protected set
init {
preferences.readWithTapping()
.register({ tappingEnabled = it })
preferences.readWithTappingInverted()
.register({ tappingInverted = it })
.register({ tappingInverted = it }, { navigator.invertMode = it })
preferences.readWithLongTap()
.register({ longTapEnabled = it })
@ -58,6 +63,10 @@ abstract class ViewerConfig(preferences: PreferencesHelper) {
.register({ alwaysShowChapterTransition = it })
}
protected abstract fun defaultNavigation(): ViewerNavigation
abstract fun updateNavigation(navigationMode: Int)
fun <T> Preference<T>.register(
valueAssignment: (T) -> Unit,
onChanged: (T) -> Unit = {}

View file

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.graphics.PointF
import android.graphics.RectF
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.util.lang.invert
abstract class ViewerNavigation {
enum class NavigationRegion {
NEXT, PREV, MENU
}
data class Region(
val rectF: RectF,
val type: NavigationRegion
) {
fun invert(invertMode: PreferenceValues.TappingInvertMode): Region {
if (invertMode == PreferenceValues.TappingInvertMode.NONE) return this
return this.copy(
rectF = this.rectF.invert(invertMode)
)
}
}
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
abstract var regions: List<Region>
var invertMode: PreferenceValues.TappingInvertMode = PreferenceValues.TappingInvertMode.NONE
fun getAction(pos: PointF): NavigationRegion {
val x = pos.x
val y = pos.y
val region = regions.map { it.invert(invertMode) }
.find { it.rectF.contains(x, y) }
return when {
region != null -> region.type
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
else -> NavigationRegion.MENU
}
}
}

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
/**
* Visualization of default state without any inversion
* +---+---+---+
* | N | N | N | P: Previous
* +---+---+---+
* | N | M | N | M: Menu
* +---+---+---+
* | N | P | N | N: Next
* +---+---+---+
*/
class EdgeNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT
),
Region(
rectF = RectF(0.33f, 0.66f, 0.66f, 1f),
type = NavigationRegion.PREV
),
Region(
rectF = RectF(0.66f, 0f, 1f, 1f),
type = NavigationRegion.NEXT
),
)
}

View file

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
/**
* Visualization of default state without any inversion
* +---+---+---+
* | M | M | M | P: Previous
* +---+---+---+
* | P | N | N | M: Menu
* +---+---+---+
* | P | N | N | N: Next
* +---+---+---+
*/
class KindlishNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
Region(
rectF = RectF(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT
),
Region(
rectF = RectF(0f, 0.33f, 0.33f, 1f),
type = NavigationRegion.PREV
)
)
}

View file

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
/**
* Visualization of default state without any inversion
* +---+---+---+
* | N | N | N | P: Previous
* +---+---+---+
* | N | M | P | M: Menu
* +---+---+---+
* | P | P | P | N: Next
* +---+---+---+
*/
open class LNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
Region(
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.NEXT
),
Region(
rectF = RectF(0f, 0f, 1f, 0.33f),
type = NavigationRegion.NEXT
),
Region(
rectF = RectF(0.66f, 0.33f, 1f, 0.66f),
type = NavigationRegion.PREV
),
Region(
rectF = RectF(0f, 0.66f, 1f, 1f),
type = NavigationRegion.PREV
)
)
}

View file

@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -29,6 +33,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
preferences.cropBorders()
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
preferences.navigationModePager()
.register({ navigationMode = it }, { updateNavigation(navigationMode) })
}
private fun zoomTypeFromPreference(value: Int) {
@ -48,6 +55,28 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
}
}
override var navigator: ViewerNavigation = defaultNavigation()
set(value) {
field = value.also { it.invertMode = this.tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return when (viewer) {
is VerticalPagerViewer -> VerticalPagerDefaultNavigation()
else -> PagerDefaultNavigation()
}
}
override fun updateNavigation(navigationMode: Int) {
navigator = when (navigationMode) {
0 -> defaultNavigation()
1 -> LNavigation()
2 -> KindlishNavigation()
3 -> EdgeNavigation()
else -> defaultNavigation()
}
}
enum class ZoomType {
Left, Center, Right
}

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.graphics.RectF
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
/**
* Visualization of default state without any inversion
* +---+---+---+
* | N | M | P | P: Previous
* +---+---+---+
* | N | M | P | M: Menu
* +---+---+---+
* | N | M | P | N: Next
* +---+---+---+
*/
class PagerDefaultNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT
),
Region(
rectF = RectF(0.66f, 0f, 1f, 1f),
type = NavigationRegion.PREV
),
)
}
class VerticalPagerDefaultNavigation : LNavigation()

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.graphics.PointF
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
@ -9,12 +10,12 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.viewpager.widget.ViewPager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import timber.log.Timber
import kotlin.math.min
@ -89,34 +90,12 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
return@f
}
val positionX = event.x
val positionY = event.y
val topSideTap = positionY < pager.height * 0.25f
val bottomSideTap = positionY > pager.height * 0.75f
val leftSideTap = positionX < pager.width * 0.33f
val rightSideTap = positionX > pager.width * 0.66f
val invertMode = config.tappingInverted
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
if (this is VerticalPagerViewer) {
when {
topSideTap && !invertVertical || bottomSideTap && invertVertical -> moveLeft()
bottomSideTap && !invertVertical || topSideTap && invertVertical -> moveRight()
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
else -> activity.toggleMenu()
}
} else {
when {
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
else -> activity.toggleMenu()
}
val pos = PointF(event.rawX / pager.width, event.rawY / pager.height)
val navigator = config.navigator
when (navigator.getAction(pos)) {
ViewerNavigation.NavigationRegion.MENU -> activity.toggleMenu()
ViewerNavigation.NavigationRegion.NEXT -> moveToNext()
ViewerNavigation.NavigationRegion.PREV -> moveToPrevious()
}
}
pager.longTapListener = f@{

View file

@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -22,5 +26,27 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
preferences.webtoonSidePadding()
.register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
preferences.navigationModeWebtoon()
.register({ navigationMode = it }, { updateNavigation(it) })
}
override var navigator: ViewerNavigation = defaultNavigation()
set(value) {
field = value.also { it.invertMode = tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation {
return WebtoonDefaultNavigation()
}
override fun updateNavigation(navigationMode: Int) {
this.navigator = when (navigationMode) {
0 -> defaultNavigation()
1 -> LNavigation()
2 -> KindlishNavigation()
3 -> EdgeNavigation()
else -> defaultNavigation()
}
}
}

View file

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
class WebtoonDefaultNavigation : LNavigation()

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.graphics.PointF
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
@ -9,12 +10,12 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.WebtoonLayoutManager
import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import kotlin.math.max
@ -101,25 +102,15 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
return@f
}
val positionX = event.rawX
val positionY = event.rawY
val topSideTap = positionY < recycler.height * 0.25f
val bottomSideTap = positionY > recycler.height * 0.75f
val leftSideTap = positionX < recycler.width * 0.33f
val rightSideTap = positionX > recycler.width * 0.66f
val invertMode = config.tappingInverted
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
when {
topSideTap && !invertVertical || bottomSideTap && invertVertical -> scrollUp()
bottomSideTap && !invertVertical || topSideTap && invertVertical -> scrollDown()
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> scrollUp()
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> scrollDown()
else -> activity.toggleMenu()
val pos = PointF(event.rawX / recycler.width, event.rawY / recycler.height)
if (!config.tappingEnabled) activity.toggleMenu()
else {
val navigator = config.navigator
when (navigator.getAction(pos)) {
ViewerNavigation.NavigationRegion.MENU -> activity.toggleMenu()
ViewerNavigation.NavigationRegion.NEXT -> scrollDown()
ViewerNavigation.NavigationRegion.PREV -> scrollUp()
}
}
}
recycler.longTapListener = f@{ event ->

View file

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.util.lang
import android.graphics.RectF
import eu.kanade.tachiyomi.data.preference.PreferenceValues
fun RectF.invert(invertMode: PreferenceValues.TappingInvertMode): RectF {
val horizontal = invertMode.shouldInvertHorizontal
val vertical = invertMode.shouldInvertVertical
return when {
horizontal && vertical -> RectF(1f - this.right, 1f - this.bottom, 1f - this.left, 1f - this.top)
vertical -> RectF(this.left, 1f - this.bottom, this.right, 1f - this.top)
horizontal -> RectF(1f - this.right, this.top, 1f - this.left, this.bottom)
else -> this
}
}

View file

@ -203,6 +203,25 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/end_navigation_preferences" />
<TextView
android:id="@+id/pager_nav_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/pref_viewer_nav"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/verticalcenter"
app:layout_constraintBaseline_toBaselineOf="@id/pager_nav"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/pager_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:entries="@array/pager_nav"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/pager_prefs"
app:layout_constraintStart_toEndOf="@id/verticalcenter"
app:layout_constraintEnd_toEndOf="@id/spinner_end" />
<TextView
android:id="@+id/scale_type_text"
android:layout_width="0dp"
@ -220,7 +239,7 @@
android:entries="@array/image_scale_type"
app:layout_constraintEnd_toEndOf="@id/spinner_end"
app:layout_constraintStart_toEndOf="@id/verticalcenter"
app:layout_constraintTop_toBottomOf="@id/pager_prefs" />
app:layout_constraintTop_toBottomOf="@+id/pager_nav" />
<TextView
android:id="@+id/zoom_start_text"
@ -326,6 +345,26 @@
app:layout_constraintRight_toRightOf="@id/spinner_end"
app:layout_constraintTop_toBottomOf="@id/webtoon_prefs" />
<TextView
android:id="@+id/webtoon_nav_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/pref_viewer_nav"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/verticalcenter"
app:layout_constraintBaseline_toBaselineOf="@id/webtoon_nav"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/webtoon_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:entries="@array/webtoon_nav"
app:layout_constraintEnd_toEndOf="@id/spinner_end"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/verticalcenter"
app:layout_constraintTop_toBottomOf="@+id/webtoon_side_padding" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/crop_borders_webtoon"
android:layout_width="match_parent"
@ -333,7 +372,7 @@
android:layout_marginTop="10dp"
android:text="@string/pref_crop_borders"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/webtoon_side_padding" />
app:layout_constraintTop_toBottomOf="@+id/webtoon_nav" />
<!-- Groups of preferences -->
@ -342,7 +381,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="pager_prefs,scale_type_text,scale_type,zoom_start_text,zoom_start,crop_borders"
app:constraint_referenced_ids="pager_prefs,pager_nav_text,pager_nav,scale_type_text,scale_type,zoom_start_text,zoom_start,crop_borders"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
@ -350,7 +389,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="webtoon_prefs,crop_borders_webtoon,webtoon_side_padding_text,webtoon_side_padding" />
app:constraint_referenced_ids="webtoon_prefs,webtoon_nav_text,webtoon_nav,crop_borders_webtoon,webtoon_side_padding_text,webtoon_side_padding" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/verticalcenter"

View file

@ -91,4 +91,18 @@
<item>@string/manga_from_library</item>
<item>@string/downloaded_chapters</item>
</string-array>
<string-array name="pager_nav">
<item>@string/default_nav</item>
<item>@string/l_nav</item>
<item>@string/kindlish_nav</item>
<item>@string/edge_nav</item>
</string-array>
<string-array name="webtoon_nav">
<item>@string/default_nav</item>
<item>@string/l_nav</item>
<item>@string/kindlish_nav</item>
<item>@string/edge_nav</item>
</string-array>
</resources>

View file

@ -277,12 +277,17 @@
<string name="black_background">Black</string>
<string name="pref_viewer_type">Default reading mode</string>
<string name="default_viewer">Default</string>
<string name="default_nav">Default</string>
<string name="l_nav">L shaped</string>
<string name="kindlish_nav">Kindle-ish</string>
<string name="edge_nav">Edge</string>
<string name="left_to_right_viewer">Left to right</string>
<string name="right_to_left_viewer">Right to left</string>
<string name="vertical_viewer">Vertical</string>
<string name="webtoon_viewer">Webtoon</string>
<string name="vertical_plus_viewer">Continuous vertical</string>
<string name="pager_viewer">Paged</string>
<string name="pref_viewer_nav">Navigation layout</string>
<string name="pref_image_decoder">Image decoder</string>
<string name="pref_image_scale_type">Scale type</string>
<string name="scale_type_fit_screen">Fit screen</string>