From 58d1c3de268ead51a5ff3b8b52431349bb24bee0 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 7 Feb 2024 11:52:42 +0200 Subject: [PATCH] Default webtoon zoom out option --- .../kotatsu/core/prefs/AppSettings.kt | 5 +++ .../kotatsu/reader/ui/ReaderViewModel.kt | 14 ++++++++ .../ui/pager/webtoon/WebtoonReaderFragment.kt | 4 +++ .../ui/pager/webtoon/WebtoonRecyclerView.kt | 32 +++++++++++++++++++ .../ui/pager/webtoon/WebtoonScalingFrame.kt | 30 +++++++++++++++-- .../settings/AppearanceSettingsFragment.kt | 10 ++---- .../settings/ReaderSettingsFragment.kt | 3 ++ .../settings/utils/PercentSummaryProvider.kt | 16 ++++++++++ .../settings/utils/SliderPreference.kt | 11 ++++--- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/pref_reader.xml | 10 ++++++ 12 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PercentSummaryProvider.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index b1b32b72c..77d2f9847 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index be673d4af..a16117e48 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -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 }, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index 4531cb8b9..57db86ab8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -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() viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) { binding.frame.isZoomEnable = it } + viewModel.defaultWebtoonZoomOut.take(1).observe(viewLifecycleOwner) { + binding.frame.zoom = 1f - it + } } override fun onDestroyView() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt index f4261786b..fb2f09f56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt @@ -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() private val scrollDispatcher = WebtoonScrollDispatcher() private val detachedViews = Collections.newSetFromMap(WeakHashMap()) + 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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt index c57858ee7..067ae9933 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt @@ -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( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt index 02c1c0c2d..25375fae8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -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(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(AppSettings.KEY_GRID_SIZE)?.summaryProvider = PercentSummaryProvider() findPreference(AppSettings.KEY_LIST_MODE)?.run { entryValues = ListMode.entries.names() setDefaultValueCompat(ListMode.GRID.name) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt index 411840007..be4e8be6c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt @@ -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(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider() updateReaderModeDependency() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PercentSummaryProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PercentSummaryProvider.kt new file mode 100644 index 000000000..66e05e261 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PercentSummaryProvider.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.settings.utils + +import androidx.preference.Preference +import org.koitharu.kotatsu.R + +class PercentSummaryProvider : Preference.SummaryProvider { + + 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()) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt index fbf2fcc0c..28fbf3aca 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt @@ -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) } } } diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 580091d0e..acb494e84 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef189c80d..1ac17ed39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -585,4 +585,5 @@ None Reset settings to default values? This action cannot be undone. Use two pages layout on landscape orientation (beta) - \ No newline at end of file + Default webtoon zoom out + diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 066d87d41..e74a6d3d6 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -28,6 +28,16 @@ android:summary="@string/webtoon_zoom_summary" android:title="@string/webtoon_zoom" /> + +