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
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>
from Vertical ViewPager and Android Pie Inconsistent Behavior with Swipe Gesture
The problem seems to be in the way swapXY exchanges the x and y values. I believe that the VelocityTracker used in ViewPager uses #getAxisValue and not #getX/#getY, and the result is not swapped. But there doesn't seem to be a way to set the axis values nor to subclass MotionEvent so I didn't find a way to stick with the simple swapXY solution. I forked ViewPager and use VelocityTracker.getYVelocity when I detect the unswapped condition.
ReplyDeletediff --git project/src/main/java/org/gc/project/util/ShopVerticalViewPager.java project/src/main/java/org/gc/project/util/ShopVerticalViewPager.java
index e5560a0..f23f9f7 100644
--- project/src/main/java/org/gc/project/util/MyVerticalViewPager.java
+++ project/src/main/java/org/gc/project/util/MyVerticalViewPager.java
@@ -179,4 +179,8 @@ public class ShopVerticalViewPager extends ViewPager {
return super.onTouchEvent( swapXY( ev ) );
}
+ public boolean isVerticalMode() {
+ return true;
+ }
+
}
\ No newline at end of file
diff --git project/src/main/java/gcandroid/support/v4/view/ViewPager.java project/src/main/java/gcandroid/support/v4/view/ViewPager.java
index 20e1448..4ae2d3c 100644
--- project/src/main/java/gcandroid/support/v4/view/ViewPager.java
+++ project/src/main/java/gcandroid/support/v4/view/ViewPager.java
@@ -205,6 +205,7 @@ public class ViewPager extends ViewGroup {
private int mMaximumVelocity;
private int mFlingDistance;
private int mCloseEnough;
+ private boolean mInvertedVelocityTrackerInVerticalMode = false;
// If the pager is at least this close to its final position, complete the scroll
// on touch down and let the user interact with the content inside instead of
@@ -391,6 +392,21 @@ public class ViewPager extends ViewGroup {
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
+
+ // tell if the velocity tracker is inverted in vertical mode (most probably uses MotionEvent.getAxisValue instead of MotionEvent.getX but
+ // I can't change these values nor inherit from MotionEvent)
+ VelocityTracker vt = VelocityTracker.obtain();
+ long time = SystemClock.uptimeMillis();
+ MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ vt.addMovement(ev);
+ ev.recycle();
+ ev = MotionEvent.obtain(time, time + 10, MotionEvent.ACTION_MOVE, 10, 0, 0);
+ ev.setLocation( 0, 10 );
+ vt.addMovement(ev);
+ ev.recycle();
+ vt.computeCurrentVelocity(1000, mMaximumVelocity);
+ mInvertedVelocityTrackerInVerticalMode = vt.getYVelocity() == 0;
+ vt.recycle();
}
@Override
@@ -2027,6 +2043,10 @@ public class ViewPager extends ViewGroup {
return mIsBeingDragged;
}
+ public boolean isVerticalMode() {
+ return false;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mFakeDragging) {
@@ -2111,8 +2131,9 @@ public class ViewPager extends ViewGroup {
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
- int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
- velocityTracker, mActivePointerId);
+ int initialVelocity = (int) ( isVerticalMode() && mInvertedVelocityTrackerInVerticalMode ? VelocityTrackerCompat.getYVelocity( velocityTracker, mActivePointerId )
+ : VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId ) );
+
mPopulatePending = true;
final int width = getClientWidth();
final int scrollX = getScrollX();