问题
I have a single vertical nestedscrollview that contains a bunch of recyclerview with a horizontal layoutmanager setup. The idea is pretty similar to how the new google play store looks. I'm able to make it functional but it isn't smooth at all. Here are the problems:
1) The horizontal recyclerview item fails to intercept the touch event most of the times even though i tap right on it. The scroll view seems to take precedence for most of the motions. It's hard for me to get a hook onto the horizontal motion. This UX is frustrating as I need to try a few times before it works. If you check the play store, it is able to intercept the touch event really well and it just works well. I noticed in the play store the way they set it up is many horizontal recyclerviews inside one vertical recyclerview. No scrollview.
2) The height of the horizontal recyclerviews have to be manually set and there is no easy way to calculate the height of the children elements.
Here is the layout I'm using:
<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" />
This UI pattern is very basic and most likely used in many different apps. I've read many SO's where ppl say it's a bad idea to put a list within a list, but it is a very common and modern UI pattern used all over the place.Think of netflix like interface with a series of horizontal scroll lists inside a vertical list. Isn't there a smooth way to accomplish this?
Example image from the store:
回答1:
So the smooth scrolling issue is fixed now. It was caused by a bug in the NestedScrollView in the Design Support Library (currently 23.1.1).
You can read about the issue and the simple fix here: https://code.google.com/p/android/issues/detail?id=194398
In short, after you performed a fling, the nestedscrollview didn't register a complete on the scroller component and so it needed an additional 'ACTION_DOWN' event to release the parent nestedscrollview from intercepting(eating up) the subsequent events. So what happened was if you tried scrolling your child list(or viewpager), after a fling, the first touch releases the parent NSV bind and the subsequent touches would work. That was making the UX really bad.
Essentially need to add this line on the ACTION_DOWN event of the NSV:
computeScroll();
Here is what I'm using:
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);
}
}
Use this class in place of your nestedscrollview in the xml file, and the child lists should intercept and handle the touch events properly.
Phew, there are actually quite a few bugs like these that makes me want to ditch the design support library altogether and revisit it when its more mature.
回答2:
I've succeded in doing horizontal scrolling in a vertically scrolling parent with a 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 extends 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);
}
Only issue : you must provide a fixed height to the view pager
回答3:
Since falc0nit3 solution doesn't work anymore (currently the project using 28.0.0
version of support library), i have found an another one.
The background reason of the issue is still the same, scrollable view eats on down event by returning true
on the second tap, where it shouldn't, because naturally second tap on the fling view stops scrolling and may be used with next move
event to start opposite scroll
The issue is reproduced as with NestedScrollView
as with RecyclerView
.
My solution is to stop scrolling manually before native view will be able to intercept it in onInterceptTouchEvent
. In this case it won't eat the ACTION_DOWN
event, because it have been stopped already.
So, for 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)
}
}
For 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)
}
}
Despite solution for RecyclerView
looks easy to read, for NestedScrollView
it's a bit complicated.
Unfortunately, there is no clear way to stop scrolling manually in widget, which the only responsibility is to manage scroll (omg). I'm interesting in abortAnimatedScroll()
method, but it is private. It is possible to use reflection to get around it, but for me better is to call method, which calls abortAnimatedScroll()
itself.
Look at onTouchEvent
handling of 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();
}
Basically stopping fling is managed in this method, but a bit later, than we have to call it to fix the bug
Unfortunately due to this we can't just create OnTouchListener
and set it outside, so only inheritance fits the requirements
来源:https://stackoverflow.com/questions/34258496/nestedscrollview-and-horizontal-recyclerview-smooth-scrolling