Added dual page split setting (#4252)

* Add DualPageSplit option

* remove extra line

* Split double-page into two pages

* Remove !isAnimated check and add (ALPHA) to the label

* Fix missing insert pages

* Pager cleanup

* Add dual split to Webtoon and fix Vertical

* Fix L2R/R2L

* Add comments and refactor code in ImageUtil

* Use a simpler split solution in webtoon mode

Co-authored-by: weng <>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
This commit is contained in:
vance 2021-02-10 06:54:44 +08:00 committed by GitHub
parent aa67229daf
commit b5017eebbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 190 additions and 4 deletions

View File

@ -23,6 +23,8 @@ object PreferenceKeys {
const val showPageNumber = "pref_show_page_number_key" const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplit = "pref_dual_page_split"
const val showReadingMode = "pref_show_reading_mode" const val showReadingMode = "pref_show_reading_mode"
const val trueColor = "pref_true_color_key" const val trueColor = "pref_true_color_key"

View File

@ -89,6 +89,8 @@ class PreferencesHelper(val context: Context) {
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true) fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true) fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false) fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)

View File

@ -65,6 +65,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BaseBottomShee
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values) binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
binding.showPageNumber.bindToPreference(preferences.showPageNumber()) binding.showPageNumber.bindToPreference(preferences.showPageNumber())
binding.fullscreen.bindToPreference(preferences.fullscreen()) binding.fullscreen.bindToPreference(preferences.fullscreen())
binding.dualPageSplit.bindToPreference(preferences.dualPageSplit())
binding.keepscreen.bindToPreference(preferences.keepScreenOn()) binding.keepscreen.bindToPreference(preferences.keepScreenOn())
binding.longTap.bindToPreference(preferences.readWithLongTap()) binding.longTap.bindToPreference(preferences.readWithLongTap())
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition()) binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.ui.reader.model
class InsertPage(val parent: ReaderPage) : ReaderPage(parent.index, parent.url, parent.imageUrl) {
override var chapter: ReaderChapter = parent.chapter
init {
stream = parent.stream
}
}

View File

@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.model
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import java.io.InputStream import java.io.InputStream
class ReaderPage( open class ReaderPage(
index: Int, index: Int,
url: String = "", url: String = "",
imageUrl: String? = null, imageUrl: String? = null,
var stream: (() -> InputStream)? = null var stream: (() -> InputStream)? = null
) : Page(index, url, imageUrl, null) { ) : Page(index, url, imageUrl, null) {
lateinit var chapter: ReaderChapter open lateinit var chapter: ReaderChapter
} }

View File

@ -24,6 +24,7 @@ abstract class ViewerConfig(preferences: PreferencesHelper, private val scope: C
var volumeKeysInverted = false var volumeKeysInverted = false
var trueColor = false var trueColor = false
var alwaysShowChapterTransition = true var alwaysShowChapterTransition = true
var dualPageSplit = false
var navigationMode = 0 var navigationMode = 0
protected set protected set
@ -54,6 +55,9 @@ abstract class ViewerConfig(preferences: PreferencesHelper, private val scope: C
preferences.alwaysShowChapterTransition() preferences.alwaysShowChapterTransition()
.register({ alwaysShowChapterTransition = it }) .register({ alwaysShowChapterTransition = it })
preferences.dualPageSplit()
.register({ dualPageSplit = it }, { imagePropertyChangedListener?.invoke() })
} }
protected abstract fun defaultNavigation(): ViewerNavigation protected abstract fun defaultNavigation(): ViewerNavigation

View File

@ -28,6 +28,7 @@ import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
@ -241,6 +242,9 @@ class PagerPageHolder(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
openStream = processDualPageSplit(openStream!!)
}
if (!isAnimated) { if (!isAnimated) {
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
} else { } else {
@ -253,6 +257,38 @@ class PagerPageHolder(
.subscribe({}, {}) .subscribe({}, {})
} }
private fun processDualPageSplit(openStream: InputStream): InputStream {
var inputStream = openStream
val (isDoublePage, stream) = when (page) {
is InsertPage -> Pair(true, inputStream)
else -> ImageUtil.isDoublePage(inputStream)
}
inputStream = stream
if (isDoublePage) {
val side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
viewer is R2LPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT
viewer is R2LPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
viewer is VerticalPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
viewer is VerticalPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
else -> error("We should choose a side!")
}
if (page !is InsertPage) {
onPageSplit()
}
inputStream = ImageUtil.splitInHalf(inputStream, side)
}
return inputStream
}
private fun onPageSplit() {
val newPage = InsertPage(page)
viewer.onPageSplit(page, newPage)
}
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */

View File

@ -12,6 +12,7 @@ import androidx.viewpager.widget.ViewPager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
@ -371,4 +372,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
} }
return false return false
} }
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
adapter.onPageSplit(currentPage, newPage, this::class.java)
}
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
@ -18,7 +19,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/** /**
* List of currently set items. * List of currently set items.
*/ */
var items: List<Any> = emptyList() var items: MutableList<Any> = mutableListOf()
private set private set
var nextTransition: ChapterTransition.Next? = null var nextTransition: ChapterTransition.Next? = null
@ -80,6 +81,9 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
} }
} }
// Resets double-page splits, else insert pages get misplaced
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
if (viewer is R2LPagerViewer) { if (viewer is R2LPagerViewer) {
newItems.reverse() newItems.reverse()
} }
@ -120,4 +124,31 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
} }
return POSITION_NONE return POSITION_NONE
} }
fun onPageSplit(current: Any?, newPage: InsertPage, clazz: Class<out PagerViewer>) {
if (current !is ReaderPage) return
val currentIndex = items.indexOf(current)
val placeAtIndex = when {
clazz.isAssignableFrom(L2RPagerViewer::class.java) -> currentIndex + 1
clazz.isAssignableFrom(VerticalPagerViewer::class.java) -> currentIndex + 1
clazz.isAssignableFrom(R2LPagerViewer::class.java) -> currentIndex
else -> currentIndex
}
// It will enter a endless cycle of insert pages
if (clazz.isAssignableFrom(R2LPagerViewer::class.java) && items[placeAtIndex - 1] is InsertPage) {
return
}
// Same here it will enter a endless cycle of insert pages
if (items[placeAtIndex] is InsertPage) {
return
}
items.add(placeAtIndex, newPage)
notifyDataSetChanged()
}
} }

View File

@ -287,6 +287,14 @@ class WebtoonPageHolder(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (viewer.config.dualPageSplit) {
val (isDoublePage, stream) = ImageUtil.isDoublePage(openStream!!)
openStream = if (!isDoublePage) {
stream
} else {
ImageUtil.splitAndMerge(stream)
}
}
if (!isAnimated) { if (!isAnimated) {
val subsamplingView = initSubsamplingImageView() val subsamplingView = initSubsamplingImageView()
subsamplingView.isVisible = true subsamplingView.isVisible = true

View File

@ -50,6 +50,11 @@ class SettingsReaderController : SettingsController() {
summaryRes = R.string.pref_show_reading_mode_summary summaryRes = R.string.pref_show_reading_mode_summary
defaultValue = true defaultValue = true
} }
switchPreference {
key = Keys.dualPageSplit
titleRes = R.string.pref_dual_page_split
defaultValue = false
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
switchPreference { switchPreference {
key = Keys.trueColor key = Keys.trueColor

View File

@ -1,5 +1,11 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.net.URLConnection import java.net.URLConnection
@ -68,4 +74,71 @@ object ImageUtil {
GIF("image/gif", "gif"), GIF("image/gif", "gif"),
WEBP("image/webp", "webp") WEBP("image/webp", "webp")
} }
/**
* Check whether the image is a double image (width > height), return the result and original stream
*/
fun isDoublePage(imageStream: InputStream): Pair<Boolean, InputStream> {
val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
return Pair(options.outWidth > options.outHeight, ByteArrayInputStream(imageBytes))
}
/**
* Extract the 'side' part from imageStream and return it as InputStream.
*/
fun splitInHalf(imageStream: InputStream, side: Side): InputStream {
val imageBytes = imageStream.readBytes()
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val height = imageBitmap.height
val width = imageBitmap.width
val singlePage = Rect(0, 0, width / 2, height)
val half = Bitmap.createBitmap(width / 2, height, Bitmap.Config.ARGB_8888)
val part = when (side) {
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
Side.LEFT -> Rect(0, 0, width / 2, height)
}
val canvas = Canvas(half)
canvas.drawBitmap(imageBitmap, part, singlePage, null)
val output = ByteArrayOutputStream()
half.compress(Bitmap.CompressFormat.JPEG, 100, output)
return ByteArrayInputStream(output.toByteArray())
}
/**
* Split the image into left and right parts, then merge them into a new image.
*/
fun splitAndMerge(imageStream: InputStream): InputStream {
val imageBytes = imageStream.readBytes()
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val height = imageBitmap.height
val width = imageBitmap.width
val result = Bitmap.createBitmap(width / 2, height * 2, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
// right -> upper
val rightPart = Rect(width - width / 2, 0, width, height)
val upperPart = Rect(0, 0, width / 2, height)
canvas.drawBitmap(imageBitmap, rightPart, upperPart, null)
// left -> bottom
val leftPart = Rect(0, 0, width / 2, height)
val bottomPart = Rect(0, height, width / 2, height * 2)
canvas.drawBitmap(imageBitmap, leftPart, bottomPart, null)
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
return ByteArrayInputStream(output.toByteArray())
}
enum class Side {
RIGHT, LEFT
}
} }

View File

@ -151,6 +151,14 @@
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/show_page_number" /> app:layout_constraintTop_toBottomOf="@id/show_page_number" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/dual_page_split"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_dual_page_split"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/fullscreen" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/cutout_short" android:id="@+id/cutout_short"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -158,7 +166,7 @@
android:text="@string/pref_cutout_short" android:text="@string/pref_cutout_short"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/fullscreen" app:layout_constraintTop_toBottomOf="@id/dual_page_split"
tools:visibility="visible" /> tools:visibility="visible" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial

View File

@ -250,6 +250,7 @@
<!-- Reader section --> <!-- Reader section -->
<string name="pref_fullscreen">Fullscreen</string> <string name="pref_fullscreen">Fullscreen</string>
<string name="pref_dual_page_split">Dual page split (ALPHA)</string>
<string name="pref_cutout_short">Show content in cutout area</string> <string name="pref_cutout_short">Show content in cutout area</string>
<string name="pref_lock_orientation">Lock orientation</string> <string name="pref_lock_orientation">Lock orientation</string>
<string name="pref_page_transitions">Animate page transitions</string> <string name="pref_page_transitions">Animate page transitions</string>