Webtoon reader fixes
This commit is contained in:
@@ -12,9 +12,11 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
|||||||
@AttrRes defStyleAttr: Int = 0,
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
|
private var _target: WebtoonImageView? = null
|
||||||
findViewById(R.id.ssiv)
|
val target: WebtoonImageView
|
||||||
}
|
get() = _target ?: findViewById<WebtoonImageView?>(R.id.ssiv).also {
|
||||||
|
_target = it
|
||||||
|
}
|
||||||
|
|
||||||
fun dispatchVerticalScroll(dy: Int): Int {
|
fun dispatchVerticalScroll(dy: Int): Int {
|
||||||
if (dy == 0) {
|
if (dy == 0) {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.core.view.ancestors
|
import androidx.core.view.ancestors
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
private const val SCROLL_UNKNOWN = -1
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class WebtoonImageView @JvmOverloads constructor(
|
class WebtoonImageView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -18,7 +20,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
private val ct = PointF()
|
private val ct = PointF()
|
||||||
|
|
||||||
private var scrollPos = 0
|
private var scrollPos = 0
|
||||||
private var scrollRange = SCROLL_UNKNOWN
|
private var debugPaint: Paint? = null
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
drawDebug(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun scrollBy(delta: Int) {
|
fun scrollBy(delta: Int) {
|
||||||
val maxScroll = getScrollRange()
|
val maxScroll = getScrollRange()
|
||||||
@@ -41,14 +50,14 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
fun getScroll() = scrollPos
|
fun getScroll() = scrollPos
|
||||||
|
|
||||||
fun getScrollRange(): Int {
|
fun getScrollRange(): Int {
|
||||||
if (scrollRange == SCROLL_UNKNOWN) {
|
if (!isReady) {
|
||||||
computeScrollRange()
|
return 0
|
||||||
}
|
}
|
||||||
return scrollRange.coerceAtLeast(0)
|
val totalHeight = (sHeight * width / sWidth.toFloat()).roundToInt()
|
||||||
|
return (totalHeight - height).coerceAtLeast(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recycle() {
|
override fun recycle() {
|
||||||
scrollRange = SCROLL_UNKNOWN
|
|
||||||
scrollPos = 0
|
scrollPos = 0
|
||||||
super.recycle()
|
super.recycle()
|
||||||
}
|
}
|
||||||
@@ -91,8 +100,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
override fun onDownsamplingChanged() {
|
override fun onDownsamplingChanged() {
|
||||||
super.onDownsamplingChanged()
|
super.onDownsamplingChanged()
|
||||||
adjustScale()
|
adjustScale()
|
||||||
computeScrollRange()
|
|
||||||
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReady() {
|
override fun onReady() {
|
||||||
@@ -102,13 +109,10 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
super.onSizeChanged(w, h, oldw, oldh)
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
|
if (oldh != h && oldw != 0 && oldh != 0 && isReady) {
|
||||||
|
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
|
||||||
computeScrollRange()
|
} else {
|
||||||
val container = ancestors.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
|
return
|
||||||
val parentHeight = parentHeight()
|
|
||||||
if (scrollPos != 0 && container.bottom < parentHeight) {
|
|
||||||
scrollTo(scrollRange)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +124,6 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
setScaleAndCenter(minScale, ct)
|
setScaleAndCenter(minScale, ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeScrollRange() {
|
|
||||||
if (!isReady) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val totalHeight = (sHeight * minScale).toIntUp()
|
|
||||||
scrollRange = (totalHeight - height).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustScale() {
|
private fun adjustScale() {
|
||||||
minScale = width / sWidth.toFloat()
|
minScale = width / sWidth.toFloat()
|
||||||
maxScale = minScale
|
maxScale = minScale
|
||||||
@@ -137,4 +133,18 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
private fun parentHeight(): Int {
|
private fun parentHeight(): Int {
|
||||||
return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun drawDebug(canvas: Canvas) {
|
||||||
|
val paint = debugPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = android.graphics.Color.RED
|
||||||
|
strokeWidth = context.resources.resolveDp(2f)
|
||||||
|
textAlign = android.graphics.Paint.Align.LEFT
|
||||||
|
textSize = context.resources.resolveDp(14f)
|
||||||
|
debugPaint = this
|
||||||
|
}
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
canvas.drawRect(1f, 1f, width.toFloat() - 1f, height.toFloat() - 1f, paint)
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
canvas.drawText("${getScroll()} / ${getScrollRange()}", 100f, 100f, paint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
|
||||||
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.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.WeakHashMap
|
import java.util.WeakHashMap
|
||||||
@@ -18,6 +17,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
||||||
private val detachedViews = WeakHashMap<View, Unit>()
|
private val detachedViews = WeakHashMap<View, Unit>()
|
||||||
|
private var isFixingScroll: Boolean = false
|
||||||
|
|
||||||
override fun onChildDetachedFromWindow(child: View) {
|
override fun onChildDetachedFromWindow(child: View) {
|
||||||
super.onChildDetachedFromWindow(child)
|
super.onChildDetachedFromWindow(child)
|
||||||
@@ -56,6 +56,13 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
return consumedY != 0 || dy == 0
|
return consumedY != 0 || dy == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onScrollStateChanged(state: Int) {
|
||||||
|
super.onScrollStateChanged(state)
|
||||||
|
if (state == SCROLL_STATE_IDLE) {
|
||||||
|
updateChildrenScroll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun consumeVerticalScroll(dy: Int): Int {
|
private fun consumeVerticalScroll(dy: Int): Int {
|
||||||
if (childCount == 0) {
|
if (childCount == 0) {
|
||||||
return 0
|
return 0
|
||||||
@@ -106,16 +113,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyScrollChanged(dy: Int) {
|
private fun notifyScrollChanged(dy: Int) {
|
||||||
updateChildrenScroll()
|
|
||||||
val listeners = onPageScrollListeners
|
val listeners = onPageScrollListeners
|
||||||
if (listeners.isNullOrEmpty()) {
|
if (listeners.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val centerPosition = findCenterViewPosition()
|
val centerPosition = findCenterViewPosition()
|
||||||
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
validateLayout()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun relayoutChildren() {
|
fun relayoutChildren() {
|
||||||
@@ -128,29 +131,35 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateChildrenScroll() {
|
fun updateChildrenScroll() {
|
||||||
forEach { child ->
|
if (isFixingScroll) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFixingScroll = true
|
||||||
|
for (child in this) {
|
||||||
val ssiv = (child as WebtoonFrameLayout).target
|
val ssiv = (child as WebtoonFrameLayout).target
|
||||||
when {
|
if (adjustScroll(child, ssiv)) {
|
||||||
child.top < 0 -> ssiv.scrollTo(ssiv.getScrollRange())
|
break
|
||||||
child.top > 0 -> ssiv.scrollTo(0)
|
|
||||||
else -> ssiv.scrollBy(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isFixingScroll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateLayout() {
|
private fun adjustScroll(child: View, ssiv: WebtoonImageView): Boolean = when {
|
||||||
forEach { child ->
|
child.bottom < height && ssiv.getScroll() < ssiv.getScrollRange() -> {
|
||||||
val ssiv = (child as WebtoonFrameLayout).target
|
val distance = minOf(height - child.bottom, ssiv.getScrollRange() - ssiv.getScroll())
|
||||||
val scroll = ssiv.getScroll()
|
scrollBy(0, -distance)
|
||||||
val assertion = when {
|
ssiv.scrollBy(distance)
|
||||||
child.top < 0 -> scroll == ssiv.getScrollRange()
|
true
|
||||||
child.top > 0 -> scroll == 0
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
if (!assertion) {
|
|
||||||
Toast.makeText(context, "Scroll = $scroll for view with top: ${child.top}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.top > 0 && ssiv.getScroll() > 0 -> {
|
||||||
|
val distance = minOf(child.top, ssiv.getScroll())
|
||||||
|
scrollBy(0, distance)
|
||||||
|
ssiv.scrollBy(-distance)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class OnPageScrollListener {
|
abstract class OnPageScrollListener {
|
||||||
|
|||||||
Reference in New Issue
Block a user