NestedScrollView中的Recycler视图导致滚动在中间开始

141

当我在NestedScrollView中添加RecyclerView时,出现了奇怪的滚动行为。

问题是,每当滚动视图的行数超过屏幕可以显示的行数时,一旦启动活动,NestedScrollView就从顶部开始偏移(图1)。如果滚动视图中的项目很少,以至于它们都可以一次性显示,那么这种情况就不会发生(图2)。

我正在使用支持库的23.2.0版本。

图1:错误 - 从顶部开始偏移

Image 1

图2:正确 - RecyclerView中只有少量项目

Image 2

以下是我的布局代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="fill_vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">

            <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="16dp"
                android:orientation="vertical">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Title:"
                    style="@style/TextAppearance.AppCompat.Caption"/>

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:padding="@dimen/bodyPadding"
                    style="@style/TextAppearance.AppCompat.Body1"
                    android:text="Neque porro quisquam est qui dolorem ipsum"/>

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Subtitle:"
                    style="@style/TextAppearance.AppCompat.Caption"/>

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    style="@style/TextAppearance.AppCompat.Body1"
                    android:padding="@dimen/bodyPadding"
                    android:text="Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."/>

            </LinearLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:focusable="false"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

我错过了什么吗?有人知道如何解决这个问题吗? 更新1 如果在初始化Activity时放置以下代码,则可以正常工作:
sv.post(new Runnable() {
        @Override
        public void run() {
            sv.scrollTo(0,0);
        }
});

这里的 sv 是 NestedScrollView 的引用,但看起来有些不正规。

更新2

根据请求,这是我的适配器代码:

public abstract class ArrayAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {

    private List<T> mObjects;

    public ArrayAdapter(final List<T> objects) {
        mObjects = objects;
    }

    /**
     * Adds the specified object at the end of the array.
     *
     * @param object The object to add at the end of the array.
     */
    public void add(final T object) {
        mObjects.add(object);
        notifyItemInserted(getItemCount() - 1);
    }

    /**
     * Remove all elements from the list.
     */
    public void clear() {
        final int size = getItemCount();
        mObjects.clear();
        notifyItemRangeRemoved(0, size);
    }

    @Override
    public int getItemCount() {
        return mObjects.size();
    }

    public T getItem(final int position) {
        return mObjects.get(position);
    }

    public long getItemId(final int position) {
        return position;
    }

    /**
     * Returns the position of the specified item in the array.
     *
     * @param item The item to retrieve the position of.
     * @return The position of the specified item.
     */
    public int getPosition(final T item) {
        return mObjects.indexOf(item);
    }

    /**
     * Inserts the specified object at the specified index in the array.
     *
     * @param object The object to insert into the array.
     * @param index  The index at which the object must be inserted.
     */
    public void insert(final T object, int index) {
        mObjects.add(index, object);
        notifyItemInserted(index);

    }

    /**
     * Removes the specified object from the array.
     *
     * @param object The object to remove.
     */
    public void remove(T object) {
        final int position = getPosition(object);
        mObjects.remove(object);
        notifyItemRemoved(position);
    }

    /**
     * Sorts the content of this adapter using the specified comparator.
     *
     * @param comparator The comparator used to sort the objects contained in this adapter.
     */
    public void sort(Comparator<? super T> comparator) {
        Collections.sort(mObjects, comparator);
        notifyItemRangeChanged(0, getItemCount());
    }
}

这是我的ViewHolder:

public class ViewHolder extends RecyclerView.ViewHolder {
    private TextView txt;
    public ViewHolder(View itemView) {
        super(itemView);
        txt = (TextView) itemView;
    }

    public void render(String text) {
        txt.setText(text);
    }
}

这里是RecyclerView中每个项目的布局(只是 android.R.layout.simple_spinner_item - 这个屏幕仅用于展示此错误的示例):

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@android:id/text1"
    style="?android:attr/spinnerItemStyle"
    android:singleLine="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:ellipsize="marquee"
    android:textAlignment="inherit"/>

尝试使用android:focusableInTouchMode="false"来处理RecyclerView了吗?显然有些东西正在“强制”你的布局到底部...(当你有1295个项目时,它从哪里开始?底部还是只有一个小的顶部偏移,就像第一个屏幕一样) - snachmsm
1
将android:clipToPadding="true"设置到你的NestedScrollView中。 - natario
我可以通过调用NestedScrollView的post方法来修复它,我已经将其添加到我的问题中。但感觉这是一个不好的hack... - Luccas Correa
@snachmsm,我已经在我的问题中添加了代码,也许这会有所帮助,但我想知道这是否是NestedScrollView实现中的内部错误... - Luccas Correa
1
即使我在使用NestedScrollView内部的RecyclerView时也遇到了同样的问题。这是一种不好的模式,因为Recycler本身的模式不起作用。所有视图将一次性绘制(因为WRAP_CONTENT需要RecyclerView的高度)。后台不会有任何视图回收,因此Recycler View本身的主要目的不起作用。但是,通过使用Recycler View来管理数据和绘制布局很容易,这是您可以使用此模式的唯一原因。因此,最好不要使用它,除非您确实需要它。 - Ashok Varma
显示剩余8条评论
13个回答

261

我通过设置解决了这个问题:

<ImageView ...
android:focusableInTouchMode="true"/>

据我看,我的RecyclerView上方的LinearLayout在不必要的滚动后被隐藏了。尝试将此属性设置为位于RecyclerView上方或包含RecyclerView的LinearLayout中(在另一个案例中对我有所帮助)。

从NestedScrollView源代码中可以看出,在onRequestFocusInDescendants中它尝试将焦点集中在第一个可能的子元素上,如果只有RecyclerView可获得焦点,则RecyclerView获胜。

编辑(感谢Waran):为了平稳滚动,请勿忘记设置yourRecyclerView.setNestedScrollingEnabled(false);


12
顺便提一下,为了让滚动更流畅,您需要添加以下代码:yourRecyclerView.setNestedScrollingEnabled(false); - Mikhail Valuyskiy
1
@Kenji 只能查看放置 RecyclerView 的 LinearLayout 中的第一个视图。如果第一个更改无效,则可以查看该 LinearLayout(RecyclerView 容器)本身。 - Dmitry Gavrilko
1
@DmitryGavrilko 我遇到了一个问题,我的RecycleView在NestedScrollview里面。我无法使用recycleview.scrollToPosition(X);它就是不起作用。我尝试了过去6天中的所有方法,但我无法解决它。你有什么建议吗?如果你能帮忙,我将非常感激! - narancs
3
您可以将 android:focusableInTouchMode="false" 应用到 RecyclerView 上,这样您就不需要对所有其他视图设置为 true。请注意保持原意,同时使文本更加通俗易懂,不得添加额外解释或信息。 - Aksiom
1
同意Dmitry Gavrilko的观点,在包含RecyclerView的LinearLayout中设置android:focusableInTouchMode="true"后,问题得到了解决。但是,对于我来说,将android:focusableInTouchMode="false"设置为RecyclerView并没有起作用。 - Shendre Kiran
显示剩余3条评论

113

在你的LinearLayout中,在NestedScrollView之后立即使用以下方式中的android:descendantFocusability

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp"
        android:descendantFocusability="blocksDescendants">

编辑

由于许多人认为这个答案有用,我将提供解释。

descendantFocusability 的用法在此处给出。至于 focusableInTouchMode,请参见此处。 因此,在descendantFocusability中使用blocksDescendants不允许其子元素在触摸时获得焦点,从而可以防止发生未计划的行为。

对于focusInTouchModeAbsListViewRecyclerView在它们的构造函数中默认调用方法setFocusableInTouchMode(true);,因此在XML布局中不需要使用该属性。

对于NestedScrollView,使用以下方法:

private void initScrollView() {
        mScroller = ScrollerCompat.create(getContext(), null);
        setFocusable(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setWillNotDraw(false);
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }

在这里,使用了setFocusable()方法而不是setFocusableInTouchMode()。但根据这个帖子,除非特定情况下需要,否则应避免使用focusableInTouchMode属性,因为它会破坏Android正常行为的一致性。游戏是可以很好地利用触摸模式下可聚焦属性的一个很好的例子。如果像Google Maps中的全屏使用MapView,则是另一个可以正确使用触摸模式下可聚焦属性的好例子。


谢谢!对于我的情况有效。不幸的是,android:focusableInTouchMode="true" 对我没有起作用。 - Mikhail
1
这对我有用。我在我的recyclerview的父视图(在我的情况下,它是一个LinearLayout)中添加了这行代码,它就像魔法一样奏效了。 - amzer
我使用的是NestedScrollView,接着是包含RecyclerView和EditText的LinearLayout。使用android:descendantFocusability="blocksDescendants"在LinearLayout内固定了滚动行为,但EditText不能获得焦点。你有什么想法吗? - Sagar Chapagain
@SagarChapagain blocksDescendants是阻止所有后代获取焦点的属性。我不确定,但尝试使用beforeDescendants - Jimit Patel
2
@JimitPatel 我的问题已经通过以下方式解决: recyclerView.setFocusable(false); nestedScrollView.requestFocus(); - Sagar Chapagain
显示剩余3条评论

20
android:descendantFocusability="blocksDescendants"

LinearLayout内部对我有效。


6
这是否意味着将会屏蔽内部视图的聚焦点,使用户无��访问它们? - android developer

11

我曾遇到同样的问题,通过扩展NestedScrollView并禁用子项的聚焦,解决了这个问题。不知为何,当我只是打开和关闭抽屉时,RecyclerView总是请求焦点。

public class DummyNestedScrollView extends NestedScrollView {
public DummyNestedScrollView(Context context) {
    super(context);
}

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

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

/**
 * Fixind problem with recyclerView in nested scrollview requesting focus
 * https://dev59.com/Luk6XIcBkEYKwwoYC_j6
 * @param child
 * @param focused
 */
@Override
public void requestChildFocus(View child, View focused) {
    Log.d(getClass().getSimpleName(), "Request focus");
    //super.requestChildFocus(child, focused);

}


/**
 * https://dev59.com/Luk6XIcBkEYKwwoYC_j6
 * @param direction
 * @param previouslyFocusedRect
 * @return
 */
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    Log.d(getClass().getSimpleName(), "Request focus descendants");
    //return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
    return false;
}
}

它能够运作但也会破坏焦点的概念,使得你容易在屏幕上出现两个焦点字段的情况。因此,只有在RecyclerView持有者内没有EditText时才使用它。 - Andrey Chernoprudov

5
在我的情况下,这段代码解决了我的问题。
RecyclerView recyclerView = findViewById(R.id.recyclerView);
NestedScrollView nestedScrollView= findViewById(R.id.nestedScrollView);

recyclerView.setFocusable(false);
nestedScrollView.requestFocus();

//populate recyclerview here

我的布局包含一个父布局作为NestedScrollView,其中有一个子LinearLayout。LinearLayout的方向是“垂直”,它包括RecyclerView和EditText。 参考链接


5

只需将android:descendantFocusability="blocksDescendants"添加到NestedScrollView内部的ViewGroup中即可。


2
我有两个猜测。
第一个:尝试将这行代码放到你的NestedScrollView上。
注:NestedScrollView 是一个可以嵌套滚动视图的 Android 控件。
app:layout_behavior="@string/appbar_scrolling_view_behavior"

第二步: 使用

<android.support.design.widget.CoordinatorLayout

今日免费次数已满, 请开通会员/明日再来
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="fill_vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">

        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                      android:layout_width="match_parent"
                      android:layout_height="wrap_content"
                      android:orientation="vertical"
                      android:padding="16dp">

            <TextView
                style="@style/TextAppearance.AppCompat.Caption"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Title:"/>

            <TextView
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="@dimen/bodyPadding"
                android:text="Neque porro quisquam est qui dolorem ipsum"/>

            <TextView
                style="@style/TextAppearance.AppCompat.Caption"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Subtitle:"/>

            <TextView
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="@dimen/bodyPadding"
                android:text="Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."/>

        </LinearLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:focusable="false"/>

    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

我最后的可能解决方案。我发誓 :)


将CoordinatorLayout作为父视图添加也没有起作用...无论如何还是谢谢。 - Luccas Correa

2
这个问题是由于Recycle View焦点引起的。
如果Recycle View的大小超过了屏幕的大小,所有焦点都会自动转移到Recycle View上。
在第一个ChildView(如TextView、Button等)中添加android:focusableInTouchMode="true"可以解决这个问题,但是API Level 25及以上的解决方案无效。
只需在ChildView(如TextView、Button等)中添加这两行代码即可解决问题(不要在ViewGroup中添加,如Linear、Relative等)。
 android:focusableInTouchMode="true"
 android:focusable="true"

我在API 25级别遇到了这个问题。希望其他人不要浪费时间。

为了使RecycleView平滑滚动,请添加以下行:

 android:nestedScrollingEnabled="false"

但是只有在API 21或更高版本中才能使用这些属性。如果您想让平滑滚动在低于API级别25的情况下工作,则请在您的类中添加以下行:

 mList = findViewById(R.id.recycle_list);
 ViewCompat.setNestedScrollingEnabled(mList, false);

0
请不要使用


android:descendantFocusability="blocksDescendants"

因为ViewGroup会阻止其子元素接收焦点。这是一个无障碍问题。
如果您想避免nestedscrollview接收焦点,您可以在nestedscrollview上方添加一个可获取焦点的虚拟布局。
   <View
       android:layout_width="0dp"
       android:layout_height="0dp"
       android:focusable="true"
       android:focusableInTouchMode="true" />

0
在我的情况下,由于我在父线性布局中添加了android:layout_gravity="center",导致滚动和聚焦到recyclerview。删除后,它可以正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接