เหวี่ยงด้วย RecyclerView + AppBarLayout


171

ฉันกำลังใช้ CoordinatorLayout ใหม่กับ AppBarLayout และ CollapsingToolbarLayout ด้านล่าง AppBarLayout ฉันมี RecyclerView พร้อมรายการเนื้อหา

ฉันได้ตรวจสอบแล้วว่าการเลื่อน Fling ทำงานบน RecyclerView เมื่อฉันเลื่อนรายการขึ้นและลง อย่างไรก็ตามฉันอยากให้ AppBarLayout เลื่อนไปมาอย่างราบรื่นในระหว่างการขยาย

เมื่อเลื่อนขึ้นเพื่อขยาย CollaspingToolbarLayout การเลื่อนจะหยุดทันทีเมื่อยกนิ้วของคุณออกจากหน้าจอ หากคุณเลื่อนขึ้นอย่างรวดเร็วบางครั้ง CollapsingToolbarLayout ก็จะยุบอีกครั้งเช่นกัน พฤติกรรมนี้กับ RecyclerView ดูเหมือนว่าจะทำงานแตกต่างจากเมื่อใช้ NestedScrollView มาก

ฉันพยายามตั้งค่าคุณสมบัติการเลื่อนที่แตกต่างกันบน recyclerview แต่ฉันไม่สามารถเข้าใจได้

นี่คือวิดีโอที่แสดงปัญหาการเลื่อนบางส่วน https://youtu.be/xMLKoJOsTAM

นี่คือตัวอย่างที่แสดงปัญหาของ RecyclerView (CheeseDetailActivity) https://github.com/tylerjroach/cheesesquare

นี่คือตัวอย่างดั้งเดิมที่ใช้ NestedScrollView จาก Chris Banes https://github.com/chrisbanes/cheesesquare


ฉันประสบปัญหาเดียวกันนี้ (ฉันใช้กับ RecyclerView) หากคุณดูรายชื่อแอป Google Play Store สำหรับแอปใด ๆ ดูเหมือนว่าจะทำงานได้อย่างถูกต้องดังนั้นจึงมีทางออกอยู่ที่นั่นแน่นอน
Aneem

เฮ้ Aneem ฉันรู้ว่านี้ไม่ได้เป็นวิธีการแก้ปัญหาที่ยิ่งใหญ่ที่สุด แต่ผมเริ่มทดลองกับห้องสมุดนี้: github.com/ksoichiro/Android-ObservableScrollView โดยเฉพาะอย่างยิ่งในกิจกรรมนี้เพื่อให้ได้ผลลัพธ์ที่ฉันต้องการ: FlexibleSpaceWithImageRecyclerViewActivity.java ขออภัยเกี่ยวกับการสะกดชื่อของคุณก่อนการแก้ไข แก้ไขอัตโนมัติ ..
tylerjroach

2
ปัญหาเดียวกันที่นี่ฉันลงเอยด้วยการหลีกเลี่ยง AppBarLayout
Renaud Cerrato

อ๋อ ฉันได้รับสิ่งที่ฉันต้องการจากห้องสมุด OvservableScrollView ฉันแน่ใจว่ามันจะได้รับการแก้ไขในรุ่นอนาคต
tylerjroach

8
พุ่งเป็นบั๊กปัญหาถูกยกขึ้น (และยอมรับ)
Renaud Cerrato

คำตอบ:


114

คำตอบของคิริลล์ Boyarshinovเกือบถูกต้องแล้ว

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

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }
}

ฉันหวังว่านี่จะช่วยได้


คุณบันทึกวันของฉัน! ดูเหมือนว่าจะทำงานได้ดีอย่างแน่นอน! ทำไมคำตอบของคุณถึงไม่ยอมรับ?
Zordid

9
หากคุณใช้ SwipeRefreshLayout เป็นพาเรนต์ของ recyclerview ของคุณเพียงเพิ่มรหัสนี้: if (target instanceof SwipeRefreshLayout && velocityY < 0) { target = ((SwipeRefreshLayout) target).getChildAt(0); }ก่อน if (target instanceof RecyclerView && velocityY < 0) {
LucasFM

1
+ 1 การวิเคราะห์การแก้ไขนี้ฉันไม่เข้าใจว่าทำไม Google ยังไม่ได้แก้ไข รหัสดูเหมือนจะค่อนข้างง่าย
Gaston Flores

3
สวัสดีวิธีการบรรลุสิ่งเดียวกันกับ appbarlayout และ Nestedscrollview ... ขอบคุณล่วงหน้า ..
Harry Sharma

1
มันไม่ทำงานสำหรับฉัน = / โดยวิธีการที่คุณไม่จำเป็นต้องย้ายชั้นเรียนไปยังแพ็คเกจการสนับสนุนเพื่อให้บรรลุมันคุณสามารถลงทะเบียน DragCallback ในตัวสร้าง
Augusto Carmo

69

ดูเหมือนว่าการv23อัปเดตยังไม่สามารถแก้ไขได้

ฉันพบแฮ็คประเภทหนึ่งเพื่อแก้ไขด้วยการเหวี่ยงลง เคล็ดลับคือการพิจารณาเหตุการณ์การขว้างปาอีกครั้งหากลูกรองของ ScrollingView อยู่ใกล้กับจุดเริ่มต้นของข้อมูลในอะแดปเตอร์

public final class FlingBehavior extends AppBarLayout.Behavior {

    public FlingBehavior() {
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (target instanceof ScrollingView) {
            final ScrollingView scrollingView = (ScrollingView) target;
            consumed = velocityY > 0 || scrollingView.computeVerticalScrollOffset() > 0;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }
}

ใช้ในเลย์เอาต์ของคุณเช่นนั้น:

 <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="your.package.FlingBehavior">
    <!--your views here-->
 </android.support.design.widget.AppBarLayout>

แก้ไข: Fling reconsuming เหตุการณ์ตอนนี้ขึ้นอยู่กับแทนของจำนวนเงินของรายการบนจากด้านบนของverticalScrollOffsetRecyclerView

EDIT2:ตรวจสอบเป้าหมายเป็นเช่นอินเตอร์เฟซแทนScrollingView RecyclerViewทั้งคู่RecyclerViewและNestedScrollingViewนำไปใช้งาน


การรับประเภทสตริงไม่ได้รับอนุญาตสำหรับข้อผิดพลาด
layout_behavior

ฉันทดสอบและใช้งานได้ดีกว่าคน! แต่วัตถุประสงค์ของ TOP_CHILD_FLING_THRESHOLD คืออะไร แล้วทำไมมันถึงเป็น 3
Julio_oa

@Julio_oa TOP_CHILD_FLING_THRESHOLD หมายความว่าเหตุการณ์การโยนจะได้รับการพิจารณาใหม่หากมุมมองของนักรีไซเคิลถูกเลื่อนไปยังองค์ประกอบที่ตำแหน่งอยู่ต่ำกว่าค่าเกณฑ์นี้ Btw ฉันปรับปรุงคำตอบให้ใช้verticalScrollOffsetซึ่งเป็นเรื่องทั่วไปมากขึ้น ตอนนี้เหตุการณ์การขว้างปาจะถูกพิจารณาใหม่เมื่อrecyclerViewเลื่อนไปด้านบน
Kirill Boyarshinov

สวัสดีวิธีการบรรลุสิ่งเดียวกันกับ appbarlayout และ Nestedscrollview ... ขอบคุณล่วงหน้า ..
Harry Sharma

2
การเปลี่ยนแปลง @Hardeep target instanceof RecyclerViewไปหรือมากขึ้นสำหรับกรณีทั่วไปtarget instanceof NestedScrollView target instanceof ScrollingViewฉันอัพเดตคำตอบแล้ว
Kirill Boyarshinov

15

ฉันพบวิธีแก้ไขแล้วโดยใช้ OnScrollingListener กับ recyclerView ตอนนี้มันใช้งานได้ดีมาก ปัญหาคือว่า recyclerview ให้ค่าการบริโภคที่ไม่ถูกต้องและพฤติกรรมไม่ทราบว่าเมื่อ recyclerview เลื่อนไปด้านบน

package com.singmak.uitechniques.util.coordinatorlayout;

import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by maksing on 26/3/2016.
 */
public final class RecyclerViewAppBarBehavior extends AppBarLayout.Behavior {

    private Map<RecyclerView, RecyclerViewScrollListener> scrollListenerMap = new HashMap<>(); //keep scroll listener map, the custom scroll listener also keep the current scroll Y position.

    public RecyclerViewAppBarBehavior() {
    }

    public RecyclerViewAppBarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     *
     * @param coordinatorLayout
     * @param child The child that attached the behavior (AppBarLayout)
     * @param target The scrolling target e.g. a recyclerView or NestedScrollView
     * @param velocityX
     * @param velocityY
     * @param consumed The fling should be consumed by the scrolling target or not
     * @return
     */
    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (target instanceof RecyclerView) {
            final RecyclerView recyclerView = (RecyclerView) target;
            if (scrollListenerMap.get(recyclerView) == null) {
                RecyclerViewScrollListener recyclerViewScrollListener = new RecyclerViewScrollListener(coordinatorLayout, child, this);
                scrollListenerMap.put(recyclerView, recyclerViewScrollListener);
                recyclerView.addOnScrollListener(recyclerViewScrollListener);
            }
            scrollListenerMap.get(recyclerView).setVelocity(velocityY);
            consumed = scrollListenerMap.get(recyclerView).getScrolledY() > 0; //recyclerView only consume the fling when it's not scrolled to the top
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    private static class RecyclerViewScrollListener extends RecyclerView.OnScrollListener {
        private int scrolledY;
        private boolean dragging;
        private float velocity;
        private WeakReference<CoordinatorLayout> coordinatorLayoutRef;
        private WeakReference<AppBarLayout> childRef;
        private WeakReference<RecyclerViewAppBarBehavior> behaviorWeakReference;

        public RecyclerViewScrollListener(CoordinatorLayout coordinatorLayout, AppBarLayout child, RecyclerViewAppBarBehavior barBehavior) {
            coordinatorLayoutRef = new WeakReference<CoordinatorLayout>(coordinatorLayout);
            childRef = new WeakReference<AppBarLayout>(child);
            behaviorWeakReference = new WeakReference<RecyclerViewAppBarBehavior>(barBehavior);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            dragging = newState == RecyclerView.SCROLL_STATE_DRAGGING;
        }

        public void setVelocity(float velocity) {
            this.velocity = velocity;
        }

        public int getScrolledY() {
            return scrolledY;
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            scrolledY += dy;

            if (scrolledY <= 0 && !dragging && childRef.get() != null && coordinatorLayoutRef.get() != null && behaviorWeakReference.get() != null) {
                //manually trigger the fling when it's scrolled at the top
                behaviorWeakReference.get().onNestedFling(coordinatorLayoutRef.get(), childRef.get(), recyclerView, 0, velocity, false);
            }
        }
    }
}

ขอบคุณสำหรับการโพสต์ของคุณ ฉันได้ลองคำตอบทั้งหมดในหน้านี้ & จากประสบการณ์ของฉันนี่คือคำตอบที่มีประสิทธิภาพที่สุด แต่ RecylerView ในเลย์เอาต์ของฉันเลื่อนภายในก่อนที่ AppBarLayout จะเลื่อนหน้าจอออกหากฉันไม่เลื่อน RecyclerView ด้วยแรงมากพอ กล่าวอีกนัยหนึ่งเมื่อฉันเลื่อน RecyclerView ด้วยแรงเพียงพอที่ AppBar เลื่อนออกจากหน้าจอโดยไม่มีการเลื่อน RecyclerView ภายใน แต่เมื่อฉันไม่เลื่อน RecyclerView ด้วยแรงพอที่จะเลื่อน RecyclerView ภายในก่อนที่ AppbarLayout จะเลื่อนออกจากหน้าจอ คุณรู้หรือไม่ว่าอะไรทำให้เกิดสิ่งนั้น
คาห์ซิมมอนส์

recyclerview ยังคงได้รับเหตุการณ์การสัมผัสนั่นคือสาเหตุที่มันยังคงเลื่อนพฤติกรรมใน NestedFling จะทำให้เคลื่อนไหวเพื่อเลื่อน appbarLayout ในเวลาเดียวกัน บางทีคุณสามารถลองแทนที่ InterceptTouch ในพฤติกรรมเพื่อเปลี่ยนสิ่งนี้ สำหรับฉันแล้วพฤติกรรมปัจจุบันเป็นที่ยอมรับจากสิ่งที่ฉันเห็น (ไม่แน่ใจว่าเราเห็นสิ่งเดียวกัน)
หมากสิงห์

@ MakSing เป็นประโยชน์อย่างมากกับการติดตั้งCoordinatorLayoutและViewPagerขอบคุณมากสำหรับวิธีแก้ปัญหาที่รอคอยมากที่สุดนี้ โปรดเขียน GIST ให้เหมือนกันเพื่อที่ devs อื่น ๆ จะได้ประโยชน์จากมันเช่นกัน ฉันกำลังแบ่งปันโซลูชันนี้ด้วย ขอบคุณอีกครั้ง.
Nitin Misra

1
@ MakSing ปิดการแก้ปัญหาทั้งหมดนี้ได้ผลดีที่สุดสำหรับฉัน ฉันปรับความเร็วที่ส่งมอบให้กับ onFundFling ความเร็วเล็กน้อย * 0.6f ... ดูเหมือนว่าจะทำให้การไหลดีขึ้น
saberrider

ได้ผลสำหรับฉัน @MakSing คุณใช้วิธี onScrolled ในการเรียกใช้การหักบัญชีของ AppBarLayout.Behavior และไม่ใช่ RecyclerViewAppBarBehavior หรือไม่ ดูเหมือนจะแปลกสำหรับฉัน
Anton Malmygin

13

ได้รับการแก้ไขตั้งแต่การออกแบบที่สนับสนุน 26.0.0

compile 'com.android.support:design:26.0.0'

2
สิ่งนี้ต้องเลื่อนขึ้น นี่คือคำอธิบายที่นี่ในกรณีที่ใครสนใจรายละเอียด
Chris Dinon

1
ตอนนี้ดูเหมือนจะมีปัญหากับแถบสถานะซึ่งเมื่อคุณเลื่อนลงแถบสถานะจะลดลงเล็กน้อยเมื่อเลื่อน ... มันน่ารำคาญสุด ๆ !
กล่อง

2
@Xiaozou ฉันใช้ 26.1.0 และยังมีปัญหากับการเหวี่ยง การพุ่งอย่างรวดเร็วบางครั้งอาจส่งผลให้เกิดการเคลื่อนไหวตรงกันข้าม (ความเร็วของการเคลื่อนที่นั้นตรงกันข้าม / ผิดที่สามารถเห็นได้ในวิธี ทำซ้ำใน Xiaomi Redmi Note 3 และ Galaxy S3
dor506

@ dor506 stackoverflow.com/a/47298312/782870 ฉันไม่แน่ใจว่าเรามีปัญหาเดียวกันเมื่อคุณพูดผลลัพธ์การเคลื่อนไหวที่ตรงกันข้าม แต่ฉันโพสต์คำตอบที่นี่ หวังว่ามันจะช่วยให้ :)
Vida


4

มันเป็นข้อผิดพลาด recyclerview มันควรจะได้รับการแก้ไขใน v23.1.0

ดูhttps://code.google.com/p/android/issues/detail?id=177729

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


9
v23.4.0 - ยังไม่แก้ไข
Arthur

3
ยังไม่ได้รับการแก้ไขใน v25.1.0
0xcaff

v25.3.1 ยังดูไม่ดี
Bawa

1
มันได้รับการแก้ไขในที่สุดv26.0.1!
Micer

2

นี่คือเลย์เอาต์ของฉันและการเลื่อนทำงานได้ตามที่ควร

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:id="@+id/container">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbarLayout"
    android:layout_height="192dp"
    android:layout_width="match_parent">

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/ctlLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"
        app:contentScrim="?attr/colorPrimary"
        app:layout_collapseMode="parallax">

        <android.support.v7.widget.Toolbar
            android:id="@+id/appbar"
            android:layout_height="?attr/actionBarSize"
            android:layout_width="match_parent"
            app:layout_scrollFlags="scroll|enterAlways"
            app:layout_collapseMode="pin"/>

    </android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/catalogueRV"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</android.support.design.widget.CoordinatorLayout>

2

ทางออกของฉันเพื่อให้ห่างไกลบนพื้นฐานของหมากสิงห์และManolo การ์เซียตอบ

มันไม่สมบูรณ์แบบโดยสิ้นเชิง ตอนนี้ฉันไม่ทราบวิธีการคำนวณความเร็ววาลลีย์เพื่อหลีกเลี่ยงเอฟเฟกต์แปลกใหม่: แอปพลิเคชันสามารถขยายได้เร็วกว่าความเร็วการเลื่อน แต่ไม่สามารถเข้าถึงสถานะที่มีแถบเลื่อนที่ขยายเพิ่มและมุมมองรีไซเคิลที่เลื่อน

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

import java.lang.ref.WeakReference;

public class FlingAppBarLayoutBehavior
        extends AppBarLayout.Behavior {

    // The minimum I have seen for a dy, after the recycler view stopped.
    private static final int MINIMUM_DELTA_Y = -4;

    @Nullable
    RecyclerViewScrollListener mScrollListener;

    private boolean isPositive;

    public FlingAppBarLayoutBehavior() {
    }

    public FlingAppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean callSuperOnNestedFling(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            float velocityX,
            float velocityY,
            boolean consumed) {
        return super.onNestedFling(
                coordinatorLayout,
                child,
                target,
                velocityX,
                velocityY,
                consumed
        );
    }

    @Override
    public boolean onNestedFling(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            float velocityX,
            float velocityY,
            boolean consumed) {

        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }

        if (target instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) target;

            if (mScrollListener == null) {
                mScrollListener = new RecyclerViewScrollListener(
                        coordinatorLayout,
                        child,
                        this
                );
                recyclerView.addOnScrollListener(mScrollListener);
            }

            mScrollListener.setVelocity(velocityY);
        }

        return super.onNestedFling(
                coordinatorLayout,
                child,
                target,
                velocityX,
                velocityY,
                consumed
        );
    }

    @Override
    public void onNestedPreScroll(
            CoordinatorLayout coordinatorLayout,
            AppBarLayout child,
            View target,
            int dx,
            int dy,
            int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    private static class RecyclerViewScrollListener
            extends RecyclerView.OnScrollListener {

        @NonNull
        private final WeakReference<AppBarLayout> mAppBarLayoutWeakReference;

        @NonNull
        private final WeakReference<FlingAppBarLayoutBehavior> mBehaviorWeakReference;

        @NonNull
        private final WeakReference<CoordinatorLayout> mCoordinatorLayoutWeakReference;

        private int mDy;

        private float mVelocity;

        public RecyclerViewScrollListener(
                @NonNull CoordinatorLayout coordinatorLayout,
                @NonNull AppBarLayout child,
                @NonNull FlingAppBarLayoutBehavior barBehavior) {
            mCoordinatorLayoutWeakReference = new WeakReference<>(coordinatorLayout);
            mAppBarLayoutWeakReference = new WeakReference<>(child);
            mBehaviorWeakReference = new WeakReference<>(barBehavior);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                if (mDy < MINIMUM_DELTA_Y
                        && mAppBarLayoutWeakReference.get() != null
                        && mCoordinatorLayoutWeakReference.get() != null
                        && mBehaviorWeakReference.get() != null) {

                    // manually trigger the fling when it's scrolled at the top
                    mBehaviorWeakReference.get()
                            .callSuperOnNestedFling(
                                    mCoordinatorLayoutWeakReference.get(),
                                    mAppBarLayoutWeakReference.get(),
                                    recyclerView,
                                    0,
                                    mVelocity, // TODO find a way to recalculate a correct velocity.
                                    false
                            );

                }
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            mDy = dy;
        }

        public void setVelocity(float velocity) {
            mVelocity = velocity;
        }

    }

}

คุณสามารถรับความเร็วปัจจุบันของ recyclerView (ณ วันที่ 25.1.0) โดยใช้การสะท้อน: Field viewFlingerField = recyclerView.getClass().getDeclaredField("mViewFlinger"); viewFlingerField.setAccessible(true); Object flinger = viewFlingerField.get(recyclerView); Field scrollerField = flinger.getClass().getDeclaredField("mScroller"); scrollerField.setAccessible(true); ScrollerCompat scroller = (ScrollerCompat) scrollerField.get(flinger); velocity = Math.signum(mVelocity) * Math.abs(scroller.getCurrVelocity());
Nicholas

2

ในกรณีของฉันฉันได้รับปัญหาซึ่งการเหวี่ยงที่RecyclerViewจะไม่เลื่อนอย่างราบรื่นทำให้ติด

นี้เป็นเพราะเหตุผลบางอย่างที่ผมลืมว่าผมได้ใส่ของฉันRecyclerViewNestedScrollViewใน

มันเป็นความผิดพลาดที่ไร้สาระ แต่ฉันใช้เวลาพอที่จะคิดออก ...


1

ฉันเพิ่มมุมมองของความสูง 1dp ภายใน AppBarLayout และใช้งานได้ดีกว่ามาก นี่คือเค้าโครงของฉัน

  <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="com.spof.spof.app.UserBeachesActivity">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.Toolbar
        android:id="@+id/user_beaches_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_alignParentTop="true"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="@style/WhiteTextToolBar"
        app:layout_scrollFlags="scroll|enterAlways" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp" />
</android.support.design.widget.AppBarLayout>


<android.support.v7.widget.RecyclerView
    android:id="@+id/user_beaches_rv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />


มันจะทำงานเฉพาะในกรณีที่คุณเลื่อนขึ้น ไม่ใช่เมื่อคุณเลื่อนลง
Arthur

สำหรับฉันทำงานได้ดีทั้งสองทิศทาง คุณเพิ่มมุมมอง 1dp ภายใน appbarlayout หรือไม่ ฉันทดสอบใน Android lollipop และ kitkat เท่านั้น
Jachumbelechao Unto Mantekilla

ดีฉันยังใช้ CollapsingToolbarLayout ซึ่งล้อมแถบเครื่องมือ ฉันใส่มุมมอง 1dp ไว้ข้างใน มันเป็นเช่นนี้ AppBarLayout -> CollapsingToolbarLayout -> Toolbar + 1dp ดู
Arthur

ฉันไม่รู้ว่ามันทำงานได้ดีกับ CollapsingToolbarLayout หรือไม่ ฉันทดสอบด้วยรหัสนี้เท่านั้น คุณพยายามวางมุมมอง 1dp นอก CollapsingToolbarLayout หรือไม่
Jachumbelechao Unto Mantekilla

ใช่. เลื่อนขึ้นทำงานเลื่อนลงไม่ขยายแถบเครื่องมือ
Arthur

1

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

private int mScrolled;
private int mPreviousDy;
private AppBarLayout mAppBar;

myRecyclerView.addOnScrollListener(new OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mScrolled += dy;
            // scrolled to the top with a little more velocity than a slow scroll e.g. flick/fling.
            // Adjust 10 (vertical change of event) as you feel fit for you requirement
            if(mScrolled == 0 && dy < -10 && mPrevDy < 0) {
                mAppBar.setExpanded(true, true);
            }
            mPreviousDy = dy;
    });

mPrevDy คืออะไร
ARR.s

1

คำตอบที่ได้รับการยอมรับไม่ได้สำหรับผมเพราะผมมีRecyclerViewอยู่ภายในและSwipeRefreshLayout ViewPagerนี่เป็นรุ่นที่ได้รับการปรับปรุงซึ่งจะค้นหา a RecyclerViewในลำดับชั้นและควรใช้กับเลย์เอาต์ใด ๆ :

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (!(target instanceof RecyclerView) && velocityY < 0) {
            RecyclerView recycler = findRecycler((ViewGroup) target);
            if (recycler != null){
                target = recycler;
            }
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    @Nullable
    private RecyclerView findRecycler(ViewGroup container){
        for (int i = 0; i < container.getChildCount(); i++) {
            View childAt = container.getChildAt(i);
            if (childAt instanceof RecyclerView){
                return (RecyclerView) childAt;
            }
            if (childAt instanceof ViewGroup){
                return findRecycler((ViewGroup) childAt);
            }
        }
        return null;
    }
}

1

คำตอบ:มันได้รับการแก้ไขในห้องสมุดสนับสนุน v26

แต่ v26 มีปัญหาในการเหวี่ยง บางครั้ง AppBar กระเด้งกลับมาอีกครั้งแม้ว่าการโยนจะไม่ยากเกินไป

ฉันจะลบเอฟเฟกต์การตีกลับบน Appbar ได้อย่างไร

หากคุณพบปัญหาเดียวกันเมื่ออัปเดตเพื่อรองรับ v26 นี่คือบทสรุปของคำตอบนี้

การแก้ไข : ขยายพฤติกรรมเริ่มต้นของ AppBar และบล็อกการโทรสำหรับ AppBar.Behavior's onNestedPreScroll () และ onNestedScroll () เมื่อแตะ AppBar ขณะที่ NestedScroll ยังไม่หยุดทำงาน


0

Julian Osถูกต้อง

คำตอบของ Manolo Garciaไม่ทำงานหาก recyclerview ต่ำกว่าขีด จำกัด และการเลื่อน คุณต้องเปรียบเทียบoffsetของ recyclerview และvelocity to the distanceไม่ใช่ตำแหน่งรายการ

ฉันทำจาวาเวอร์ชันโดยอ้างอิงถึงรหัส kotlin ของจูเลียนและการสะท้อนการลบ

public final class FlingBehavior extends AppBarLayout.Behavior {

    private boolean isPositive;

    private float mFlingFriction = ViewConfiguration.getScrollFriction();

    private float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
    private final float INFLEXION = 0.35f;
    private float mPhysicalCoeff;

    public FlingBehavior(){
        init();
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init(){
        final float ppi = BaseApplication.getInstance().getResources().getDisplayMetrics().density * 160.0f;
        mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                * 39.37f // inch/meter
                * ppi
                * 0.84f; // look and feel tuning
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {

        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            RecyclerView recyclerView = (RecyclerView) target;

            double distance = getFlingDistance((int) velocityY);
            if (distance < recyclerView.computeVerticalScrollOffset()) {
                consumed = true;
            } else {
                consumed = false;
            }
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

    public double getFlingDistance(int velocity){
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    private double getSplineDeceleration(int velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }

}

ไม่สามารถ resloveBaseApplication
ARR.s

@ ARR.s ขออภัยคุณเพียงแทนที่บริบทของคุณเช่นด้านล่าง
정 성 민

YOUR_CONTEXT.getResources (). getDisplayMetrics (). density * 160.0f;
정성민


0

ด้วยการอ้างอิงถึงปัญหาการติดตามของ Googleมันได้รับการแก้ไขด้วยห้องสมุดสนับสนุนรุ่น Android 26.0.0-beta2

โปรดอัปเดตห้องสมุดสนับสนุน Android เวอร์ชัน 26.0.0-beta2 ของคุณ

หากปัญหาใด ๆ ยังคงมีอยู่โปรดรายงานที่Google ปัญหาติดตามพวกเขาจะเปิดอีกครั้งเพื่อตรวจสอบ


0

การเพิ่มคำตอบอื่นที่นี่เนื่องจากคำตอบข้างต้นไม่ได้ตอบสนองความต้องการของฉันอย่างสมบูรณ์หรือทำงานได้ไม่ดีนัก อันนี้บางส่วนขึ้นอยู่กับความคิดแพร่กระจายที่นี่

ดังนั้นสิ่งนี้จะทำอย่างไร

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

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

AFAIK นี่คือพฤติกรรมที่ระบุ

มีการสะท้อนจำนวนมากที่เกี่ยวข้องและเป็นธรรมเนียมที่ค่อนข้างสวย ยังไม่พบปัญหา มันเขียนไว้ใน Kotlin ด้วย แต่การทำความเข้าใจมันไม่ควรมีปัญหา คุณสามารถใช้ปลั๊กอิน IntelliJ Kotlin เพื่อคอมไพล์ไปยัง bytecode -> และถอดรหัสกลับไปที่ Java หากต้องการใช้งานให้วางไว้ในแพ็คเกจ android.support.v7.widget และตั้งเป็นพฤติกรรมของผู้ประสานงานของ AppBarLayout ของ CoordinatorLayout.LayoutParams ในโค้ด (หรือเพิ่มคอนสตรัคเตอร์ xml ที่เกี่ยวข้องหรือสิ่งอื่น ๆ )

/*
 * Copyright 2017 Julian Ostarek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.v7.widget

import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.widget.ScrollerCompat
import android.view.View
import android.widget.OverScroller

class SmoothScrollBehavior(recyclerView: RecyclerView) : AppBarLayout.Behavior() {
    // We're using this SplineOverScroller from deep inside the RecyclerView to calculate the fling distances
    private val splineOverScroller: Any
    private var isPositive = false

    init {
        val scrollerCompat = RecyclerView.ViewFlinger::class.java.getDeclaredField("mScroller").apply {
            isAccessible = true
        }.get(recyclerView.mViewFlinger)
        val overScroller = ScrollerCompat::class.java.getDeclaredField("mScroller").apply {
            isAccessible = true
        }.get(scrollerCompat)
        splineOverScroller = OverScroller::class.java.getDeclaredField("mScrollerY").apply {
            isAccessible = true
        }.get(overScroller)
    }

    override fun onNestedFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float, consumed: Boolean): Boolean {
        // Making sure the velocity has the correct sign (seems to be an issue)
        var velocityY: Float
        if (isPositive != givenVelocity > 0) {
            velocityY = givenVelocity * - 1
        } else velocityY = givenVelocity

        if (velocityY < 0) {
            // Decrement the velocity to the maximum velocity if necessary (in a negative sense)
            velocityY = Math.max(velocityY, - (target as RecyclerView).maxFlingVelocity.toFloat())

            val currentOffset = (target as RecyclerView).computeVerticalScrollOffset()
            if (currentOffset == 0) {
                super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
                return true
            } else {
                val distance = getFlingDistance(velocityY.toInt()).toFloat()
                val remainingVelocity = - (distance - currentOffset) * (- velocityY / distance)
                if (remainingVelocity < 0) {
                    (target as RecyclerView).addOnScrollListener(object : RecyclerView.OnScrollListener() {
                        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                                recyclerView.post { recyclerView.removeOnScrollListener(this) }
                                if (recyclerView.computeVerticalScrollOffset() == 0) {
                                    super@SmoothScrollBehavior.onNestedFling(coordinatorLayout, child, target, velocityX, remainingVelocity, false)
                                }
                            }
                        }
                    })
                }
                return false
            }
        }
        // We're not getting here anyway, flings with positive velocity are handled in onNestedPreFling
        return false
    }

    override fun onNestedPreFling(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout, target: View?, velocityX: Float, givenVelocity: Float): Boolean {
        // Making sure the velocity has the correct sign (seems to be an issue)
        var velocityY: Float
        if (isPositive != givenVelocity > 0) {
            velocityY = givenVelocity * - 1
        } else velocityY = givenVelocity

        if (velocityY > 0) {
            // Decrement to the maximum velocity if necessary
            velocityY = Math.min(velocityY, (target as RecyclerView).maxFlingVelocity.toFloat())

            val topBottomOffsetForScrollingSibling = AppBarLayout.Behavior::class.java.getDeclaredMethod("getTopBottomOffsetForScrollingSibling").apply {
                isAccessible = true
            }.invoke(this) as Int
            val isCollapsed = topBottomOffsetForScrollingSibling == - child.totalScrollRange

            // The AppBarlayout is collapsed, we'll let the RecyclerView handle the fling on its own
            if (isCollapsed)
                return false

            // The AppbarLayout is not collapsed, we'll calculate the remaining velocity, trigger the appbar to collapse and fling the RecyclerView manually (if necessary) as soon as that is done
            val distance = getFlingDistance(velocityY.toInt())
            val remainingVelocity = (distance - (child.totalScrollRange + topBottomOffsetForScrollingSibling)) * (velocityY / distance)

            if (remainingVelocity > 0) {
                (child as AppBarLayout).addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
                    override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
                        // The AppBarLayout is now collapsed
                        if (verticalOffset == - appBarLayout.totalScrollRange) {
                            (target as RecyclerView).mViewFlinger.fling(velocityX.toInt(), remainingVelocity.toInt())
                            appBarLayout.post { appBarLayout.removeOnOffsetChangedListener(this) }
                        }
                    }
                })
            }

            // Trigger the expansion of the AppBarLayout
            super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, false)
            // We don't let the RecyclerView fling already
            return true
        } else return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)
    }

    override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout?, child: AppBarLayout?, target: View?, dx: Int, dy: Int, consumed: IntArray?) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed)
        isPositive = dy > 0
    }

    private fun getFlingDistance(velocity: Int): Double {
        return splineOverScroller::class.java.getDeclaredMethod("getSplineFlingDistance", Int::class.javaPrimitiveType).apply {
            isAccessible = true
        }.invoke(splineOverScroller, velocity) as Double
    }

}

วิธีการตั้งค่า
ARR.s

0

นี่คือทางออกของฉันในโครงการของฉัน
เพียงหยุด mScroller เมื่อรับ Action_Down

XML:

    <android.support.design.widget.AppBarLayout
        android:id="@+id/smooth_app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:elevation="0dp"
        app:layout_behavior="com.sogou.groupwenwen.view.topic.FixAppBarLayoutBehavior">

FixAppBarLayoutBehavior.java:

    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
        if (ev.getAction() == ACTION_DOWN) {
            Object scroller = getSuperSuperField(this, "mScroller");
            if (scroller != null && scroller instanceof OverScroller) {
                OverScroller overScroller = (OverScroller) scroller;
                overScroller.abortAnimation();
            }
        }

        return super.onInterceptTouchEvent(parent, child, ev);
    }

    private Object getSuperSuperField(Object paramClass, String paramString) {
        Field field = null;
        Object object = null;
        try {
            field = paramClass.getClass().getSuperclass().getSuperclass().getDeclaredField(paramString);
            field.setAccessible(true);
            object = field.get(paramClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return object;
    }

//or check the raw file:
//https://github.com/shaopx/CoordinatorLayoutExample/blob/master/app/src/main/java/com/spx/coordinatorlayoutexample/util/FixAppBarLayoutBehavior.java

0

สำหรับ androidx

หากไฟล์ Manifest ของคุณมีเส้น android: hardwareAccelerated = "false" ให้ลบออก

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