RecyclerView สแนปเลื่อนแนวนอนตรงกลาง


89

ฉันกำลังพยายามสร้างมุมมองที่เหมือนภาพหมุนที่นี่โดยใช้ RecyclerView ฉันต้องการให้รายการเข้ามาตรงกลางหน้าจอเมื่อเลื่อนทีละรายการ ฉันได้ลองใช้recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

แต่มุมมองยังคงเลื่อนได้อย่างราบรื่นฉันได้พยายามใช้ตรรกะของตัวเองโดยใช้ตัวฟังแบบเลื่อนดังนี้:

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();
                        }
                    }

ตอนนี้การปัดจากขวาไปซ้ายใช้งานได้ดี แต่ในทางกลับกันฉันหายไปไหน

คำตอบ:


164

ด้วยLinearSnapHelperตอนนี้มันง่ายมาก

สิ่งที่คุณต้องทำคือ:

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

อัปเดต

มีให้ตั้งแต่ 25.1.0 PagerSnapHelperสามารถช่วยให้บรรลุViewPagerผลเช่นนี้ ใช้มันตามที่คุณต้องการใช้ไฟล์LinearSnapHelper.

วิธีแก้ปัญหาเก่า:

หากคุณต้องการให้มันทำงานคล้ายกับสิ่งViewPagerนี้ให้ลองทำสิ่งนี้แทน:

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);

การใช้งานข้างต้นจะส่งคืนตำแหน่งที่อยู่ถัดจากรายการปัจจุบัน (ศูนย์กลาง) ตามทิศทางของความเร็วโดยไม่คำนึงถึงขนาด

โซลูชันเดิมเป็นโซลูชันของบุคคลที่หนึ่งที่รวมอยู่ในไลบรารีสนับสนุนเวอร์ชัน 24.2.0 หมายความว่าคุณต้องเพิ่มสิ่งนี้ลงในโมดูลแอปของคุณbuild.gradleหรืออัปเดต

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

สิ่งนี้ไม่ได้ผลสำหรับฉัน (แต่วิธีแก้ปัญหาโดย @ Albert Vila Calvo ทำ) คุณได้ดำเนินการหรือไม่ เราควรจะลบล้างวิธีการจากมันหรือไม่? ฉันคิดว่าชั้นเรียนควรทำงานทุกอย่างนอกกรอบ
galaxigirl

ใช่ดังนั้นคำตอบ ฉันยังไม่ได้ทดสอบอย่างละเอียด เพื่อความชัดเจนฉันทำสิ่งนี้โดยใช้แนวนอนLinearLayoutManagerซึ่งมุมมองทั้งหมดมีขนาดปกติ ไม่มีอะไรอื่นนอกจากต้องการข้อมูลโค้ดด้านบน
razzledazzle

@galaxigirl ขอโทษฉันเข้าใจสิ่งที่คุณหมายถึงและได้ทำการแก้ไขบางอย่างโดยยอมรับว่าโปรดแสดงความคิดเห็น นี่เป็นทางเลือกอื่นกับไฟล์LinearSnapHelper.
razzledazzle

สิ่งนี้เหมาะสำหรับฉัน @galaxigirl การแทนที่วิธีนี้จะช่วยในการที่ตัวช่วยสแน็ปเชิงเส้นจะสแน็ปไปยังรายการที่ใกล้ที่สุดเมื่อการเลื่อนถูกตั้งค่าไม่ใช่แค่รายการถัดไป / ก่อนหน้าเท่านั้น ขอบคุณมากสำหรับสิ่งนี้ razzledazzle
AA_PV

LinearSnapHelper เป็นวิธีการแก้ปัญหาเบื้องต้นที่ใช้ได้ผลสำหรับฉัน แต่ดูเหมือนว่า PagerSnapHelper จะให้ภาพเคลื่อนไหวที่ดีขึ้น
LeonardoSibela

77

การอัปเดต Google I / O 2019

ViewPager2อยู่ที่นี่แล้ว!

Google เพิ่งประกาศในงานเสวนา 'มีอะไรใหม่ใน Android' (aka 'The Android keynote') ว่าพวกเขากำลังทำงานบน ViewPager ใหม่ที่ใช้ RecyclerView!

จากสไลด์:

เหมือน ViewPager แต่ดีกว่า

  • โยกย้ายได้ง่ายจาก ViewPager
  • ขึ้นอยู่กับ RecyclerView
  • รองรับโหมดขวาไปซ้าย
  • อนุญาตการเพจแนวตั้ง
  • ปรับปรุงการแจ้งเตือนการเปลี่ยนแปลงชุดข้อมูล

คุณสามารถตรวจสอบรุ่นล่าสุดที่นี่และปล่อยบันทึกที่นี่ นอกจากนี้ยังมีกลุ่มตัวอย่างเป็นทางการ

ความคิดเห็นส่วนตัว:ฉันคิดว่านี่เป็นส่วนเสริมที่จำเป็นจริงๆ เมื่อเร็ว ๆ นี้ฉันมีปัญหามากมายกับการPagerSnapHelper แกว่งไปทางซ้ายไปเรื่อย ๆ - ดูตั๋วที่ฉันเปิด


คำตอบใหม่ (2016)

ตอนนี้คุณสามารถใช้SnapHelperได้แล้ว

หากคุณต้องการให้พฤติกรรมสแนปชิดตรงกลางคล้ายกับViewPagerให้ใช้PagerSnapHelper :

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

นอกจากนี้ยังมีLinearSnapHelper ฉันได้ลองแล้วและถ้าคุณพุ่งด้วยพลังงานมันจะเลื่อน 2 รายการโดยเหวี่ยง 1 ครั้ง โดยส่วนตัวแล้วฉันไม่ชอบ แต่เพียงแค่ตัดสินใจด้วยตัวเอง - ลองใช้เวลาเพียงไม่กี่วินาที


คำตอบเดิม (2016)

หลังจากใช้เวลาหลายชั่วโมงในการลองใช้วิธีแก้ปัญหาที่แตกต่างกัน 3 วิธีที่พบในที่นี้ในที่สุดฉันก็ได้สร้างโซลูชันที่เลียนแบบพฤติกรรมที่พบในไฟล์ViewPager.

การแก้ปัญหานั้นขึ้นอยู่กับโซลูชัน @eDizzle ซึ่งฉันเชื่อว่าฉันได้ปรับปรุงมากพอที่จะบอกว่ามันทำงานได้เกือบจะเหมือนไฟล์ViewPager.

สำคัญ: RecyclerViewรายการของฉันกว้างเท่ากับหน้าจอทุกประการ ฉันยังไม่ได้ลองขนาดอื่น LinearLayoutManagerนอกจากนี้ผมใช้มันกับแนวนอน ฉันคิดว่าคุณจะต้องปรับรหัสหากคุณต้องการเลื่อนแนวตั้ง

คุณมีรหัสที่นี่:

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);
            }
        }
    }

}

สนุก!


ดีจับ onScrollStateChanged (); มิฉะนั้นข้อผิดพลาดเล็ก ๆ จะปรากฏขึ้นหากเราปัด RecyclerView ช้าๆ ขอบคุณ!
Mauricio Sartori

เพื่อรองรับระยะขอบ (android: layout_marginStart = "16dp") ฉันลองใช้ int screenwidth = getWidth (); และดูเหมือนว่าจะได้ผล
eigil

AWESOOOMMEEEEEEEEE
raditya gumay

1
@galaxigirl ไม่ฉันยังไม่ได้ลอง LinearSnapHelper เลย ฉันเพิ่งอัปเดตคำตอบเพื่อให้ผู้คนทราบเกี่ยวกับโซลูชันอย่างเป็นทางการ โซลูชันของฉันใช้งานได้โดยไม่มีปัญหาใด ๆ ในแอปของฉัน
Albert Vila Calvo

1
@AlbertVilaCalvo ฉันลอง LinearSnapHelper LinearSnapHelper ทำงานร่วมกับ Scroller มันเลื่อนและยึดตามแรงโน้มถ่วงและความเร็วในการพุ่ง ความเร็วในการเลื่อนหน้ามากขึ้น ฉันจะไม่แนะนำสำหรับ ViewPager เช่นพฤติกรรม
mipreamble

47

หากเป้าหมายคือการทำให้RecyclerViewพฤติกรรมเลียนแบบViewPagerมีแนวทางที่ค่อนข้างง่าย

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);

โดยใช้PagerSnapHelperคุณจะได้รับพฤติกรรมเช่นViewPager


4
ฉันใช้LinearSnapHelperแทนPagerSnapHelper และได้ผลสำหรับฉัน
fanjavaid

1
ใช่ไม่มีเอฟเฟกต์สแน็ปอินLinearSnapHelper
บุญญากิจพิทักษ์

1
สิ่งนี้เกาะติดรายการบนมุมมองที่สามารถเลื่อนได้
แซม

@BoonyaKitpitak ผมลองLinearSnapHelperแล้วมันก็ยึดเป็นของกลางๆ PagerSnapHelperขัดขวางการเลื่อนได้อย่างง่ายดาย (อย่างน้อยก็คือรายการรูปภาพ)
CoolMind

@BoonyaKitpitak ช่วยบอกหน่อยได้ไหมว่ามี 1 รายการใน cente มองเห็นได้ชัดและด้านข้างมองเห็นได้ครึ่งหนึ่ง
Rucha Bhatt Joshi

30

คุณต้องใช้ findFirstVisibleItemPosition เพื่อไปในทิศทางตรงกันข้าม และสำหรับการตรวจสอบว่าการปัดไปในทิศทางใดคุณจะต้องได้รับความเร็วในการพุ่งหรือการเปลี่ยนแปลงของ x ฉันเข้าหาปัญหานี้จากมุมที่แตกต่างจากที่คุณมีเล็กน้อย

สร้างคลาสใหม่ที่ขยายคลาส RecyclerView จากนั้นแทนที่วิธีการเหวี่ยงของ RecyclerView ดังนี้:

@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 กำหนดไว้ที่ไหน?
ltsai

@ อิไต: อ๊ะ! คำถามที่ดี. เป็นตัวแปรที่ฉันตั้งไว้ที่อื่นในชั้นเรียน เป็นความกว้างหน้าจอของอุปกรณ์เป็นพิกเซลไม่ใช่พิกเซลความหนาแน่นเนื่องจากเมธอด smoothScrollBy ต้องใช้จำนวนพิกเซลที่คุณต้องการให้เลื่อน
eDizzle

3
@ltsai คุณสามารถเปลี่ยนเป็น getWidth () (Recyclerview.getWidth ()) แทน screenWidth screenWidth ทำงานไม่ถูกต้องเมื่อ RecyclerView มีขนาดเล็กกว่าความกว้างของหน้าจอ
VAdaihiep

ฉันพยายามขยาย RecyclerView แต่ได้รับ "Error inflating class custom RecyclerView" คุณสามารถช่วย? ขอบคุณ

@bEtTyBarnes: คุณอาจต้องการค้นหาสิ่งนั้นในฟีดคำถามอื่น มันค่อนข้างเกินขอบเขตสำหรับคำถามเริ่มต้นที่นี่
eDizzle

6

เพียงแค่เพิ่มpaddingและmarginในrecyclerViewและrecyclerView item:

ดูรายการ:

<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>

ดู:

<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" />

และตั้งค่าPagerSnapHelper:

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

dp เป็น px:

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

ผลลัพธ์:

ป้อนคำอธิบายภาพที่นี่


มีวิธีในการเพิ่มช่องว่างระหว่างรายการหรือไม่? เช่นถ้าไพ่ใบแรกและใบสุดท้ายไม่จำเป็นต้องอยู่ตรงกลางเพื่อแสดงไพ่ถัดไป / ก่อนหน้ามากกว่าเทียบกับไพ่กลางที่อยู่กึ่งกลาง?
RamPrasadBismil

2

วิธีแก้ปัญหาของฉัน:

/**
 * 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;
        }
    }
}

ฉันส่งตัวจัดการเลย์เอาต์นี้ไปที่ RecycledView และตั้งค่าออฟเซ็ตที่จำเป็นเพื่อจัดกึ่งกลางรายการ รายการทั้งหมดของฉันมีความกว้างเท่ากันดังนั้นการชดเชยคงที่ก็โอเค


0

PagerSnapHelper ใช้ไม่ได้กับ GridLayoutManagerกับ spanCount> 1 ดังนั้นวิธีแก้ปัญหาของฉันภายใต้สถานการณ์นี้คือ:

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
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.