diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ScalingFrame.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ScalingFrame.kt
new file mode 100644
index 000000000..b7ec2ea45
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ScalingFrame.kt
@@ -0,0 +1,196 @@
+package org.koitharu.kotatsu.reader.ui.pager.webtoon
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.widget.FrameLayout
+import android.widget.OverScroller
+import androidx.core.view.GestureDetectorCompat
+
+private const val TAG = "ScalingFrame"
+private const val MAX_SCALE = 2.5f
+private const val MIN_SCALE = 0.5f
+
+class ScalingFrame @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyles: Int = 0
+): FrameLayout(context, attrs, defStyles), ScaleGestureDetector.OnScaleGestureListener {
+ private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) }
+
+ private val scaleDetector = ScaleGestureDetector(context, this)
+ private val gestureDetector = GestureDetectorCompat(context, GestureListener())
+ private val overScroller = OverScroller(context, AccelerateDecelerateInterpolator())
+ private val transformMatrix = Matrix()
+ private val matrixValues = FloatArray(9)
+ private val scale
+ get() = matrixValues[Matrix.MSCALE_X]
+ private val transX
+ get() = halfWidth*(scale - 1f) + matrixValues[Matrix.MTRANS_X]
+ private val transY
+ get() = halfHeight*(scale - 1f) + matrixValues[Matrix.MTRANS_Y]
+ private var halfWidth = 0f
+ private var halfHeight = 0f
+ private val translateBounds = RectF()
+ private val targetTouchRect = Rect()
+
+ init {
+ syncMatrixValues()
+ }
+
+ override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
+ ev ?: return super.dispatchTouchEvent(ev)
+ if (ev.action == MotionEvent.ACTION_DOWN && overScroller.computeScrollOffset()) {
+ overScroller.forceFinished(true)
+ }
+
+ gestureDetector.onTouchEvent(ev)
+ scaleDetector.onTouchEvent(ev)
+
+ // Offset event to inside the child view
+ if (scale < 1) {
+ targetChild.getHitRect(targetTouchRect)
+ if (!targetTouchRect.contains(ev.x.toInt(), ev.y.toInt())) {
+ ev.offsetLocation(halfWidth - ev.x - targetChild.width/4, 0f)
+ }
+ }
+
+ return super.dispatchTouchEvent(ev)
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+ return super.onInterceptTouchEvent(ev) || scaleDetector.isInProgress
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ halfWidth = measuredWidth / 2f
+ halfHeight = measuredHeight / 2f
+ }
+
+ private fun invalidateRecycler() {
+ adjustBounds()
+ targetChild.run {
+ scaleX = scale
+ scaleY = scale
+ translationX = transX
+ translationY = transY
+ }
+
+ val newHeight = if (scale < 1f) (height / scale).toInt() else height
+ if (newHeight != targetChild.height) {
+ targetChild.layoutParams.height = newHeight
+ targetChild.requestLayout()
+ }
+
+ }
+
+ private fun syncMatrixValues() {
+ transformMatrix.getValues(matrixValues)
+ }
+
+ private fun adjustBounds() {
+ syncMatrixValues()
+ val dx = when {
+ transX < translateBounds.left -> translateBounds.left - transX
+ transX > translateBounds.right -> translateBounds.right - transX
+ else -> 0f
+ }
+
+ val dy = when {
+ transY < translateBounds.top -> translateBounds.top - transY
+ transY > translateBounds.bottom -> translateBounds.bottom - transY
+ else -> 0f
+ }
+
+ transformMatrix.postTranslate(dx, dy)
+ syncMatrixValues()
+ }
+
+ private fun scaleChild(newScale: Float, focusX: Float, focusY: Float) {
+ val factor = newScale / scale
+ if (newScale > 1) {
+ translateBounds.set(
+ halfWidth * (1 - newScale),
+ halfHeight * (1 - newScale),
+ halfWidth * (newScale - 1),
+ halfHeight * (newScale - 1)
+ )
+ } else {
+ translateBounds.set(
+ 0f,
+ halfHeight - halfHeight / newScale,
+ 0f,
+ halfHeight - halfHeight / newScale
+ )
+ }
+ transformMatrix.postScale(factor, factor, focusX, focusY)
+ invalidateRecycler()
+ }
+
+
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ val newScale = (scale * detector.scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE)
+ scaleChild(newScale, halfWidth, halfHeight)
+ return true
+ }
+
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
+
+ override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
+
+
+ private inner class GestureListener(): GestureDetector.SimpleOnGestureListener(), Runnable {
+ override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
+ if (scale <= 1f) return false
+ transformMatrix.postTranslate(-distanceX, -distanceY)
+ invalidateRecycler()
+ return true
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ val newScale = if (scale != 1f) 1f else MAX_SCALE
+ ObjectAnimator.ofFloat(scale, newScale).run {
+ interpolator = AccelerateDecelerateInterpolator()
+ duration = 300
+ addUpdateListener {
+ scaleChild(it.animatedValue as Float, e.x, e.y)
+ }
+ start()
+ }
+ return true
+ }
+
+ override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
+ if (scale <= 1) return false
+
+ overScroller.fling(
+ transX.toInt(),
+ transY.toInt(),
+ velocityX.toInt(),
+ velocityY.toInt(),
+ translateBounds.left.toInt(),
+ translateBounds.right.toInt(),
+ translateBounds.top.toInt(),
+ translateBounds.bottom.toInt()
+ )
+ postOnAnimation(this)
+ return true
+ }
+
+ override fun run() {
+ if (overScroller.computeScrollOffset()) {
+ transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY)
+ invalidateRecycler()
+ postOnAnimation(this)
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/fragment_reader_webtoon.xml b/app/src/main/res/layout/fragment_reader_webtoon.xml
index 2a3020690..5a09e8fdc 100644
--- a/app/src/main/res/layout/fragment_reader_webtoon.xml
+++ b/app/src/main/res/layout/fragment_reader_webtoon.xml
@@ -1,9 +1,14 @@
-
\ No newline at end of file
+ android:layout_height="match_parent">
+
+