簡體   English   中英

如何在 ViewPager2 中禁用特定方向的滑動

[英]How to disable swiping in specific direction in ViewPager2

我想在ViewPager2禁用從右向左滑動。 我的導航抽屜中基本上有一個帶有 2 頁的 viewpager2 元素。 我希望我的第二頁僅在我單擊第一頁中的某個元素時顯示(從第一頁從右向左滑動不應打開第二頁),而當我在第二頁中時,viewpager2 滑動(向左向右滑動)應該像在 viewpager 中那樣滑動。

我已經嘗試擴展ViewPager2類並覆蓋觸摸事件,但不幸的是ViewPager2是最終類,所以我無法擴展它。

其次,我嘗試使用setUserInputEnabled方法設置為 false,但這完全禁用了所有滑動(我只想禁用從右到左的滑動)。 如果我能找到一些偵聽器在滑動之前檢查當前頁面並禁用滑動,否則它可能會起作用。

     implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha05'

設置 ViewPager2 的代碼

      ViewPager2 pager = view.findViewById(R.id.pager);
      ArrayList<Fragment> abc = new ArrayList<>();
      abc.add(first);
      abc.add(second);
      navigationDrawerPager.setAdapter(new DrawerPagerAdapter(
                this, drawerFragmentList));
      pager.setAdapter(new FragmentStateAdapter(this), abc);

我找到了一個監聽器,它可以在用戶嘗試滑動時進行監聽,然后它會檢查當前頁面,如果它是第一頁,則禁用用戶輸入,否則默認啟用它。

這是代碼片段

在 Java 中:

pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrollStateChanged(int state) {
        super.onPageScrollStateChanged(state);

        if (state == SCROLL_STATE_DRAGGING && pager.getCurrentItem() == 0) {
            pager.setUserInputEnabled(false);
        } else {
            pager.setUserInputEnabled(true);
        }
    }
});

在科特林:

viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
    override fun onPageScrollStateChanged(state: Int) {
        super.onPageScrollStateChanged(state)

        viewPager.isUserInputEnabled = !(state == SCROLL_STATE_DRAGGING && viewPager.currentItem == 0)
    }
})

由於我的場景只有 2 頁,檢查頁碼對我有好處,但如果我們有超過 2 頁並且我們需要禁用一個特定方向的滑動,我們可以使用onPageScrolled(int position, float positionOffset, int positionOffsetPixels) viewpager2偵聽器,並根據positionpositionOffset的正負值處理所需的場景。

超過 2 個片段的解決方案。 (向下滾動解決方案,最后更新代碼以糾正一些錯誤。)

這個有點棘手。

我認為人們希望在一個方向上禁用滑動的原因,在我看來,是因為沒有辦法在運行時添加 Fragments,同時保持顯示之前 Fragments 的狀態。

所以人們正在做的是他們預加載所有的 Fragment,並使它好像那些沒有顯示的片段根本不存在一樣。

現在,如果團隊讓 Adapter 不受 ViewLifeCycle 的限制,這可以通過使用 ListDiffer 選項輕松解決,這會正確地將更新傳播到 RecyclerView Adapter,但是因為 ViewPager2 的每個 dataSetChanged 都需要一個新的適配器,整個 Fragments 需要重新創建,ListDiffer 對 ViewPager2 沒有影響。

但也許不完全是因為我不確定 ListDiffer 是否能夠識別“位置交換”以保留狀態。

現在,關於建議使用registerOnPageChangeCallback()的答案。

之所以用超過 2 個 Fragment 注冊 OnPageChangeCallback() 沒有用,是因為當這個方法被調用時,做某事已經太晚了,這導致窗口在中途變得無響應,而不是addOnItemTouchListener(); 它能夠在到達視圖之前攔截觸摸。

在某種意義上,阻塞和允許滑動的完整事務將由兩個方法來執行,registerOnPageChangeCallback() 和 addOnItemTouchListener()。

registerOnPageChangeCallback()會告訴我們的適配器哪個方向應該停止工作(通常是從左到右(我會簡單地稱之為“左”))以及在哪個頁面,而addOnItemTouchListener()會告訴視圖在正確的時刻攔截addOnItemTouchListener()朝着我們想要的方向。

問題是要使用該 TouchListener,我們需要訪問 ViewPager2 內部的 RecyclerView。

這樣做的方法是覆蓋FragmentStateAdapteronAttachedToWindow()方法。

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
}

現在附加到 RecyclerView 的正確偵聽器稱為RecyclerView.SimpleOnItemTouchListener() ,問題是偵聽器無法區分“右”和“左”。

public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e)

我們需要混合 2 種行為來獲得想要的結果:

a) rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING

b) e.getX()

我們還需要跟蹤最后一個 x 點,這是因為偵聽器會在rv.getScrollState()變為SCROLL_STATE_DRAGGING之前觸發多次。

解決方案。

我用來從左到右識別的類:

public class DirectionResolver {

    private float previousX = 0;

    public Direction resolve(float newX) {
        Direction directionResult = null;
            float result = newX - previousX;
            if (result != 0) {
                directionResult = result > 0 ? Direction.left_to_right : Direction.right_to_left ;
            }
        previousX = newX;
        return directionResult;
    }

    public enum Direction {
        right_to_left, left_to_right
    }

}

事務后不需要將previousX int歸零,因為resolve()方法在rv.getScrollState()變成SCROLL_STATE_DRAGGING之前至少執行了3次以上

一旦定義了這個類,整個代碼應該是這樣的(在FragmentStateAdapter ):

private final DirectionResolver resolver = new DirectionResolver();


private final AtomicSupplier<DirectionResolver.Direction> directionSupplier = new AtomicSupplier<>();

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {

    recyclerView.addOnItemTouchListener(
            new RecyclerView.SimpleOnItemTouchListener(){
                @Override
                public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {

                    boolean shouldIntercept = super.onInterceptTouchEvent(rv, e);

                    DirectionResolver.Direction direction = directionSupplier.get();

                    if (direction != null) {
                        DirectionResolver.Direction resolved = resolver.resolve(e.getX());
                        if (rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
                            //resolved will never be null if state is already dragging
                            shouldIntercept = resolved.equals(direction);
                        }
                    }
                    return shouldIntercept;
                }
            }
    );

    super.onAttachedToRecyclerView(recyclerView);
}

public void disableDrag(DirectionResolver.Direction direction) {
    Log.println(Log.WARN, TAG, "disableDrag: disabling swipe: " + direction.name());
    directionSupplier.set(() -> direction);
}

public void enableDrag() {
    Log.println(Log.VERBOSE, TAG, "enableDrag: enabling swipe");
    directionSupplier.set(() -> null);
}

如果您問 AtomicSupplier 是什么,它類似於 AtomicReference<>,因此如果您想使用它,它會給出相同的結果。 這個想法是重用相同的SimpleOnItemTouchListener()並且為了做到這一點,我們需要為它提供參數。

我們需要檢查空值,因為供應商將在第一次為空(除非您為其提供初始值)首先將 recyclerView 附加到窗口。

現在正在使用它。

    binding.journalViewPager.registerOnPageChangeCallback(
            new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                        if (conditionToDisableLeftSwipe.at(position)) {
                            adapter.disableDrag(DirectionResolver.Direction.right_to_left);
                        } else {
                            adapter.enableDrag();
                        }
                    }

             }
         }
    );

更新


對 DirectionResolver.class 進行了一些更新以解決 sme 錯誤和更多功能:

private static class DirectionResolver {

    private float previousX = 0;
    private boolean right2left;

    public Direction resolve(float newX) {
        Direction directionResult = null;
        float result = newX - previousX;
        if (result != 0) {
            directionResult = result > 0 ? Direction.left_to_right : Direction.right_to_left;
        } else {
            directionResult = Direction.left_and_right;
        }
        previousX = newX;
        return right2left ? Direction.right_to_left : directionResult;
    }


    public void reset(Direction direction) {
        previousX = direction == Direction.left_to_right ? previousX : 0;
    }

    public void reset() {
        right2left = false;
    }


}

方向枚舉:

public enum Direction {
    right_to_left, left_to_right, left_and_right;

    //Nested RecyclerViews generate a wrong response from the resolve() method in the direction resolver.
    public boolean equals(Direction direction, DirectionResolver resolver) {
        boolean result = direction == left_and_right || super.equals(direction);
        resolver.right2left = !result && direction == left_to_right;
        return result;
    }

}

執行:

@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {

    recyclerView.addOnItemTouchListener(
            new RecyclerView.SimpleOnItemTouchListener(){
                @Override
                public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {

                    boolean shouldIntercept = super.onInterceptTouchEvent(rv, e);

                    Direction direction = directionSupplier.get();

                    if (direction != null) {
                        Direction resolved = resolver.resolve(e.getX());
                        if (rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
                            //resolved will never be null if state is already dragging
                            shouldIntercept = resolved.equals(direction, resolver);
                            resolver.reset(direction);
                        }
                    }

                    return shouldIntercept;
                }
            }
    );

    super.onAttachedToRecyclerView(recyclerView);
}

public void disableDrag(Direction direction) {
    directionSupplier.set(() -> direction);
    resolver.reset();
}

public void enableDrag() {
    directionSupplier.set(() -> null);
}

用:

    binding.journalViewPager.registerOnPageChangeCallback(
            new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                        if (conditionToDisableLeftSwipe.at(position)) {
                            adapter.disableDrag(DirectionResolver.Direction.right_to_left);
                        } else {
                            adapter.enableDrag();
                        }
                    }

             }
         }
    );

覆蓋諸如MotionEvent之類的滑動方法。

超過 2 個片段的最簡單解決方案:

int previousPage = 0;
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels);
        if(position < previousPage){
            pager.setCurrentItem(previousPage, false);
        } else {
            previousPage = position;
        }
    }
});

擴展 viewpager 類並覆蓋onInterceptTouchEventonTouchEvent函數。 然后確定滑動的方向,如果不想滑動則返回false。

您可以使用此輔助方法進行滑動檢測:

float downX;  // define class level variable in viewpager 
private boolean wasSwipeToLeftEvent(MotionEvent event){
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            return false;

        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
            return event.getX() - downX > 0;

        default:
            return false;
    }
}

然后在您的觸摸事件方法中:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {


    return !this.wasSwipeToLeftEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    return return !this.wasSwipeToLeftEvent(event);
}

我修改了這個答案中的代碼,如果您需要更多解釋,請參閱: https : //stackoverflow.com/a/34111034/4428159

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM