Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.reactnativekeyboardcontroller.views

import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ScrollView
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup
import com.reactnativekeyboardcontroller.extensions.px
import kotlin.math.max

@SuppressLint("ViewConstructor")
class ClippingScrollViewDecoratorView(
Expand All @@ -15,13 +17,42 @@ class ClippingScrollViewDecoratorView(
private var insetBottom = 0.0
private var insetTop = 0.0
private var appliedTopInsetPx = 0
private var paddingScrollWorkaroundActive = false

override fun onAttachedToWindow() {
super.onAttachedToWindow()

decorateScrollView()
}

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val scrollView = findScrollView(this)

if (scrollView == null) {
return super.dispatchTouchEvent(event)
}

if (event.actionMasked == MotionEvent.ACTION_DOWN) {
paddingScrollWorkaroundActive = shouldUsePaddingScrollWorkaround(scrollView, event)
}

val handled =
if (paddingScrollWorkaroundActive) {
dispatchWithExpandedContentRange(scrollView, event)
} else {
super.dispatchTouchEvent(event)
}

if (
event.actionMasked == MotionEvent.ACTION_UP ||
event.actionMasked == MotionEvent.ACTION_CANCEL
) {
paddingScrollWorkaroundActive = false
}

return handled
}

fun setContentInsetBottom(value: Double) {
insetBottom = value
decorateScrollView()
Expand Down Expand Up @@ -68,6 +99,65 @@ class ClippingScrollViewDecoratorView(
appliedTopInsetPx = newTopInsetPx
}

private fun shouldUsePaddingScrollWorkaround(
scrollView: ScrollView,
event: MotionEvent,
): Boolean {
val contentView = scrollView.getChildAt(0) ?: return false
val viewportHeight =
scrollView.height - scrollView.paddingTop - scrollView.paddingBottom
val paddingCreatesScrollRange = contentView.height > viewportHeight

return scrollView.scrollY == 0 &&
paddingCreatesScrollRange &&
!scrollView.canScrollVertically(1) &&
isTouchInScrollContent(scrollView, event)
}

private fun dispatchWithExpandedContentRange(
scrollView: ScrollView,
event: MotionEvent,
): Boolean {
val contentView = scrollView.getChildAt(0) ?: return super.dispatchTouchEvent(event)
val originalBottom = contentView.bottom
val expandedBottom =
max(originalBottom, scrollView.height + scrollView.scrollY + MIN_SCROLL_RANGE_PX)

if (expandedBottom == originalBottom) {
return super.dispatchTouchEvent(event)
}

return try {
contentView.bottom = expandedBottom
super.dispatchTouchEvent(event)
} finally {
contentView.bottom = originalBottom
}
}

private fun isTouchInScrollContent(
scrollView: ScrollView,
event: MotionEvent,
): Boolean {
val contentView = scrollView.getChildAt(0) ?: return false
val thisLocation = IntArray(COORDINATES_SIZE)
val scrollViewLocation = IntArray(COORDINATES_SIZE)

getLocationOnScreen(thisLocation)
scrollView.getLocationOnScreen(scrollViewLocation)

val x = event.x + thisLocation[0] - scrollViewLocation[0]
val y = event.y + thisLocation[1] - scrollViewLocation[1]
val scrollY = scrollView.scrollY

return !(
y < contentView.top - scrollY ||
y >= contentView.bottom - scrollY ||
x < contentView.left ||
x >= contentView.right
)
}

private fun findScrollView(view: View?): ScrollView? {
var result: ScrollView? = null

Expand All @@ -83,4 +173,9 @@ class ClippingScrollViewDecoratorView(

return result
}

companion object {
private const val COORDINATES_SIZE = 2
private const val MIN_SCROLL_RANGE_PX = 2
}
}
Loading