如何在Android上实现嵌套滚动?

56

在支持库 22.1.0 上,安卓系统已经支持嵌套滚动(android 5.0之前)。不幸的是,这个特性并没有得到很好的记录。它包含两个接口(NestedScrollingParentNestedScrollingChild)以及两个辅助代理类(NestedScrollingChildHelperNestedScrollingParentHelper)。

有人在安卓上使用过嵌套滚动吗?

我尝试设置一个小例子,在其中使用了NestedScrollView,它同时实现了NestedScrollingParentNestedScrollingChild接口。

我的布局看起来像这样:

<android.support.v4.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">

    <View
        android:id="@+id/header"
        android:layout_width="match_parent" android:layout_height="100dp"
        android:background="#AF1233"/>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/child"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >

      <FrameLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#12AF33"
            android:text="@string/long_text"/>

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

  </LinearLayout>

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

我想在一个NestedScrollView(id = parent)中显示一个header view和另一个NestedScrollView(id = child)。

我的想法是使用一个OnPredrawListener来动态调整子滚动视图的高度:

public class MainActivity extends Activity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final NestedScrollView parentScroll = (NestedScrollView) findViewById(R.id.parent);
    final NestedScrollView nestedScroll = (NestedScrollView) findViewById(R.id.child);
    parentScroll.setNestedScrollingEnabled(false);
    final View header = findViewById(R.id.header);

    parentScroll.getViewTreeObserver()
        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
          @Override public boolean onPreDraw() {
            if (parentScroll.getHeight() > 0) {
              parentScroll.getViewTreeObserver().removeOnPreDrawListener(this);
              nestedScroll.getLayoutParams().height = parentScroll.getHeight() - 40;
              nestedScroll.setLayoutParams(nestedScroll.getLayoutParams());
              nestedScroll.invalidate();
              return false;
            }
            return true;
          }
        });

  }
}
因此,标题视图将被部分滚动,因为我将嵌套子滚动视图的高度设置为parentScroll.getHeight() - 40,其中40个像素会保持可见。 好的,运行时设置高度并滚动父滚动视图按预期工作(标题滚出,剩下的40个像素可见,然后子滚动视图填充标题以下屏幕的其余部分)。
我希望“嵌套滚动”意味着我可以在屏幕上的任何位置进行滚动手势(由父滚动视图捕获的触摸事件),如果父滚动视图已达到末尾,则嵌套子滚动视图开始滚动。 但是似乎并非如此(对于简单的滚动手势或快速滑动手势都不是如此)。
如果触摸事件在嵌套子滚动视图的边界内开始,则始终由嵌套子滚动视图处理触摸事件,否则由父滚动视图处理。
这是否是“嵌套滚动”的预期行为,还是有更改该行为的选项?
我还尝试用NestedRecyclerView替换了嵌套子滚动视图。 我子类化RecyclerView并实现NestedScrollingChild,其中我将所有方法委托给NestedScrollingChildHelper
public class NestedRecyclerView extends RecyclerView implements NestedScrollingChild {

  private final NestedScrollingChildHelper scrollingChildHelper =
      new NestedScrollingChildHelper(this);


  public void setNestedScrollingEnabled(boolean enabled) {
    scrollingChildHelper.setNestedScrollingEnabled(enabled);
  }

  public boolean isNestedScrollingEnabled() {
    return scrollingChildHelper.isNestedScrollingEnabled();
  }

  public boolean startNestedScroll(int axes) {
    return scrollingChildHelper.startNestedScroll(axes);
  }

  public void stopNestedScroll() {
    scrollingChildHelper.stopNestedScroll();
  }

  public boolean hasNestedScrollingParent() {
    return scrollingChildHelper.hasNestedScrollingParent();
  }

  public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
      int dyUnconsumed, int[] offsetInWindow) {

    return scrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed,
        dyUnconsumed, offsetInWindow);
  }

  public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    return scrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
  }

  public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    return scrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
  }

  public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    return scrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
  }
}

但是NestedRecyclerView根本无法滚动。 所有触摸事件都被父滚动视图捕获。


我面临同样的问题并认为这个小部件应该得到改进,以允许父级将未消耗的滚动事件传递给其子级。 - Morty Choi
我认为将RecyclerView制作为一个NestedScrollingChild并不容易,因为它使用LayoutManager来滚动和定位其项。因此,仅实现带有NestedScrollingChildHelper的NestedScrollingChild是无法正常工作的,因为这些函数在滚动期间不被调用。 - Morty Choi
如果您检查RecyclerView的源代码,在onTouchEvent方法中,没有调用那些NestedScrollingChild方法,而NestedScrollView有。 - Morty Choi
如果你只想实现从父视图开始触摸事件并继续滚动到子视图,你可以在OnInterceptionTouchEvent中偏移运动事件,使触摸事件似乎从子滚动视图开始。然后在onPreNestedScroll方法中,你消耗dy直到父滚动视图的scrollY达到一定限制。通过使用这个技巧,记得在不再希望在OnNestedPreScroll上消耗任何dy时,在OnInterceptionTouchEvent中消耗应用于运动事件的额外偏移量。 - Morty Choi
你可以在这里更好地理解:https://dev59.com/SYXca4cB1Zd3GeqPJ41I - K K
显示剩余4条评论
1个回答

4

我花了很多时间研究安卓代码,试图弄清楚NestedScrollView中发生了什么。以下内容应该有效。

public abstract class ParentOfNestedScrollView extends NestedScrollView{

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

    /* 
    Have this return the range you want to scroll to until the 
    footer starts scrolling I have it as headerCard.getHeight() 
    on most implementations
    */
    protected abstract int getScrollRange();

    /*
    This has the parent do all the scrolling that happens until 
    you are ready for the child to scroll.
    */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
        if (dy > 0 && getScrollY() < getScrollRange()) {
            int oldScrollY = getScrollY();
            scrollBy(0, dy);
            consumed[1] = getScrollY() - oldScrollY;
        }
    }

    /*
    Sometimes the parent scroll intercepts the event when you don't
    want it to.  This prevents this from ever happening.
    */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
}

我的一些代码是从这个问题中借鉴的。 我只是根据需要扩展了这个类。 我的xml将子项作为NestedScrollView的子项,父项作为此项。 这不太好处理flings,仍在进行中。


我在这个问题上浪费了一天时间,你的解决方案救了我,谢谢! - Stefano Giacone

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