Default webtoon zoom out option
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user