垂直 ViewPager 和 Android Pie 与滑动手势不一致的行为
Posted
技术标签:
【中文标题】垂直 ViewPager 和 Android Pie 与滑动手势不一致的行为【英文标题】:Vertical ViewPager and Android Pie Inconsistent Behavior with Swipe Gesture 【发布时间】:2019-05-10 02:25:59 【问题描述】:我的问题与尚未回答的另外两个问题密切相关。
ViewPager not responding to touch in layout area created dynamically in Fragment
https://***.com/questions/53469581/problem-with-vertical-viewpager-like-inshorts
我的 Vertical ViewPager 在我测试过的任何设备以及任何 OS 5 - 8 中都能出色且始终如一地工作。我最近使用 android Pie 升级了像素 2XL,现在我的 Vertical ViewPager 似乎没有响应,然后工作,然后它就像它失去焦点,然后工作。拖动一页,它会移动并迅速回到原始位置。或者只是反弹。同样,类似于上面链接的其他两个问题。
在 Android 9 之前,垂直滚动和分页是完美的。我尝试使用反射并取得了一些成功。它会更好地滑动,并且似乎不会失去焦点。但是,如果我尝试用另一只手滑动它就会停止,或者如果我改变我滑动的位置,它就会停止。这是非常令人困惑的。我已经添加了在运行 Android 9 的设备上复制此问题所需的所有代码。
活动
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);
片段
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;
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));
ViewPager 适配器
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;
活动 XML
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_
android:layout_
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_
android:layout_/>
<LinearLayout
android:id="@+id/fullscreen_content_controls"
style="?metaButtonBarStyle"
android:layout_
android:layout_
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_
android:layout_
android:layout_weight="1"
android:text="@string/dummy_button" />
</LinearLayout>
</FrameLayout>
片段 XML
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_
android:layout_>
<ImageView
android:id="@+id/iv_imagefragment"
android:layout_
android:layout_
android:scaleType="centerCrop"/>
</LinearLayout>
更新:1
https://github.com/youngkaaa/YViewPagerDemo
还有另一个库,它在 Android Pie 上运行非常流畅,但几乎没有软崩溃。此外,它在 API 19 上崩溃。
更新:2
Google 最近发布了带有 androidx 支持库 https://developer.android.com/jetpack/androidx/releases/viewpager2 的 ViewPager2,它支持垂直视图分页器。但是,它仍处于 alpha 版本,并且存在许多已知问题。
【问题讨论】:
您找到解决方案了吗?我遇到了同样的问题。 我没有,还在搜索,我可能很快会再试一次。 我检查了很多 github 演示。所有人都遇到了同样的问题。我也制造了问题。希望他们能尽快回复一些好的解决方案。 大家好!你有解决办法吗?我得到了同样的行为...... 这个演示使用了一个页面转换器。这个链接没用 【参考方案1】:问题似乎在于#swapXY
交换 x 和 y 值的方式。我相信ViewPager
中使用的VelocityTracker
使用#getAxisValue
而不是#getX/#getY
,结果没有交换。但似乎没有办法设置轴值或子类MotionEvent
,所以我没有找到坚持使用简单#swapXY
解决方案的方法。当我检测到未交换的条件时,我分叉了ViewPager
并使用了VelocityTracker.getYVelocity
。
diff --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();
【讨论】:
那么你最后用垂直 ViewPager 解决了速度问题吗?您必须分叉整个 ViewPager,但是,它有效吗?使用setViewPagerObserver
之类的内容复制整个文件没有问题吗?
叉子工作得很好,是的。我不使用setViewPagerObserver
,至少在我的用例中我也没有发现用户有问题。
我最终也分叉了所有代码,并在此处将getXVelocity
更改为android.googlesource.com/platform/frameworks/support/+/… 为getYVelocity
。到目前为止似乎工作正常。【参考方案2】:
你可以试试这个库: VerticalViewPager,这在我的项目中运行良好。
但是这个lib是从v19拷贝过来的,所以有些方法不会存在,可以自己实现。
【讨论】:
请不要只写链接答案。【参考方案3】:在 SO 上花费了大量时间后,尝试了许多 GitHub 图书馆并等待有人回应赏金, 然后我想出了以下解决方案,希望它可以帮助需要它的人。
起初,this 的问题引起了我的注意,我认为那里的大多数答案都很有帮助,因此我投了赞成票。 帮助我的主要答案是link-1 和link-2。
即使我必须做一些小改动来忽略水平滑动 抛出事件。
请尝试此代码并提供您的反馈以进行任何进一步的改进,在此先感谢。快乐编码:)
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);
【讨论】:
这根本不滚动 请分享具体细节,以便我们帮助您解决问题。以上是关于垂直 ViewPager 和 Android Pie 与滑动手势不一致的行为的主要内容,如果未能解决你的问题,请参考以下文章