RecyclerXem phần chụp cuộn ngang ở giữa


89

Tôi đang cố tạo chế độ xem dạng băng chuyền ở đây bằng cách sử dụng RecyclerView, tôi muốn mục này nằm gọn ở giữa màn hình khi cuộn, từng mục một. Tôi đã thử sử dụngrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

nhưng chế độ xem vẫn cuộn trơn tru, tôi cũng đã cố gắng triển khai logic của riêng mình bằng cách sử dụng trình nghe cuộn như vậy:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

Thao tác vuốt từ phải sang trái hiện đang hoạt động tốt, nhưng không phải ngược lại, tôi còn thiếu điều gì ở đây?

Câu trả lời:


161

Với LinearSnapHelper, điều này bây giờ rất dễ dàng.

Tất cả những gì bạn cần làm là:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Cập nhật

Có sẵn kể từ ngày 25.1.0, PagerSnapHelpercó thể giúp đạt được ViewPagerhiệu ứng tương tự. Sử dụng nó như bạn sẽ sử dụng LinearSnapHelper.

Cách giải quyết cũ:

Nếu bạn muốn nó hoạt động tương tự như vậy ViewPager, hãy thử điều này thay thế:

LinearSnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        View centerView = findSnapView(layoutManager);
        if (centerView == null) 
            return RecyclerView.NO_POSITION;

        int position = layoutManager.getPosition(centerView);
        int targetPosition = -1;
        if (layoutManager.canScrollHorizontally()) {
            if (velocityX < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        if (layoutManager.canScrollVertically()) {
            if (velocityY < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        final int firstItem = 0;
        final int lastItem = layoutManager.getItemCount() - 1;
        targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
        return targetPosition;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

Việc thực hiện ở trên chỉ trả lại vị trí bên cạnh mục hiện tại (ở giữa) dựa trên hướng của vận tốc, bất kể độ lớn.

Giải pháp trước đây là giải pháp của bên thứ nhất có trong Thư viện hỗ trợ phiên bản 24.2.0. Có nghĩa là bạn phải thêm cái này vào mô-đun ứng dụng của mình build.gradlehoặc cập nhật nó.

compile "com.android.support:recyclerview-v7:24.2.0"

Điều này không hiệu quả với tôi (nhưng giải pháp của @Albert Vila Calvo đã làm được). Bạn đã thực hiện nó chưa? Chúng ta có phải ghi đè các phương thức từ nó không? Tôi nghĩ rằng cả lớp nên làm tất cả công việc một cách xuất sắc
galaxigirl

Có, do đó câu trả lời. Tuy nhiên, tôi đã không kiểm tra kỹ lưỡng. Để rõ ràng, tôi đã làm điều này với một chiều ngang LinearLayoutManager, nơi tất cả các chế độ xem đều có kích thước bình thường. Không có gì khác ngoài đoạn mã trên là cần thiết.
razzledazzle

@galaxigirl xin lỗi, tôi đã hiểu ý bạn và đã thực hiện một số chỉnh sửa để công nhận điều đó, vui lòng bình luận. Đây là một giải pháp thay thế với LinearSnapHelper.
razzledazzle

Điều này đã làm việc phù hợp với tôi. @galaxigirl ghi đè phương pháp này giúp ích trong đó trình trợ giúp chụp nhanh tuyến tính sẽ bắt kịp mục gần nhất khi cuộn đang giải quyết, không chỉ chính xác mục tiếp theo / trước đó. Cảm ơn rất nhiều vì điều này, razzledazzle
AA_PV

LinearSnapHelper là giải pháp ban đầu phù hợp với tôi, nhưng có vẻ như PagerSnapHelper đã cho phép vuốt hoạt hình tốt hơn.
LeonardoSibela

76

Cập nhật Google I / O 2019

ViewPager2 là ở đây!

Google vừa thông báo tại buổi nói chuyện 'Có gì mới trong Android' (hay còn gọi là 'Bài phát biểu của Android') rằng họ đang làm việc trên một ViewPager mới dựa trên RecyclerView!

Từ các trang trình bày:

Giống như ViewPager, nhưng tốt hơn

  • Di chuyển dễ dàng từ ViewPager
  • Dựa trên RecyclerView
  • Hỗ trợ chế độ từ phải sang trái
  • Cho phép phân trang dọc
  • Thông báo thay đổi tập dữ liệu được cải thiện

Bạn có thể kiểm tra phiên bản mới nhất tại đây và ghi chú phát hành tại đây . Cũng có một mẫu chính thức .

Ý kiến ​​cá nhân: Tôi nghĩ đây là một sự bổ sung thực sự cần thiết. Gần đây tôi đã gặp rất nhiều rắc rối với việc PagerSnapHelper dao động trái phải vô thời hạn - hãy xem tôi đã mở.


Câu trả lời mới (2016)

Bây giờ bạn có thể chỉ cần sử dụng SnapHelper .

Nếu bạn muốn hành vi chụp nhanh căn giữa tương tự như ViewPager thì hãy sử dụng PagerSnapHelper :

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

Ngoài ra còn có một LinearSnapHelper . Tôi đã thử nó và nếu bạn ném bằng năng lượng thì nó sẽ cuộn 2 mục với 1 lần ném. Cá nhân tôi không thích nó, nhưng hãy tự quyết định - thử chỉ mất vài giây.


Câu trả lời gốc (2016)

Sau nhiều giờ thử 3 giải pháp khác nhau được tìm thấy ở đây trong SO, cuối cùng, tôi đã tạo ra một giải pháp bắt chước rất gần với hành vi được tìm thấy trong a ViewPager.

Giải pháp dựa trên giải pháp @eDizzle , mà tôi tin rằng tôi đã cải thiện đủ để nói rằng nó hoạt động gần giống như một ViewPager.

Quan trọng: RecyclerViewchiều rộng các mặt hàng của tôi giống hệt như màn hình. Tôi chưa thử với các kích thước khác. Ngoài ra tôi sử dụng nó với một chiều ngang LinearLayoutManager. Tôi nghĩ rằng bạn sẽ cần phải điều chỉnh mã nếu bạn muốn cuộn dọc.

Ở đây bạn có mã:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.com/a/29171652/4034572

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

        int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

        // views on the screen
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

        // distance we need to scroll
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;

        if (Math.abs(velocityX) < 1000) {
            // The fling is slow -> stay at the current page if we are less than half through,
            // or go to the next page if more than half through

            if (leftEdge > screenWidth / 2) {
                // go to next page
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                // go to next page
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                // stay at current page
                if (velocityX > 0) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
            return true;

        } else {
            // The fling is fast -> go to next page

            if (velocityX > 0) {
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                smoothScrollBy(-scrollDistanceRight, 0);
            }
            return true;

        }

    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);

        // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
        // This code fixes this. This code is not strictly necessary but it improves the behaviour.

        if (state == SCROLL_STATE_IDLE) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;

            if (leftEdge > screenWidth / 2) {
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                smoothScrollBy(scrollDistanceLeft, 0);
            }
        }
    }

}

Thưởng thức!


Rất vui khi nắm bắt được onScrollStateChanged (); nếu không, một lỗi nhỏ sẽ xuất hiện nếu chúng ta vuốt chậm RecyclerView. Cảm ơn!
Mauricio Sartori

Để hỗ trợ lề (android: layout_marginStart = "16dp"), tôi đã thử với int screenwidth = getWidth (); và nó dường như hoạt động.
eigil

AWESOOOMMEEEEEEEEE
raditya gum,

1
@galaxigirl không, tôi chưa thử LinearSnapHelper. Mình chỉ cập nhật đáp án để mọi người biết về lời giải chính thức. Giải pháp của tôi hoạt động mà không có bất kỳ sự cố nào trên ứng dụng của tôi.
Albert Vila Calvo

1
@AlbertVilaCalvo Tôi đã thử LinearSnapHelper. LinearSnapHelper hoạt động với Scroller. Nó cuộn và chụp dựa trên trọng lực và vận tốc bay. Tốc độ cao hơn sẽ có nhiều trang được cuộn hơn. Tôi sẽ không giới thiệu nó cho một hành vi giống như ViewPager.
mipreamble

46

Nếu mục tiêu là làm cho RecyclerViewhành vi bắt chước hành vi của ViewPagernó thì cách tiếp cận khá dễ dàng

RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);

LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
SnapHelper snapHelper = new PagerSnapHelper();
recyclerView.setLayoutManager(layoutManager);
snapHelper.attachToRecyclerView(mRecyclerView);

Bằng cách sử dụng, PagerSnapHelperbạn có thể có được hành vi nhưViewPager


4
Tôi đang sử dụng LinearSnapHelperthay vì PagerSnapHelper và nó làm việc cho tôi
fanjavaid

1
Vâng, không có tác dụng snap trongLinearSnapHelper
Boonya Kitpitak

1
điều này dán mục trên chế độ xem cho phép có thể cuộn
Sam

@BoonyaKitpitak, tôi đã thử LinearSnapHelpervà nó chuyển sang mục trung bình. PagerSnapHelpercản trở để dễ dàng cuộn (ít nhất, một danh sách hình ảnh).
CoolMind

@BoonyaKitpitak bạn có thể cho tôi biết cách thực hiện việc này không, một mục bằng cente có thể nhìn thấy hoàn toàn và các mục bên cạnh hiển thị một nửa?
Rucha Bhatt Joshi

30

Bạn cần sử dụng findFirstVbrokenItemPosition để đi theo hướng ngược lại. Và để phát hiện hướng vuốt theo hướng nào, bạn sẽ cần nhận được vận tốc lướt hoặc sự thay đổi trong x. Tôi tiếp cận vấn đề này từ một góc độ hơi khác so với bạn.

Tạo một lớp mới mở rộng lớp RecyclerView và sau đó ghi đè phương thức ném của RecyclerView như sau:

@Override
public boolean fling(int velocityX, int velocityY) {
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

//these four variables identify the views you see on screen.
    int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
    int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
    View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
    View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);

//these variables get the distance you need to scroll in order to center your views.
//my views have variable sizes, so I need to calculate side margins separately.     
//note the subtle difference in how right and left margins are calculated, as well as
//the resulting scroll distances.
    int leftMargin = (screenWidth - lastView.getWidth()) / 2;
    int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
    int leftEdge = lastView.getLeft();
    int rightEdge = firstView.getRight();
    int scrollDistanceLeft = leftEdge - leftMargin;
    int scrollDistanceRight = rightMargin - rightEdge;

//if(user swipes to the left) 
    if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
    else smoothScrollBy(-scrollDistanceRight, 0);

    return true;
}

ScreenWidth được xác định ở đâu?
ltsai

@Itsai: Rất tiếc! Câu hỏi hay. Đó là một biến tôi đặt ở nơi khác trong lớp. Đó là chiều rộng màn hình của thiết bị tính bằng pixel, không phải pixel mật độ, vì phương thức SmoothScrollBy yêu cầu số pixel bạn muốn nó cuộn theo.
eDizzle

3
@ hậu bị Bạn có thể thay đổi thành getWidth () (renderview.getWidth ()) thay vì screenWidth. screenWidth không hoạt động chính xác khi RecyclerView của nhỏ hơn chiều rộng của màn hình.
VAdaihiep

Tôi đã cố gắng mở rộng RecyclerView nhưng gặp lỗi "Lỗi thổi phồng RecyclerView tùy chỉnh của lớp". Bạn có thể giúp? Cảm ơn

@bEtTyBarnes: Bạn có thể muốn tìm kiếm điều đó trong một nguồn cấp câu hỏi khác. Nó khá nằm ngoài phạm vi với câu hỏi ban đầu ở đây.
eDizzle

6

Chỉ cần thêm paddingmarginvào recyclerViewrecyclerView item:

tái chếXem mục:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentLayout"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_marginLeft="8dp" <!-- here -->
    android:layout_marginRight="8dp" <!-- here  -->
    android:layout_width="match_parent"
    android:layout_height="200dp">

   <!-- child views -->

</RelativeLayout>

người tái chếXem:

<androidx.recyclerview.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp" <!-- here -->
    android:paddingRight="8dp" <!-- here -->
    android:clipToPadding="false" <!-- important!-->
    android:scrollbars="none" />

và đặt PagerSnapHelper:

int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

dp sang px:

public static int dpToPx(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}

kết quả:

nhập mô tả hình ảnh ở đây


Có cách nào để đạt được khoảng đệm thay đổi giữa các mục không? giống như nếu thẻ đầu tiên và cuối cùng không cần phải được căn giữa để hiển thị nhiều thẻ tiếp theo / trước so với các thẻ trung gian được căn giữa?
RamPrasadBismil

2

Giải pháp của tôi:

/**
 * Horizontal linear layout manager whose smoothScrollToPosition() centers
 * on the target item
 */
class ItemLayoutManager extends LinearLayoutManager {

    private int centeredItemOffset;

    public ItemLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setCenteredItemOffset(int centeredItemOffset) {
        this.centeredItemOffset = centeredItemOffset;
    }

    /**
     * ********** Inner Classes **********
     */

    private class Scroller extends LinearSmoothScroller {

        public Scroller(Context context) {
            super(context);
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return ItemLayoutManager.this.computeScrollVectorForPosition(targetPosition);
        }

        @Override
        public int calculateDxToMakeVisible(View view, int snapPreference) {
            return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
        }
    }
}

Tôi chuyển trình quản lý bố cục này cho RecycledView và đặt độ lệch cần thiết để căn giữa các mục. Tất cả các mặt hàng của tôi có cùng chiều rộng nên bù đắp không đổi là ok


0

PagerSnapHelperkhông hoạt động với GridLayoutManagerspanCount> 1, vì vậy giải pháp của tôi trong trường hợp này là:

class GridPagerSnapHelper : PagerSnapHelper() {
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
            velocityX > 0
        } else {
            velocityY > 0
        }
        val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
        return centerPosition +
            if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.