NestedScrollView平滑滑动嵌套 Fling
手机屏幕越来越大,android页面布局也越来越复杂,仅仅使用一个listview或scrollview是远远不够的,所以很多情况下需要嵌套滑动
Android的嵌套滑动一直是新手朋友很蛋疼纠结的事,这里就几种解决方式作出自己的见解
1.ListView setHeader
即将页面其余布局放入ListViewHeader中,这是最简单有效的方式,也是Android5.0嵌套机制之前官方建议的实现方式
但是这种方式耦合性太强,一个很直观的例子,一篇文章下面通常需要嵌入评论列表,而评论系统通常是单独的Fragment,反向将布局插入子Fragment中,不直观,也是反逻辑的
并且一个布局中需要多个ListView时,这种方式便无能为力
2.ScrollView + ListView
ListView外层包裹ScrollView,然后将ListView的高度设置为与items的高度总和相同
第一种是重写ListView的onMeasure方法
另一种是通过Adapter遍历所有item,计算ListView的高度,如这种实现方式 http://www.cnblogs.com/zhwl/p/3333585.html
但是,如非必要,请不要这样写
因为这会使ListView的重用机制失效,adapter会一次性创建所有item,如果数据量过大,容易引起OOM
3.重写ScrollView
重写ScrollView的onInterceptTouchEvent方法,拦截子ListView的滑动事件,在需要其滑动时返回false
这种方式符合逻辑,推荐这种方式
//以下方式基于Android最新的嵌套机制
//如果对此机制不了解的,可以看看 http://blog.csdn.net/chen930724/article/details/50307193
4.CoordinatorLayout + RecyclerView
关于RecyclerView:
第一次见到RecyclerView时,就觉得这个东西可以替换所有的数据容器了,拔插式的设计方式,可以适应更复杂的设计方式
不了解的朋友可以去这里看看 http://www.cnblogs.com/shen-hua/p/5818172.html 非常详细
但是RecyclerView并不支持item点击和长按事件,需要在Adapter中自己监听
需要引入V7库,CoordinatorLayout 必须作为父类,并且可以使用V7库中的Toolbar、FloatingActionButton等非常好用的系统控件
更详细的资料见 http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0717/3196.html
但是测试的时候发现,滑动父控件时并没有惯性滑动(Fling)效果,对于我这种强迫症非常不能忍
5.NestedScrollView + RecyclerView
NestedScrollView 是V4库中新添加的 实现了NestedScrollingParent和NestedScrollingChild的控件
网上是有这样的实现方式,但是,实验后很不理想,依旧不能流畅滑动
但是有人通过一行代码解决了,mRecyclerView.setNestedScrollingEnabled(false);
But,千万不要这样做
这和第二种实现方式的情况是类似的,RecycleView会自动扩展高度,Adapter中所有的Item都会创建
6.自己写一个FlingNestedScrollView + RecyclerView
当然,你也可以拉到最下面复制代码
解决的问题:可以自我嵌套,顺畅的滑动效果,SwipeRefreshLayout,ViewPager嵌套均不存在问题
Android新的嵌套滑动机制本质:
1.scroll
滑动发起者是NestedScrollingChild(下简称子控件),有滑动需求时询问NestedScrollingParent(下简称父控件)是否需要消费【(子)dispatchNestedPreScroll->(父)onNestedPreScroll】*
父控件消费后将剩余交还给子控件,子控件消费后再将剩余交给父控件【(子)dispatchNestedScroll->(父)onNestedScroll】
如果你觉得他们问来问去太麻烦,可以省去第二步
2.fling
子控件有惯性滑动需求时询问父控件,父控件选择截获或者不截获【(子)dispatchNestedPreFling->(父)onNestedPreFling】*
然后子控件选择是否消费,不消费再传回父控件【(子)dispatchNestedFling->(父)onNestedFling】
核心便是这8个函数,但是第二步通常不需要,一次交互即可,所以最最核心便只有4个函数,这么捋下来是不是觉得很简单呢
实现思路:
父控件在布局时遍历所有子控件,找到所有实现了NestedScrollingChild的子控件,方便操作
1.scroll
核心函数:nestedScrollBy(int dy)
子控件询问父控件时,父控件判断当前子控件是否能够滑动,能够滑动则不消费,否则父控件滑动自己
2.fling
所有的惯性滑动都由父控件来处理,由ScrollerCompat来计算滑动位置
以下为全部代码,竟可能加了一些注释,只实现了纵向滑动,有需要的也可以自己完善
因为没有作更充分的测试,所以可能存在没有考虑到的bug,希望共同完善吧
package com.simple.carpool.view; import android.content.Context; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ScrollingView; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ScrollerCompat; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * Created by xl on 2016/11/22. */ public class FlingNestedScrollView extends ViewGroup implements NestedScrollingParent, NestedScrollingChild, ScrollingView { private NestedScrollingParentHelper parentHelper; private NestedScrollingChildHelper childHelper; //手势判断 private GestureDetectorCompat mGestureDetector; //惯性滑动计算 private ScrollerCompat scroller; private View nestedScrollingView; private OnScrollChangeListener listener; private final int[] mScrollConsumed = new int[2]; private final int[] mNestedOffsets = new int[2]; private int windowOffsetY = 0; private List<View> nestedScrollingChildList = new ArrayList<>(); private int currentFlingY = 0; public FlingNestedScrollView(Context context) { this(context, null); } public FlingNestedScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FlingNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOverScrollMode(View.OVER_SCROLL_NEVER); parentHelper = new NestedScrollingParentHelper(this); childHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); scroller = ScrollerCompat.create(context); mGestureDetector = new GestureDetectorCompat(getContext(), new MyGestureListener()); } public void setOnScrollChangeListener(OnScrollChangeListener listener) { this.listener = listener; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); View view = getChildAt(0); if (view == null || !(view instanceof ViewGroup)) { throw new IllegalArgumentException("must have one child ViewGroup"); } measureChild(view, widthMeasureSpec, heightMeasureSpec); ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { View view = getChildAt(0); if (view == null || !(view instanceof ViewGroup)) { throw new IllegalArgumentException("must have one child ViewGroup"); } ViewGroup viewGroup = (ViewGroup) view; int parentHeight = getMeasuredHeight(); int top = 0; int width = r - l; for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); LayoutParams layoutParams = child.getLayoutParams(); if (layoutParams.height == LayoutParams.MATCH_PARENT) { layoutParams.height = parentHeight; } else { int childMeasuredHeight = child.getMeasuredHeight(); layoutParams.height = childMeasuredHeight; } child.setLayoutParams(layoutParams); child.layout(0, top, width, top + layoutParams.height); top += layoutParams.height; } viewGroup.layout(0, 0, width, top); } @Override protected void onFinishInflate() { super.onFinishInflate(); View view = getChildAt(0); if (view == null || !(view instanceof ViewGroup)) { throw new IllegalArgumentException("must have one child ViewGroup"); } nestedScrollingChildList.clear(); ViewGroup viewGroup = (ViewGroup) view; getAllNestedChildren(viewGroup); Log.i("carpool", "Children count:" + nestedScrollingChildList.size()); Collections.sort(nestedScrollingChildList, new SortComparator()); } private void onScrollChange(View view, int scroll) { if(listener != null) listener.onScrollChange(view, scroll); } //获取本身滑动高度 private int getScrollHeight() { int height = getHeight() - getPaddingTop() - getPaddingBottom(); int bottom = getChildAt(0).getHeight(); return bottom - height; } //获取所有可以滑动的NestedScrollChild private void getAllNestedChildren(ViewGroup viewGroup) { for (int i = 0, len = viewGroup.getChildCount(); i < len; i++) { View child = viewGroup.getChildAt(i); if(ViewCompat.isNestedScrollingEnabled(child) && child instanceof ScrollingView) { nestedScrollingChildList.add(child); } else if(child instanceof ViewGroup) { getAllNestedChildren((ViewGroup) child); } } } //子view是否显示,不显示则不计算 private boolean isShownInVertical(View view) { if(!view.isShown()) return false; int[] position = new int[2]; view.getLocationOnScreen(position); int screenLeft = position[0]; int screenRight = screenLeft + view.getWidth(); if(screenRight < getWidth() / 3 || screenLeft > getWidth() / 3 * 2) { return false; } return true; } //滑动子view,如果子view正在滑动则返回,否则会引起循环调用,nestedChild会“歘”一下就滑到底部 private void scrollViewYTo(View view, int y) { onScrollChange(view, y); if(view == nestedScrollingView) return; int currentScroll = getViewScrollY(view); view.scrollBy(0, y - currentScroll); } private void scrollViewYBy(View view, int dy) { onScrollChange(view, getViewScrollY(view) + dy); // if(view == nestedScrollingView) return; view.scrollBy(0, dy); } private void scrollYTo(int dy) { onScrollChange(this, dy); super.scrollTo(0, dy); } private void scrollYBy(int dy) { int scrollYTo = getScrollY() + dy; onScrollChange(this, scrollYTo); super.scrollBy(0, dy); } //获取view相对于root的坐标 private int getYWithView(View view, View parent) { int[] position = new int[2]; view.getLocationOnScreen(position); int viewTop = position[1]; parent.getLocationOnScreen(position); int parentTop = position[1]; return viewTop - parentTop; } //获取view相对于root的top private int getTopWithView(View view, View parent) { int parentY = getYWithView(view, parent); return getViewScrollY(parent) + parentY; } //获取子view当前滑动位置 private int getViewScrollY(View view) { if(view instanceof ScrollingView) { return ((ScrollingView)view).computeVerticalScrollOffset(); } else { return view.getScrollY(); } } //------------------------------------scrollBy(因为在滑动过程中,scrollingview不知道自己的最大高度,所以不能使用全局坐标!)---------- //全局滑动,包括子scrollingview //true:消费,false:未消费 private boolean nestedScrollBy(int dy) { if(dy == 0) return true; int scrollY = getScrollY(); int scrollMax = getScrollHeight(); //有正在滑动的子view View currentScrollChild = getCurrentScrollChild(dy); if(currentScrollChild != null) { //如果两者相等,返回false,让其自己消费,以免循环调用 //按逻辑来说,还应该判断currentScrollChild是否是nestedScrollingView的子控件 //但是子控件的scrollBy好像并不会再次触发onPreNestedScroll,暂未发现bug //所以就先这么写吧 if(currentScrollChild == nestedScrollingView) { onScrollChange(currentScrollChild, getViewScrollY(currentScrollChild) + dy); return false; } else { scrollViewYBy(currentScrollChild, dy); return true; } } //将会滑动到子view View nextScrollChild = getNextScrollChild(dy); if(nextScrollChild != null) { scrollYBy(getYWithView(nextScrollChild, this)); return true; } //仅滑动自己 if(dy < 0 && scrollY == 0) return false; if(dy > 0 && scrollY == scrollMax) return false; if(scrollY + dy <= 0) scrollYTo(0); else if(scrollY + dy >= scrollMax) scrollYTo(scrollMax); else scrollYBy(dy); return true; } //获取正在滑动的view private View getCurrentScrollChild(int dy) { for(View child: nestedScrollingChildList) { int childY = getYWithView(child, this); if(childY == 0 && isShownInVertical(child)) { if(canScrollVertically(child, dy)) return child; } } return null; } //获取下一个将会自滑动的view //consumed: index0: parentscroll, index1: viewscroll private View getNextScrollChild(int dy) { View view = null; int viewY = 0; for(View child: nestedScrollingChildList) { int childY = getYWithView(child, this); if(dy > 0 && childY > 0) { view = child; viewY = childY; break; } else if(dy < 0 && childY < 0) { view = child; viewY = childY; } } if(view == null) return null; if(!canScrollVertically(view, dy)) return null; if((dy < 0 && dy - viewY < 0) || (dy > 0 && dy - viewY > 0)) { return view; } return null; } //子View是否能够滑动 private boolean canScrollVertically(View view, int scrollY) { if(scrollY > 0 && ViewCompat.canScrollVertically(view, 1)) { return true; } else if(scrollY < 0 && ViewCompat.canScrollVertically(view, -1)) { return true; } return false; } //-----------------------------------scrollBy(end)------------------------------------------ //惯性滑动 private void flingY(int velocityY) { if (getChildCount() > 0) { currentFlingY = 0; scroller.fling(0, 0, 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); invalidate(); } } @Override public void computeScroll() { if (scroller.computeScrollOffset()) { int y = scroller.getCurrY(); nestedScrollBy(y - currentFlingY); currentFlingY = y; invalidate(); } } @Override public void scrollBy(int x, int y) { nestedScrollBy(y); } //---------------------------NestedScrollParent---------------------------------- @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { Log.i("carpool", "start"); if(!nestedScrollingChildList.contains(target)) { onFinishInflate(); } nestedScrollingView = target; scroller.abortAnimation(); return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { Log.i("carpool", "accept"); parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); } @Override public void onStopNestedScroll(View target) { Log.i("carpool", "stop"); if(nestedScrollingView == target) nestedScrollingView = null; parentHelper.onStopNestedScroll(target); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { boolean isConsumed = nestedScrollBy(dy); if(isConsumed) { consumed[0] = 0; consumed[1] = dy; } } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { Log.i("carpool", "fling"); flingY((int) velocityY); return true; } @Override public int getNestedScrollAxes() { return parentHelper.getNestedScrollAxes(); } //---------------------------NestedScrollParent(End)----------------------------- //---------------------------NestedScrollChild---------------------------------- @Override public void setNestedScrollingEnabled(boolean enabled) { childHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return childHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return childHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { childHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return childHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return childHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return childHelper.dispatchNestedPreFling(velocityX, velocityY); } //---------------------------NestedScrollChild(End)----------------------------- //---------------------------ScrollingView--------------------------- @Override public int computeHorizontalScrollRange() { return 0; } public int computeHorizontalScrollOffset() { return 0; } public int computeHorizontalScrollExtent() { return 0; } public int computeVerticalScrollRange() { return getChildAt(0).getHeight(); } public int computeVerticalScrollOffset() { return getScrollY(); } public int computeVerticalScrollExtent() { return getHeight() - getPaddingTop() - getPaddingBottom(); } //--------------------------ScrollingView(End)------------------------------ @Override public boolean onTouchEvent(MotionEvent event) { event.offsetLocation(0, windowOffsetY); mGestureDetector.onTouchEvent(event); if(event.getAction() == MotionEvent.ACTION_UP) { windowOffsetY = 0; stopNestedScroll(); } return true; } private class MyGestureListener implements GestureDetector.OnGestureListener { @Override public boolean onDown(MotionEvent e) { scroller.abortAnimation(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); windowOffsetY = 0; return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { int dx = (int) distanceX; int dy = (int) distanceY; dispatchNestedPreScroll(dx, dy, mScrollConsumed, mNestedOffsets); windowOffsetY += mNestedOffsets[1]; dy -= mScrollConsumed[1]; if(dy == 0) return true; else nestedScrollBy(dy); return true; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if(!dispatchNestedPreFling(-velocityX, -velocityY)) { dispatchNestedFling(-velocityX, -velocityY, true); flingY((int) -velocityY); } return true; } } public class SortComparator implements Comparator<View> { @Override public int compare(View lhs, View rhs) { return getTopWithView(lhs, FlingNestedScrollView.this) - getTopWithView(rhs, FlingNestedScrollView.this); } } public interface OnScrollChangeListener { void onScrollChange(View view, int scroll); } }