Migrate com.facebook.react.views.text.ReactSwipeRefreshLayout to Kotlin (#47610)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47610

As per title.

Changelog: [Internal]

Reviewed By: tdn120

Differential Revision: D65661766

fbshipit-source-id: 3d56f94f90e7b70160e4ca7898aa0a49997ddb87
This commit is contained in:
Fabrizio Cucci 2024-11-14 09:27:40 -08:00 committed by Facebook GitHub Bot
parent 184eb17261
commit db3c1a47d5
3 changed files with 133 additions and 153 deletions

View File

@ -7113,14 +7113,14 @@ public final class com/facebook/react/views/scroll/ScrollEventType$Companion {
public final fun getJSEventName (Lcom/facebook/react/views/scroll/ScrollEventType;)Ljava/lang/String;
}
public class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout {
public final class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout {
public fun <init> (Lcom/facebook/react/bridge/ReactContext;)V
public fun canChildScrollUp ()Z
public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z
public fun onLayout (ZIIII)V
public fun onTouchEvent (Landroid/view/MotionEvent;)Z
public fun requestDisallowInterceptTouchEvent (Z)V
public fun setProgressViewOffset (F)V
public final fun setProgressViewOffset (F)V
public fun setRefreshing (Z)V
}

View File

@ -1,151 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.swiperefresh;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.events.NativeGestureUtil;
/** Basic extension of {@link SwipeRefreshLayout} with ReactNative-specific functionality. */
@Nullsafe(Nullsafe.Mode.LOCAL)
public class ReactSwipeRefreshLayout extends SwipeRefreshLayout {
private static final float DEFAULT_CIRCLE_TARGET = 64;
private boolean mDidLayout = false;
private boolean mRefreshing = false;
private float mProgressViewOffset = 0;
private int mTouchSlop;
private float mPrevTouchX;
private boolean mIntercepted;
private boolean mNativeGestureStarted = false;
public ReactSwipeRefreshLayout(ReactContext reactContext) {
super(reactContext);
mTouchSlop = ViewConfiguration.get(reactContext).getScaledTouchSlop();
}
@Override
public void setRefreshing(boolean refreshing) {
mRefreshing = refreshing;
// `setRefreshing` must be called after the initial layout otherwise it
// doesn't work when mounting the component with `refreshing = true`.
// Known Android issue: https://code.google.com/p/android/issues/detail?id=77712
if (mDidLayout) {
super.setRefreshing(refreshing);
}
}
public void setProgressViewOffset(float offset) {
mProgressViewOffset = offset;
// The view must be measured before calling `getProgressCircleDiameter` so
// don't do it before the initial layout.
if (mDidLayout) {
int diameter = getProgressCircleDiameter();
int start = Math.round(PixelUtil.toPixelFromDIP(offset)) - diameter;
int end = Math.round(PixelUtil.toPixelFromDIP(offset + DEFAULT_CIRCLE_TARGET) - diameter);
setProgressViewOffset(false, start, end);
}
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!mDidLayout) {
mDidLayout = true;
// Update values that must be set after initial layout.
setProgressViewOffset(mProgressViewOffset);
setRefreshing(mRefreshing);
}
}
@Override
public boolean canChildScrollUp() {
View firstChild = getChildAt(0);
if (firstChild != null) {
return firstChild.canScrollVertically(-1);
} else {
return super.canChildScrollUp();
}
}
/**
* {@link SwipeRefreshLayout} overrides {@link ViewGroup#requestDisallowInterceptTouchEvent} and
* swallows it. This means that any component underneath SwipeRefreshLayout will now interact
* incorrectly with Views that are above SwipeRefreshLayout. We fix that by transmitting the call
* to this View's parents.
*/
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
mNativeGestureStarted = true;
// If the pull-to-refresh gesture is interrupted by a parent with its own
// onInterceptTouchEvent then the refresh indicator gets stuck on-screen
// so we ask the parent to not intercept this touch event after it started
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP && mNativeGestureStarted) {
NativeGestureUtil.notifyNativeGestureEnded(this, ev);
mNativeGestureStarted = false;
}
return super.onTouchEvent(ev);
}
/**
* {@link SwipeRefreshLayout} completely bypasses ViewGroup's "disallowIntercept" by overriding
* {@link ViewGroup#onInterceptTouchEvent} and never calling super.onInterceptTouchEvent(). This
* means that horizontal scrolls will always be intercepted, even though they shouldn't, so we
* have to check for that manually here.
*/
private boolean shouldInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevTouchX = ev.getX();
mIntercepted = false;
break;
case MotionEvent.ACTION_MOVE:
final float eventX = ev.getX();
final float xDiff = Math.abs(eventX - mPrevTouchX);
if (mIntercepted || xDiff > mTouchSlop) {
mIntercepted = true;
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.swiperefresh
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.events.NativeGestureUtil
/** Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. */
public class ReactSwipeRefreshLayout(reactContext: ReactContext) :
SwipeRefreshLayout(reactContext) {
private var didLayout: Boolean = false
private var refreshing: Boolean = false
private var progressViewOffset: Float = 0f
private val touchSlop: Int = ViewConfiguration.get(reactContext).scaledTouchSlop
private var prevTouchX: Float = 0f
private var intercepted: Boolean = false
private var nativeGestureStarted: Boolean = false
public override fun setRefreshing(refreshing: Boolean) {
this.refreshing = refreshing
// `setRefreshing` must be called after the initial layout otherwise it
// doesn't work when mounting the component with `refreshing = true`.
// Known Android issue: https://code.google.com/p/android/issues/detail?id=77712
if (didLayout) {
super.setRefreshing(refreshing)
}
}
public fun setProgressViewOffset(offset: Float) {
progressViewOffset = offset
// The view must be measured before calling `getProgressCircleDiameter` so
// don't do it before the initial layout.
if (didLayout) {
val diameter = progressCircleDiameter
val start = Math.round(PixelUtil.toPixelFromDIP(offset)) - diameter
val end = Math.round(PixelUtil.toPixelFromDIP(offset + DEFAULT_CIRCLE_TARGET)) - diameter
setProgressViewOffset(false, start, end)
}
}
public override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (!didLayout) {
didLayout = true
// Update values that must be set after initial layout.
setProgressViewOffset(progressViewOffset)
setRefreshing(refreshing)
}
}
public override fun canChildScrollUp(): Boolean {
val firstChild = getChildAt(0)
return firstChild?.canScrollVertically(-1) ?: super.canChildScrollUp()
}
/**
* [SwipeRefreshLayout] overrides [ViewGroup.requestDisallowInterceptTouchEvent] and swallows it.
* This means that any component underneath SwipeRefreshLayout will now interact incorrectly with
* Views that are above SwipeRefreshLayout. We fix that by transmitting the call to this View's
* parents.
*/
public override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
}
public override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev)
nativeGestureStarted = true
// If the pull-to-refresh gesture is interrupted by a parent with its own
// onInterceptTouchEvent then the refresh indicator gets stuck on-screen
// so we ask the parent to not intercept this touch event after it started
parent?.requestDisallowInterceptTouchEvent(true)
return true
}
return false
}
public override fun onTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_UP && nativeGestureStarted) {
NativeGestureUtil.notifyNativeGestureEnded(this, ev)
nativeGestureStarted = false
}
return super.onTouchEvent(ev)
}
/**
* [SwipeRefreshLayout] completely bypasses ViewGroup's "disallowIntercept" by overriding
* [ViewGroup.onInterceptTouchEvent] and never calling super.onInterceptTouchEvent(). This means
* that horizontal scrolls will always be intercepted, even though they shouldn't, so we have to
* check for that manually here.
*/
private fun shouldInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
prevTouchX = ev.x
intercepted = false
}
MotionEvent.ACTION_MOVE -> {
val eventX = ev.x
val xDiff = Math.abs(eventX - prevTouchX)
if (intercepted || xDiff > touchSlop) {
intercepted = true
return false
}
}
}
return true
}
private companion object {
private const val DEFAULT_CIRCLE_TARGET = 64f
}
}