NestedScrollView 和 Horizontal RecyclerView 平滑滚动
NestedScrollView and Horizontal RecyclerView Smooth Scrolling
我有一个垂直嵌套滚动视图,其中包含一堆带有水平布局管理器设置的回收视图。这个想法与新 google Play 商店的外观非常相似。我能够让它发挥作用,但它一点也不流畅。以下是问题:
1) 水平的 recyclerview 项目在大多数情况下都无法拦截触摸事件,即使我在其上右击也是如此。滚动视图似乎优先于大多数动作。我很难抓住水平运动的钩子。这个 UX 令人沮丧,因为我需要尝试几次才能起作用。如果你查看 Play 商店,它能够很好地拦截触摸事件,而且效果很好。我在 Play 商店中注意到他们设置它的方式是在一个垂直回收视图中放置许多水平回收视图。没有滚动视图。
2) 水平recyclerviews的高度必须手动设置,并且没有简单的方法来计算子元素的高度。
这是我正在使用的布局:
<android.support.v4.widget.NestedScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:background="@color/dark_bgd"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/main_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="gone"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/starring_list"
android:paddingLeft="@dimen/spacing_major"
android:paddingRight="@dimen/spacing_major"
android:layout_width="match_parent"
android:layout_height="180dp" />
这个 UI 模式非常基本,很可能在许多不同的应用程序中使用。我读过很多 SO,其中有人说将列表放在列表中是个坏主意,但它是一种非常常见且现代的 UI 模式,在 netflix 的 place.Think 中广泛使用,例如界面垂直列表中的一系列水平滚动列表。有没有一个很顺利的方法可以做到?
来自商店的示例图片:
我已经成功地使用 ViewPager 在垂直滚动的父对象中进行了水平滚动:
<android.support.v4.widget.NestedScrollView
...
<android.support.v4.view.ViewPager
android:id="@+id/pager_known_for"
android:layout_width="match_parent"
android:layout_height="350dp"
android:minHeight="350dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:clipToPadding="false"/>
public class UniversityKnownForPagerAdapter 扩展了 PagerAdapter {
public UniversityKnownForPagerAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);
...
container.addView(rootView);
return rootView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
@Override
public int getCount() {
return 4;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
}
唯一问题:您必须为视图分页器提供固定高度
平滑滚动问题现已解决。这是由设计支持库(当前为 23.1.1)中的 NestedScrollView 中的错误引起的。
您可以在此处阅读有关该问题和简单修复的信息:
https://code.google.com/p/android/issues/detail?id=194398
简而言之,在你执行了一个 fling 之后,nestedscrollview 没有在滚动组件上注册一个完整的,所以它需要一个额外的 'ACTION_DOWN' 事件来释放父 nestedscrollview 从拦截(吃掉)随后发生的事件。所以发生的事情是,如果你尝试滚动你的子列表(或 viewpager),在一扔之后,第一次触摸释放父 NSV 绑定,随后的触摸将起作用。这让用户体验变得非常糟糕。
基本上需要在 NSV 的 ACTION_DOWN 事件中添加这一行:
computeScroll();
这是我正在使用的:
public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;
public MyNestedScrollView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
ViewConfiguration config = ViewConfiguration.get(context);
slop = config.getScaledEdgeSlop();
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private float xDistance, yDistance, lastX, lastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
// This is very important line that fixes
computeScroll();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if (xDistance > yDistance) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}
使用此 class 代替 xml 文件中的 nestedscrollview,子列表应正确拦截和处理触摸事件。
呸,实际上有很多这样的错误让我想完全放弃设计支持库并在它更成熟时重新访问它。
由于 falc0nit3 解决方案不再有效(目前该项目使用 28.0.0
版本的支持库),我找到了另一个。
问题的背景原因仍然相同,可滚动视图通过在第二次点击时返回 true
吃掉向下事件,这是不应该的,因为在快速视图上第二次点击自然会停止滚动并且可以与下一个 move
事件一起使用以开始相反的滚动
问题重现为 NestedScrollView
和 RecyclerView
。
我的解决方案是在本机视图能够在 onInterceptTouchEvent
中拦截之前手动停止滚动。在这种情况下,它不会吃掉 ACTION_DOWN
事件,因为它已经停止了。
所以,对于 NestedScrollView
:
class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
NestedScrollView(context, attrs) {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev)
}
}
对于RecyclerView
:
class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
RecyclerView(context, attrs) {
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
this.stopScroll()
}
return super.onInterceptTouchEvent(e)
}
}
尽管 RecyclerView
的解决方案看起来很容易阅读,但 NestedScrollView
的解决方案有点复杂。
不幸的是,没有明确的方法可以停止在小部件中手动滚动,唯一的责任是管理滚动(omg)。我对 abortAnimatedScroll()
方法很感兴趣,但它是私有的。可以使用反射来绕过它,但对我来说更好的方法是调用方法,它调用 abortAnimatedScroll()
本身。
查看 onTouchEvent
对 ACTION_DOWN
的处理:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
Log.i(TAG, "abort animated scroll");
abortAnimatedScroll();
}
基本上停止 fling 是在这个方法中管理的,但是稍后,我们必须调用它来修复错误
不幸的是,由于这个原因,我们不能只创建 OnTouchListener
并将其设置在外部,因此只有继承符合要求
我有一个垂直嵌套滚动视图,其中包含一堆带有水平布局管理器设置的回收视图。这个想法与新 google Play 商店的外观非常相似。我能够让它发挥作用,但它一点也不流畅。以下是问题:
1) 水平的 recyclerview 项目在大多数情况下都无法拦截触摸事件,即使我在其上右击也是如此。滚动视图似乎优先于大多数动作。我很难抓住水平运动的钩子。这个 UX 令人沮丧,因为我需要尝试几次才能起作用。如果你查看 Play 商店,它能够很好地拦截触摸事件,而且效果很好。我在 Play 商店中注意到他们设置它的方式是在一个垂直回收视图中放置许多水平回收视图。没有滚动视图。
2) 水平recyclerviews的高度必须手动设置,并且没有简单的方法来计算子元素的高度。
这是我正在使用的布局:
<android.support.v4.widget.NestedScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:background="@color/dark_bgd"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/main_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="gone"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/starring_list"
android:paddingLeft="@dimen/spacing_major"
android:paddingRight="@dimen/spacing_major"
android:layout_width="match_parent"
android:layout_height="180dp" />
这个 UI 模式非常基本,很可能在许多不同的应用程序中使用。我读过很多 SO,其中有人说将列表放在列表中是个坏主意,但它是一种非常常见且现代的 UI 模式,在 netflix 的 place.Think 中广泛使用,例如界面垂直列表中的一系列水平滚动列表。有没有一个很顺利的方法可以做到?
来自商店的示例图片:
我已经成功地使用 ViewPager 在垂直滚动的父对象中进行了水平滚动:
<android.support.v4.widget.NestedScrollView
...
<android.support.v4.view.ViewPager
android:id="@+id/pager_known_for"
android:layout_width="match_parent"
android:layout_height="350dp"
android:minHeight="350dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:clipToPadding="false"/>
public class UniversityKnownForPagerAdapter 扩展了 PagerAdapter {
public UniversityKnownForPagerAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);
...
container.addView(rootView);
return rootView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
@Override
public int getCount() {
return 4;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
}
唯一问题:您必须为视图分页器提供固定高度
平滑滚动问题现已解决。这是由设计支持库(当前为 23.1.1)中的 NestedScrollView 中的错误引起的。
您可以在此处阅读有关该问题和简单修复的信息: https://code.google.com/p/android/issues/detail?id=194398
简而言之,在你执行了一个 fling 之后,nestedscrollview 没有在滚动组件上注册一个完整的,所以它需要一个额外的 'ACTION_DOWN' 事件来释放父 nestedscrollview 从拦截(吃掉)随后发生的事件。所以发生的事情是,如果你尝试滚动你的子列表(或 viewpager),在一扔之后,第一次触摸释放父 NSV 绑定,随后的触摸将起作用。这让用户体验变得非常糟糕。
基本上需要在 NSV 的 ACTION_DOWN 事件中添加这一行:
computeScroll();
这是我正在使用的:
public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;
public MyNestedScrollView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
ViewConfiguration config = ViewConfiguration.get(context);
slop = config.getScaledEdgeSlop();
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private float xDistance, yDistance, lastX, lastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
// This is very important line that fixes
computeScroll();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if (xDistance > yDistance) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}
使用此 class 代替 xml 文件中的 nestedscrollview,子列表应正确拦截和处理触摸事件。
呸,实际上有很多这样的错误让我想完全放弃设计支持库并在它更成熟时重新访问它。
由于 falc0nit3 解决方案不再有效(目前该项目使用 28.0.0
版本的支持库),我找到了另一个。
问题的背景原因仍然相同,可滚动视图通过在第二次点击时返回 true
吃掉向下事件,这是不应该的,因为在快速视图上第二次点击自然会停止滚动并且可以与下一个 move
事件一起使用以开始相反的滚动
问题重现为 NestedScrollView
和 RecyclerView
。
我的解决方案是在本机视图能够在 onInterceptTouchEvent
中拦截之前手动停止滚动。在这种情况下,它不会吃掉 ACTION_DOWN
事件,因为它已经停止了。
所以,对于 NestedScrollView
:
class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
NestedScrollView(context, attrs) {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev)
}
}
对于RecyclerView
:
class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
RecyclerView(context, attrs) {
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
this.stopScroll()
}
return super.onInterceptTouchEvent(e)
}
}
尽管 RecyclerView
的解决方案看起来很容易阅读,但 NestedScrollView
的解决方案有点复杂。
不幸的是,没有明确的方法可以停止在小部件中手动滚动,唯一的责任是管理滚动(omg)。我对 abortAnimatedScroll()
方法很感兴趣,但它是私有的。可以使用反射来绕过它,但对我来说更好的方法是调用方法,它调用 abortAnimatedScroll()
本身。
查看 onTouchEvent
对 ACTION_DOWN
的处理:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
Log.i(TAG, "abort animated scroll");
abortAnimatedScroll();
}
基本上停止 fling 是在这个方法中管理的,但是稍后,我们必须调用它来修复错误
不幸的是,由于这个原因,我们不能只创建 OnTouchListener
并将其设置在外部,因此只有继承符合要求