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);
    }
}

 

posted @ 2016-12-15 16:37  simpleone  阅读(2359)  评论(1编辑  收藏  举报