背景
由于业务需求,要求使用原生的ViewPager2在rn中实现tab侧滑功能,而tab页面中会有水平滑动的FlatList列表,这样就会造成滑动冲突的情况。
解决思路:因为ViewPager2被声明为final的,所以只能从FlatList去动手,于是本文基于谷歌官方的NestedScrollableHost
进行改造。
步骤
改造NestedScrollableHost给rn使用
用isFromRN
字段标识该组件是由rn创建而来的
ReactFindViewUtil.findView
可以找到当前根view中声明了nativeID属性的rn组件view
class NestedScrollableHost : FrameLayout {
private var isFromRN: Boolean = false
constructor(context: Context, isFromRN: Boolean) : super(context){
this.isFromRN = isFromRN
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View?
get() {
return when {
isFromRN -> {
var view : View? = ReactFindViewUtil.findView(this, "FlatList")
return view
}
childCount > 0 -> {
getChildAt(0)
}
else -> {
null
}
}
}
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
handleInterceptTouchEvent(it) }
return super.dispatchTouchEvent(ev)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
// if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
// }
}else{
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
child为嵌套在NestedScrollableHost中的FlatList组件
public class NestedScrollableHostManager extends ViewGroupManager<NestedScrollableHost> {
@NonNull
@Override
public String getName() {
return "NestedScrollableHost";
}
@NonNull
@Override
protected NestedScrollableHost createViewInstance(@NonNull ThemedReactContext reactContext) {
return new NestedScrollableHost(reactContext, true);
}
}
rn端使用
const NestedScrollableHost = Platform.OS == 'android' ? requireNativeComponent("NestedScrollableHost") : <></>
interface Props{
style?: StyleProp<ViewStyle>
}
const MyNestedScrollableHost: React.FC<Props> = (props) => {
return (
<NestedScrollableHost
{
...props}
>
{
props.children}
</NestedScrollableHost>
)
}
export default MyNestedScrollableHost
在有可能跟原生组件造成滑动冲突的地方引入
<MyNestedScrollableHost>
<FlatList
nativeID={
"FlatList"}
...
/>
</MyNestedScrollableHost>
nativeID的值要与ReactFindViewUtil.findView第二个参数一致
本文有任何不足或者有更好的实现思路的话,欢迎在评论中指出~