Default webtoon zoom out option

This commit is contained in:
Koitharu
2024-02-07 11:52:42 +02:00
parent ba2ed6a2ef
commit 58d1c3de26
12 changed files with 122 additions and 17 deletions

View File

@@ -361,6 +361,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@get:FloatRange(from = 0.0, to = 0.5)
val defaultWebtoonZoomOut: Float
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
@get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float
get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f)
@@ -538,6 +542,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_HISTORY_ORDER = "history_order"
const val KEY_FAVORITES_ORDER = "fav_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -128,6 +129,14 @@ class ReaderViewModel @Inject constructor(
val isWebtoonZooEnabled = observeIsWebtoonZoomEnabled()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
if (it) {
observeWebtoonZoomOut()
} else {
flowOf(0f)
}
}.flowOn(Dispatchers.Default)
val isZoomControlsEnabled = getObserveIsZoomControlEnabled().flatMapLatest { zoom ->
if (zoom) {
combine(readerMode, isWebtoonZooEnabled) { mode, ze -> ze || mode != ReaderMode.WEBTOON }
@@ -438,6 +447,11 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isWebtoonZoomEnable },
)
private fun observeWebtoonZoomOut() = settings.observeAsFlow(
key = AppSettings.KEY_WEBTOON_ZOOM_OUT,
valueProducer = { defaultWebtoonZoomOut },
)
private fun getObserveIsZoomControlEnabled() = settings.observeAsFlow(
key = AppSettings.KEY_READER_ZOOM_BUTTONS,
valueProducer = { isReaderZoomButtonsEnabled },

View File

@@ -7,6 +7,7 @@ import android.view.animation.DecelerateInterpolator
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
@@ -55,6 +56,9 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
binding.frame.isZoomEnable = it
}
viewModel.defaultWebtoonZoomOut.take(1).observe(viewLifecycleOwner) {
binding.frame.zoom = 1f - it
}
}
override fun onDestroyView() {

View File

@@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.View
import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach
import androidx.core.view.iterator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections
@@ -18,6 +19,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>()
private val scrollDispatcher = WebtoonScrollDispatcher()
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
private var isFixingScroll = false
override fun onChildDetachedFromWindow(child: View) {
super.onChildDetachedFromWindow(child)
@@ -121,6 +123,36 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
}
fun updateChildrenScroll() {
if (isFixingScroll) {
return
}
isFixingScroll = true
for (child in this) {
val ssiv = (child as WebtoonFrameLayout).target
if (adjustScroll(child, ssiv)) {
break
}
}
isFixingScroll = false
}
private fun adjustScroll(child: View, ssiv: WebtoonImageView): Boolean = when {
child.bottom < height && ssiv.getScroll() < ssiv.getScrollRange() -> {
val distance = minOf(height - child.bottom, ssiv.getScrollRange() - ssiv.getScroll())
ssiv.scrollBy(distance)
true
}
child.top > 0 && ssiv.getScroll() > 0 -> {
val distance = minOf(child.top, ssiv.getScroll())
ssiv.scrollBy(-distance)
true
}
else -> false
}
private class WebtoonScrollDispatcher {
private var firstPos = NO_POSITION

View File

@@ -16,6 +16,7 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.OverScroller
import androidx.core.animation.doOnEnd
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewConfigurationCompat
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
@@ -32,8 +33,6 @@ class WebtoonScalingFrame @JvmOverloads constructor(
ScaleGestureDetector.OnScaleGestureListener,
ZoomControl.ZoomControlListener {
private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) as WebtoonRecyclerView }
private val scaleDetector = ScaleGestureDetector(context, this)
private val gestureDetector = GestureDetectorCompat(context, GestureListener())
private val overScroller = OverScroller(context, AccelerateDecelerateInterpolator())
@@ -59,6 +58,15 @@ class WebtoonScalingFrame @JvmOverloads constructor(
}
}
var zoom: Float
get() = scale
set(value) {
if (value != scale) {
scaleChild(value, halfWidth, halfHeight)
onPostScale(invalidateLayout = true)
}
}
init {
syncMatrixValues()
}
@@ -163,6 +171,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
}
private fun invalidateTarget() {
val targetChild = findTargetChild()
adjustBounds()
targetChild.run {
scaleX = scale
@@ -239,7 +248,19 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true
}
override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
override fun onScaleEnd(p0: ScaleGestureDetector) {
onPostScale(invalidateLayout = false)
}
private fun onPostScale(invalidateLayout: Boolean) {
val target = findTargetChild()
target.post {
target.updateChildrenScroll()
if (invalidateLayout) {
target.requestLayout()
}
}
}
private fun smoothScaleTo(target: Float) {
val newScale = target.coerceIn(MIN_SCALE, MAX_SCALE)
@@ -248,10 +269,13 @@ class WebtoonScalingFrame @JvmOverloads constructor(
setDuration(context.getAnimationDuration(android.R.integer.config_shortAnimTime))
interpolator = DecelerateInterpolator()
addUpdateListener { scaleChild(it.animatedValue as Float, halfWidth, halfHeight) }
doOnEnd { onPostScale(invalidateLayout = false) }
start()
}
}
private fun findTargetChild() = getChildAt(0) as WebtoonRecyclerView
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
override fun onScroll(

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference
import javax.inject.Inject
@@ -38,14 +39,7 @@ class AppearanceSettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_appearance)
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run {
val pattern = context.getString(R.string.percent_string_pattern)
summary = pattern.format(value.toString())
setOnPreferenceChangeListener { preference, newValue ->
preference.summary = pattern.format(newValue.toString())
true
}
}
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.summaryProvider = PercentSummaryProvider()
findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
entryValues = ListMode.entries.names()
setDefaultValueCompat(ListMode.GRID.name)

View File

@@ -17,6 +17,8 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference
@AndroidEntryPoint
class ReaderSettingsFragment :
@@ -46,6 +48,7 @@ class ReaderSettingsFragment :
entryValues = ZoomMode.entries.names()
setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
}
findPreference<SliderPreference>(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider()
updateReaderModeDependency()
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.settings.utils
import androidx.preference.Preference
import org.koitharu.kotatsu.R
class PercentSummaryProvider : Preference.SummaryProvider<SliderPreference> {
private var percentPattern: String? = null
override fun provideSummary(preference: SliderPreference): CharSequence? {
val pattern = percentPattern ?: preference.context.getString(R.string.percent_string_pattern).also {
percentPattern = it
}
return pattern.format(preference.value.toString())
}
}

View File

@@ -24,6 +24,7 @@ class SliderPreference @JvmOverloads constructor(
private var valueTo: Int = 100
private var stepSize: Int = 1
private var currentValue: Int = 0
private var isTickVisible: Boolean = false
var value: Int
get() = currentValue
@@ -46,10 +47,9 @@ class SliderPreference @JvmOverloads constructor(
R.styleable.SliderPreference_android_valueFrom,
valueFrom.toFloat(),
).toInt()
valueTo =
getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt()
stepSize =
getFloat(R.styleable.SliderPreference_android_stepSize, stepSize.toFloat()).toInt()
valueTo = getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt()
stepSize = getFloat(R.styleable.SliderPreference_android_stepSize, stepSize.toFloat()).toInt()
isTickVisible = getBoolean(R.styleable.SliderPreference_tickVisible, isTickVisible)
}
}
@@ -61,6 +61,7 @@ class SliderPreference @JvmOverloads constructor(
slider.valueFrom = valueFrom.toFloat()
slider.valueTo = valueTo.toFloat()
slider.stepSize = stepSize.toFloat()
slider.isTickVisible = isTickVisible
slider.setValueRounded(currentValue.toFloat())
slider.isEnabled = isEnabled
}
@@ -112,7 +113,7 @@ class SliderPreference @JvmOverloads constructor(
private fun syncValueInternal(sliderValue: Int) {
if (sliderValue != currentValue) {
if (callChangeListener(sliderValue)) {
setValueInternal(sliderValue, notifyChanged = false)
setValueInternal(sliderValue, notifyChanged = true)
}
}
}

View File

@@ -23,6 +23,7 @@
<attr name="android:valueFrom" />
<attr name="android:valueTo" />
<attr name="android:stepSize" />
<attr name="tickVisible" />
</declare-styleable>
<declare-styleable name="ListItemTextView">

View File

@@ -585,4 +585,5 @@
<string name="none">None</string>
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string>
<string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string>
</resources>
<string name="default_webtoon_zoom_out">Default webtoon zoom out</string>
</resources>

View File

@@ -28,6 +28,16 @@
android:summary="@string/webtoon_zoom_summary"
android:title="@string/webtoon_zoom" />
<org.koitharu.kotatsu.settings.utils.SliderPreference
android:dependency="webtoon_zoom"
android:key="webtoon_zoom_out"
android:stepSize="10"
android:title="@string/default_webtoon_zoom_out"
android:valueFrom="0"
android:valueTo="50"
app:defaultValue="0"
app:tickVisible="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_zoom_buttons"