垂直 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 与滑动手势不一致的行为的主要内容,如果未能解决你的问题,请参考以下文章

Android 可手动管理并垂直滚动轮播控件

Android 可手动管理并垂直滚动轮播控件

Android 可手动管理并垂直滚动轮播控件

android 卡片画廊效果及RecycleView、ViewPager、ScrollView之前的冲突解决

带有卡片堆栈的 Android 垂直视图寻呼机

android使用Tablayout+viewpager2