Default webtoon zoom out option

master
Koitharu 2 years ago
parent ba2ed6a2ef
commit 58d1c3de26
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -361,6 +361,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isWebtoonZoomEnable: Boolean val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) 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) @get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float var readerAutoscrollSpeed: Float
get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f) 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_HISTORY_ORDER = "history_order"
const val KEY_FAVORITES_ORDER = "fav_order" const val KEY_FAVORITES_ORDER = "fav_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom" 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_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale" const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging" const val KEY_LOGGING_ENABLED = "logging"

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

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

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.core.view.ViewCompat.TYPE_TOUCH import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.iterator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.Collections import java.util.Collections
@ -18,6 +19,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>() private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>()
private val scrollDispatcher = WebtoonScrollDispatcher() private val scrollDispatcher = WebtoonScrollDispatcher()
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>()) private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
private var isFixingScroll = false
override fun onChildDetachedFromWindow(child: View) { override fun onChildDetachedFromWindow(child: View) {
super.onChildDetachedFromWindow(child) 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 class WebtoonScrollDispatcher {
private var firstPos = NO_POSITION private var firstPos = NO_POSITION

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

@ -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.names
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import javax.inject.Inject import javax.inject.Inject
@ -38,14 +39,7 @@ class AppearanceSettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_appearance) addPreferencesFromResource(R.xml.pref_appearance)
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run { findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.summaryProvider = PercentSummaryProvider()
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<ListPreference>(AppSettings.KEY_LIST_MODE)?.run { findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
entryValues = ListMode.entries.names() entryValues = ListMode.entries.names()
setDefaultValueCompat(ListMode.GRID.name) setDefaultValueCompat(ListMode.GRID.name)

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

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

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

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

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

@ -28,6 +28,16 @@
android:summary="@string/webtoon_zoom_summary" android:summary="@string/webtoon_zoom_summary"
android:title="@string/webtoon_zoom" /> 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 <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="reader_zoom_buttons" android:key="reader_zoom_buttons"

Loading…
Cancel
Save