callstack / react-native-pager-view

React Native wrapper for the Android ViewPager and iOS UIPageViewController.
MIT License
2.69k stars 413 forks source link

Vertical Android ViewPager not working on Android 9 (Pie) #117

Open ferrannp opened 4 years ago

ferrannp commented 4 years ago

Swiping for a short distance and fast should change the page. This is working fine in my Pixel 2 (Android 10) and in Android the emulator. However, we tried a couple of devices where is not working correctly:

This is the behavior on for the vertical ViewPager:

vertical

Basically the user needs to scroll all the way down or up (manually) to trigger a change of page. With the horizontal ViewPager (recorded on the same phone), it works just as expected:

horizontal

Would love to get some help from you @devvit if you know what might be causing this.

devvit commented 4 years ago

@ferrannp, What about this ? I haven't these devices, only tested on my 5 devices(4.4 - 9.0) :(

ferrannp commented 4 years ago

Thanks for the reference @devvit! I'll be trying it tomorrow.

ferrannp commented 4 years ago

Mmm for now still that did not really help @devvit. That just seems to play with the threshold the page needs to be moved to swipe... But I think the problem here is different. If you drag very small distance but fast (with power), it should swipe to the next page. Still investigating... By the way, I was able to reproduce it on an emulator (Note 9 Android 9) using https://developer.samsung.com/remotetestlab/. The only drawback is that every time you want to try something you need to build the production apk.

ferrannp commented 4 years ago

Ok, I can reproduce the issue with the Android emulator (good to find a solution) on API level 28 (Android P). It seems to work before and after but not on Android P (most popular nowadays).

devvit commented 4 years ago

@ferrannp, Make ViewPager snap with shorter drag

My reflection solution, you can change the value of mMinimumVelocity and mFlingDistance.

    public void setOrientation(boolean vertical) {
        mVertical = vertical;
        if (!mVertical) return;

        // Make page transit vertical
        setPageTransformer(true, new VerticalPageTransformer());

        // Nested scroll issue, follow the link
        // https://stackoverflow.com/questions/46828920/vertical-viewpager-with-horizontalscrollview-inside-fragment
        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                return Math.abs(distanceY) > Math.abs(distanceX);
            }
        });

        // reflection
        try {
            Field mFlingDistance = ViewPager.class.getDeclaredField("mFlingDistance");
            mFlingDistance.setAccessible(true);
            mFlingDistance.setInt(this, 10);

            Field mMinimumVelocity = ViewPager.class.getDeclaredField("mMinimumVelocity");
            mMinimumVelocity.setAccessible(true);
            mMinimumVelocity.setInt(this, 5);
        } catch (Exception e) {
            Log.e("###", e.getMessage());
        }
    }

Could you try straya's answer? I'm not sure that.

ferrannp commented 4 years ago

@devvit I found a a perfect working solution modifying the source code. https://android.googlesource.com/platform/frameworks/support/+/5b614a46f6ffb3e9ca5ab6321c12412550a4e13a/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java#2251. Instead of getXVelocity, if pager is vertical would be getYVelocity.

Can we achieve this by reflection without copying the whole source code?

devvit commented 4 years ago

Looks impossible 😓, just change mMinimumVelocity. Or maybe we commit a PR to android master dev 😅

ferrannp commented 4 years ago

Or I'll fork the internal code here (it will not change anyway as they are with ViewPager2 now). I'll prepare a PR.

devvit commented 4 years ago

Yes, fork it, freeze it, until we migrate to VP2

troZee commented 4 years ago

we can close it, when https://github.com/react-native-community/react-native-viewpager/pull/139 will be merged.

todorone commented 4 years ago

@troZee @ferrannp Is there any workaround to use vertical ViewPager now? I was so excited about native feel viewpager on react native, but found out that it's broken on Android...😿

PS: Thanks for working on it!

ferrannp commented 4 years ago

@todorone you can wait for https://github.com/react-native-community/react-native-viewpager/pull/139 or you can use this branch for now https://github.com/react-native-community/react-native-viewpager/tree/fix/vertical-pager-android-9-bug.

troZee commented 4 years ago

@todorone or you can create patch-package of #139. I need to fix one thing. Ofc I am not guarantee, that it will work, but you can try.

PS: Thanks for working on it!

<3

todorone commented 4 years ago

@troZee @ferrannp Thanks for prompt feedback, you guys are awesome.

Just tried both fix/vertical-pager-android-9-bug branch and also viewpager2 branch. Both of them didn't help, I still need to swipe with finger for a almost full height distance to make vertical transition happen. Checked on both horizontal and vertical device alignment, Android 9 Mi 5 Note device(Emulator with Android 10 works perfectly)...

laoxubuer commented 4 years ago

@todorone you can use a custom hook to handle touch events by yourself like below:

// the hook
import React, {useState} from 'react';
function TouchGesture(ref) {
  const [duration, setDuration] = useState(0);
  const [points, setPoints] = useState([0, 0]);
  function onTouchStart(e) {
    const {pageX, pageY} = e.nativeEvent;
    setDuration(Date.now());
    setPoints([pageX, pageY]);
  }
  function onTouchEnd(e) {
    const {pageX, pageY} = e.nativeEvent;
    if (Date.now() - duration > 200 || Math.abs(points[0] - pageX) > 50) {
      return;
    }
   // this just an example
   // you can use viewPager's position (add a onPageSelected function there) to judge which page you'll set; 
    if (points[1] - pageY > 30) {
      ref?.current.setPage(1);
    } else if (pageY - points[1] > 30) {
      ref?.current.setPage(0);
    }
  }
  return {onTouchEnd, onTouchStart};
}
export default TouchGesture;

// usage
const viewPagerRef = useRef(null);
const {onTouchEnd, onTouchStart} = useTouchGesture(viewPagerRef);
 const handleTouchEnd = useCallback(e => onTouchEnd(e), [onTouchEnd]);
const handleTouchStart = useCallback(e => onTouchStart(e), [onTouchStart]);
......
<ViewPager
      ref={viewPagerRef}
      scrollEnabled={false}
      orientation="vertical"
      pageMargin={0}
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
      style={styles.viewPager}
      initialPage={0}>
    ......
</ViewPager>