Search code examples
androidandroid-viewpagergestureandroid-9.0-pie

Vertical ViewPager and Android Pie Inconsistent Behavior with Swipe Gesture


My problem is closely related to two other questions that haven't been answered yet.

ViewPager not responding to touch in layout area created dynamically in Fragment

https://stackoverflow.com/questions/53469581/problem-with-vertical-viewpager-like-inshorts

My Vertical ViewPager works wonderfully and consistently within any device I have tested and with any OS 5 - 8. I recently upgraded a pixel 2XL with Android Pie and now my Vertical ViewPager appears to be unresponsive, then works, then it acts like it loses focus, then works. Drag a page and it moves and snaps back to original position. Or just bounces back. Again, similar to the other two questions linked above.

Prior to Android 9, vertical scrolling and paging is perfect. I've tried using reflection with a little success. It will swipe better and doesn't seem to lose focus as much. But if I try swiping with my other hand it stops, or if I change my placement of where I am swiping it will stop. This is very perplexing. I have added all the code required to replicate this issue on a device running Android 9.

The Activity

public class FullscreenActivity extends AppCompatActivity {

VerticalViewPager verticalViewPager;
FragmentStatePagerExample fragmentStatePagerExample;

int pagerPadding;

/**
 * Whether or not the system UI should be auto-hidden after
 * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
 */
private static final boolean AUTO_HIDE = true;

/**
 * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
 * user interaction before hiding the system UI.
 */
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;

/**
 * Some older devices needs a small delay between UI widget updates
 * and a change of the status and navigation bar.
 */
private static final int UI_ANIMATION_DELAY = 300;
private final Handler mHideHandler = new Handler();
private FrameLayout mContentView;

private final Runnable mHidePart2Runnable = new Runnable() {
    @SuppressLint("InlinedApi")
    @Override
    public void run() {
        // Delayed removal of status and navigation bar

        // Note that some of these constants are new as of API 16 (Jelly Bean)
        // and API 19 (KitKat). It is safe to use them, as they are inlined
        // at compile-time and do nothing on earlier devices.
        verticalViewPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
    }
};
private View mControlsView;
private final Runnable mShowPart2Runnable = new Runnable() {
    @Override
    public void run() {
        // Delayed display of UI elements
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.show();
        }
        mControlsView.setVisibility(View.VISIBLE);
    }
};
private boolean mVisible;
private final Runnable mHideRunnable = new Runnable() {
    @Override
    public void run() {
        hide();
    }
};
/**
 * Touch listener to use for in-layout UI controls to delay hiding the
 * system UI. This is to prevent the jarring behavior of controls going away
 * while interacting with activity UI.
 */
private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (AUTO_HIDE) {
            delayedHide(AUTO_HIDE_DELAY_MILLIS);
        }
        return false;
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_fullscreen);


    pagerPadding = getScreenDimension(this);

    mVisible = true;
    mControlsView = findViewById(R.id.fullscreen_content_controls);

    verticalViewPager = findViewById(R.id.main_viewpager);

    verticalViewPager.setPadding(0,0,0,pagerPadding);
    verticalViewPager.setClipToPadding(false);

    fragmentStatePagerExample = new FragmentStatePagerExample(getSupportFragmentManager());

    verticalViewPager.setAdapter(fragmentStatePagerExample);

    verticalViewPager.setCurrentItem(0);

    // Set up the user interaction to manually show or hide the system UI.
    verticalViewPager.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            toggle();
        }
    });

    // Upon interacting with UI controls, delay any scheduled hide()
    // operations to prevent the jarring behavior of controls going away
    // while interacting with the UI.
    findViewById(R.id.dummy_button).setOnTouchListener(mDelayHideTouchListener);
}

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);

    // Trigger the initial hide() shortly after the activity has been
    // created, to briefly hint to the user that UI controls
    // are available.
    delayedHide(100);
}

private void toggle() {
    if (mVisible) {
        hide();
    } else {
        show();
    }
}

private void hide() {
    // Hide UI first
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
        actionBar.hide();
    }
    mControlsView.setVisibility(View.GONE);
    mVisible = false;

    // Schedule a runnable to remove the status and navigation bar after a delay
    mHideHandler.removeCallbacks(mShowPart2Runnable);
    mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
}

@SuppressLint("InlinedApi")
private void show() {
    // Show the system bar
    mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
    mVisible = true;

    // Schedule a runnable to display UI elements after a delay
    mHideHandler.removeCallbacks(mHidePart2Runnable);
    mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
}

/**
 * Schedules a call to hide() in delay milliseconds, canceling any
 * previously scheduled calls.
 */
private void delayedHide(int delayMillis) {
    mHideHandler.removeCallbacks(mHideRunnable);
    mHideHandler.postDelayed(mHideRunnable, delayMillis);
}

private static int getScreenDimension(Context context)
{
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();
    DisplayMetrics metrics = new DisplayMetrics();
    display.getMetrics(metrics);
    int width = metrics.widthPixels;
    int height = metrics.heightPixels;

    return (int)Math.round(height * .2);
}
}

The Fragment

public class ImageFragment extends Fragment{

ImageView imageView;

String imageUrl = "";

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);

    Bundle bundle = getArguments();

    imageUrl = bundle.getString("url");

    return inflater.inflate(R.layout.fragment_image, container,false);

}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    imageView = (ImageView)view.findViewById(R.id.iv_imagefragment);

    Glide.with(getActivity()).load(imageUrl).into(imageView);

}

public static Fragment getInstance(int position, String url){

    Bundle bundle = new Bundle();
    bundle.putString("url",url);
    ImageFragment fragment = new ImageFragment();
    fragment.setArguments(bundle);
    return fragment;

}
}

The ViewPager

public class VerticalViewPager extends ViewPager {

public VerticalViewPager(Context context) {
    super(context);
    init();
}

public VerticalViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

private void init() {
    // The majority of the magic happens here
    setPageTransformer(true, new VerticalPageTransformer());
    setOffscreenPageLimit(2);
    // The easiest way to get rid of the overscroll drawing that happens on the left and right
    setOverScrollMode(OVER_SCROLL_NEVER);
}

private class VerticalPageTransformer implements ViewPager.PageTransformer {

    @Override
    public void transformPage(View view, float position) {

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 1) { // [-1,1]
            view.setAlpha(1);

            // Counteract the default slide transition
            view.setTranslationX(view.getWidth() * -position);

            //set Y position to swipe in from top
            float yPosition = position * view.getHeight();
            view.setTranslationY(yPosition);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

/**
 * Swaps the X and Y coordinates of your touch event.
 */
private MotionEvent swapXY(MotionEvent ev) {
    float width = getWidth();
    float height = getHeight();

    float newX = (ev.getY() / height) * width;
    float newY = (ev.getX() / width) * height;

    ev.setLocation(newX, newY);

    return ev;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
    boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
    swapXY(ev); // return touch coordinates to original reference frame for any child views
    return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    return super.onTouchEvent(swapXY(ev));
}

}

The ViewPager Adapter

public class FragmentStatePagerExample extends FragmentStatePagerAdapter {

String url = "";

public FragmentStatePagerExample(FragmentManager fm) {
    super(fm);
}

@Override
public Fragment getItem(int position) {


    switch (position){
        case 0:
            url = "https://images.unsplash.com/photo-1532977692289-827d858a170b?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=29b1d5377ad9db8de64b1b73d21812c7&auto=format&fit=crop&w=1474&q=80";
            return ImageFragment.getInstance(position,url);
        case 1:
            url = "https://images.unsplash.com/photo-1533029516911-0458c644baea?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0f618e036e338f48ef919b8fb86c5ba1&auto=format&fit=crop&w=701&q=80";
            return ImageFragment.getInstance(position,url);
        case 2:
            url = "https://images.unsplash.com/photo-1532989622000-d4f013a215e1?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1a69643c04176376315714b9b2897de5&auto=format&fit=crop&w=677&q=80";
            return ImageFragment.getInstance(position,url);
        default:
            url = "https://images.unsplash.com/photo-1532983819500-85d633c73b7a?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1f0b228b67f03064241534a6c65d9497&auto=format&fit=crop&w=1050&q=80";
            return ImageFragment.getInstance(position,url);
    }


}

@Override
public int getCount() {
    return 4;
}
}

Activity XML

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0099cc"
tools:context=".FullscreenActivity">

<!-- This FrameLayout insets its children based on system windows using
     android:fitsSystemWindows. -->


    <com.david.verticalviewpagerexample.VerticalViewPager
        android:id="@+id/main_viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>



    <LinearLayout
        android:id="@+id/fullscreen_content_controls"
        style="?metaButtonBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:background="@color/black_overlay"
        android:orientation="horizontal"
        tools:ignore="UselessParent">

        <Button
            android:id="@+id/dummy_button"
            style="?metaButtonBarButtonStyle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/dummy_button" />

    </LinearLayout>


</FrameLayout>

Fragment XML

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
    android:id="@+id/iv_imagefragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="centerCrop"/>

</LinearLayout>

Update : 1

https://github.com/youngkaaa/YViewPagerDemo

There is another library and that works really smooth on Android Pie, but it has few soft crashes. Also, it crashes on API 19.

Update : 2

Google has recently released ViewPager2 with androidx support library https://developer.android.com/jetpack/androidx/releases/viewpager2, that supports vertical viewpager. However, it is still in alpha version and it has many known issues.


Solution

  • After spending hell amount of time on SO, trying many GitHub libraries and waiting for someone to respond on the bounty, then I came up with below solutions and hope that it helps whoever needs it.

    At first, this question got my attention and I think most of the answers over there are helpful, hence I upvoted them. The main answer which helped me are link-1 and link-2.

    Even though I have to make a few minor changes to ignore horizontal swipe fling events.

    Please do try this code and provide your feedback for any further improvements, thanks in advance. Happy Coding :)

    import android.content.Context;
    import android.support.v4.view.MotionEventCompat;
    import android.support.v4.view.ViewPager;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    
    public class VerticalViewPager extends ViewPager {
        float x = 0;
        float mStartDragX = 0;
        private static final float SWIPE_X_MIN_THRESHOLD = 50; // Decide this magical nuber as per your requirement
    
        public VerticalViewPager(Context context) {
            super(context);
            init();
        }
    
        public VerticalViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            // The majority of the magic happens here
            setPageTransformer(true, new VerticalPageTransformer());
            // The easiest way to get rid of the overscroll drawing that happens on the left and right
            setOverScrollMode(OVER_SCROLL_NEVER);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (getAdapter() != null) {
                if (getCurrentItem() >= 0 || getCurrentItem() < getAdapter().getCount()) {
                    swapXY(event);
                    final int action = event.getAction();
                    switch (action & MotionEventCompat.ACTION_MASK) {
                        case MotionEvent.ACTION_MOVE:
                            break;
                        case MotionEvent.ACTION_UP:
                            mStartDragX = event.getX();
                            if (x < mStartDragX
                                    && (mStartDragX - x > SWIPE_X_MIN_THRESHOLD)
                                    && getCurrentItem() > 0) {
                                Log.i("VerticalViewPager", "down " + x + " : " + mStartDragX + " : " + (mStartDragX - x));
                                setCurrentItem(getCurrentItem() - 1, true);
                                return true;
                            } else if (x > mStartDragX
                                    && (x - mStartDragX > SWIPE_X_MIN_THRESHOLD)
                                    && getCurrentItem() < getAdapter().getCount() - 1) {
                                Log.i("VerticalViewPager", "up " + x + " : " + mStartDragX + " : " + (x - mStartDragX));
                                mStartDragX = 0;
                                setCurrentItem(getCurrentItem() + 1, true);
                                return true;
                            }
                            break;
                    }
                } else {
                    mStartDragX = 0;
                }
                swapXY(event);
                return super.onTouchEvent(swapXY(event));
            }
            return false;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            boolean intercepted = super.onInterceptTouchEvent(swapXY(event));
            switch (event.getAction() & MotionEventCompat.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    x = event.getX();
                    break;
            }
            swapXY(event); // return touch coordinates to original reference frame for any child views
            return intercepted;
        }
    
        /**
         * Swaps the X and Y coordinates of your touch event.
         */
        private MotionEvent swapXY(MotionEvent ev) {
            float width = getWidth();
            float height = getHeight();
    
            float newX = (ev.getY() / height) * width;
            float newY = (ev.getX() / width) * height;
    
            ev.setLocation(newX, newY);
    
            return ev;
        }
    
        private class VerticalPageTransformer implements PageTransformer {
            @Override
            public void transformPage(View view, float position) {
    
                if (position < -1) { // [-Infinity,-1)
                    // This page is way off-screen to the left.
                    view.setAlpha(0);
    
                } else if (position <= 1) { // [-1,1]
                    view.setAlpha(1);
    
                    // Counteract the default slide transition
                    view.setTranslationX(view.getWidth() * -position);
    
                    //set Y position to swipe in from top
                    float yPosition = position * view.getHeight();
                    view.setTranslationY(yPosition);
    
                } else { // (1,+Infinity]
                    // This page is way off-screen to the right.
                    view.setAlpha(0);
                }
            }
        }
    }