具有回弹效果的RecyclerView,RecyclerView外层可滚动容器

具有回弹效果的RecyclerView,RecyclerView外层可滚动容器_第1张图片

    一个具有回弹效果的RecyclerView,本文通过实现RecyclerView外层的容器的上下滑动达到了回弹的效果,在整个滑动的事件分发机制中,外层容器的事件拦截机制进行判断是否拦截事件,判断标准为RecyclerView是否滚动到了第一个item或者最后一个item,如果下滑滚动到了第一个item还继续下滑,外层容器的事件拦截机制将此事件进行拦截,交给外层容器的onTouchEvent进行消费;上滑,同理。如果不拦截,将交给子view即RecyclerView进行消费。

    下面是我自己总结的事件分发机制,希望对你能有帮助。不清楚的童鞋可以打印出来进行记忆。每天翻看几遍,死记住,以后你会慢慢的明白其原理。

dispatchTouchEvent(MotionEvent ev)事件分发
1.返回true,表示这件事由dispatchTouchEvent消费掉了,事件停止向下传递。
2.返回false,表示事件不分发,返回给上一层activity或者父控件中的onTouchEvent进行消费。
3.返回super.dispatchTouchEvent(ev),事件交由当前view的onlnterceptTouchEvent进行事件拦截。

onlnterceptTouchEvent(MotionEvent ev) 事件拦截
1.返回true,表示事件拦截成功,交由当前view的onTouchEvent消费。
2.返回false,表示事件放行,将事件传递给下一个子view进行处理(下一个子view的dispatchTouchEvent进行处理)。
3.返回super.onlnterceptTouchEvent(ev),表示事件拦截,交由当前view的onTouchEvent消费,同true。

onTouchEvent(MotionEvent ev) 事件响应
前提:当前控件事件获得响应
1.返回 false,事件将从当前的view向上传递,由父view的onTouchEvent来接收消费事件。
2.返回 true,事件被当前的view接收并消费掉。
3.返回onTouchEvent(ev),同false。

具有回弹效果的RecyclerView,RecyclerView外层可滚动容器_第2张图片

    下面是这个容器的代码:

容器类

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;

/**
 * Created by Sick on 2016/8/8.
 * 自定义可滚动组件的弹性容器,仿IOS回弹效果
 */
public class RVScrollLayout extends LinearLayout {
    private final String TAG = this.getClass().getSimpleName();
    /**
     * 容器中的组件
     */
    private View convertView;
    /**
     * 如果容器中的组件为RecyclerView
     */
    private RecyclerView recyclerView;
    /**
     * 滚动结束
     */
    private int mStart;
    /**
     * 滚动结束
     */
    private int mEnd;
    /**
     * 上一次滑动的坐标
     */
    private int mLastY;
    /**
     * 滚动辅助类
     */
    private Scroller mScroller;

    public RVScrollLayout(Context context) {
        this(context, null);
    }

    public RVScrollLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RVScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() > 1) {
            throw new RuntimeException(RVScrollLayout.class.getSimpleName() + "只能有一个子控件");
        }
        convertView = getChildAt(0);
       //TODO 可以拓展ListView等可滑动的组件
        if (convertView instanceof RecyclerView) {
            recyclerView = (RecyclerView) convertView;
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (changed) {
            View view = getChildAt(0);
            view.layout(left, top, right, bottom);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();  //终止动画
                }
                scrollTo(0, (int) ((mLastY - y) * 0.4));
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                /**
                 * 回弹动画,第一二个参数为开始的x,y
                 * 第三个和第四个参数为滚动的距离(注意方向问题)
                 * 第五个参数是回弹时间
                 */
                mScroller.startScroll(0, mEnd, 0, -dScrollY, 1000);
                break;
        }
        postInvalidate();
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        int y = (int) ev.getY();
        Log.d(TAG, "相对于组件滑过的距离==getY():" + y);
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * 下面两个判断来自于 BGARefreshLayout 框架中的判断,github 上搜索 BGARefreshLayout
                 */
                if (convertView instanceof RecyclerView) {
                    if (y - mLastY > 0) {
                        if (Util.isRecyclerViewToTop(recyclerView)) {
                            Log.d(TAG, "滑倒顶部时时间拦截成功");
                            return true;
                        }
                    }

                    if (y - mLastY < 0) {
                        if (Util.isRecyclerViewToBottom(recyclerView)) {
                            Log.d(TAG, "滑倒底部时时间拦截成功");
                            return true;
                        }
                    }
                }
                break;
        }

        return false;
    }


    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }
}

区别scrollBy()和scrollTo(),前者是在上一个动作的基础上移动,后者是从view的最开始移动,移动是相对于括号中的参数而言。

还有最重要的两点:第一,它们移动的是当前view中的content,当前viewgroup中的子view,在recyclerview中相当于移动的它里面的子item;第二,他们的移动,为什么说他们的移动是一个重点呢,因为如果你将移动的x,y直接写入参数,你会发现视图是反方向的移动,这里你可以将子item看成一个很大的画板,手机(recyclerview)看成一个中空的挡板,移动中空挡板就相当于子item移动了,所以它们的参数应该是一个负值。  即scrollBy(-offsetX,-offsetY)

getScrollY() 获取的是view或这viewgroup滚动过的距离。

    再捋一捋这一流程的事件分发,首先滑动的时候事件是从外层向内层传递,本文中外层是容器,内层是recyclerview组件,事件分发给容器,容器进行判断是否拦截,return true表示拦截,在容器自身的onTouchEvent()中消费,如果return false将交给recyclerview进行消费,这里将不关注recyclerview。

    起初我的思路是重写recyclerview的事件分发的方法,对外层容器传递过来的事件进行不分发返回给上层容器onTouchEvent()进行消费 或者 在它的OnTouchEvent()事件响应中返回return false 交给上层容器onTouchEvent()进行消费来达到容器可以回弹的效果。

父容器的回弹效果可以通过很多种方式进行实现,可以设置父容器的marginTop和marginBottom达到弹性的效果,而本文采用了Scroller辅助类滚动来实现的。

配置类(判断recyclerview滚动)

import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.view.ViewParent;

import java.lang.reflect.Field;

import cn.bingoogolapple.refreshlayout.BGAStickyNavLayout;

/**
 * Created by Sick on 2016/8/10.
 */
public class Util {
    public static boolean isRecyclerViewToTop(RecyclerView recyclerView) {
        if (recyclerView != null) {
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager == null) {
                return true;
            }
            if (manager.getItemCount() == 0) {
                return true;
            }

            if (manager instanceof LinearLayoutManager) {
                LinearLayoutManager layoutManager = (LinearLayoutManager) manager;

                int firstChildTop = 0;
                if (recyclerView.getChildCount() > 0) {
                    // 处理item高度超过一屏幕时的情况
                    View firstVisibleChild = recyclerView.getChildAt(0);
                    if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
                        if (android.os.Build.VERSION.SDK_INT < 14) {
                            return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0);
                        } else {
                            return !ViewCompat.canScrollVertically(recyclerView, -1);
                        }
                    }

                    // 如果RecyclerView的子控件数量不为0,获取第一个子控件的top

                    // 解决item的topMargin不为0时不能触发下拉刷新
                    View firstChild = recyclerView.getChildAt(0);
                    RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams();
                    firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop();
                }

                if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 通过反射获取RecyclerView的item的topInset
     *
     * @param layoutParams
     * @return
     */
    private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) {
        try {
            Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets");
            field.setAccessible(true);
            // 开发者自定义的滚动监听器
            Rect decorInsets = (Rect) field.get(layoutParams);
            return decorInsets.top;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) {
        if (recyclerView != null) {
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager == null || manager.getItemCount() == 0) {
                return false;
            }

            if (manager instanceof LinearLayoutManager) {
                // 处理item高度超过一屏幕时的情况
                View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
                if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
                    if (android.os.Build.VERSION.SDK_INT < 14) {
                        return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0);
                    } else {
                        return !ViewCompat.canScrollVertically(recyclerView, 1);
                    }
                }

                LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
                if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
                    BGAStickyNavLayout stickyNavLayout = getStickyNavLayout(recyclerView);
                    if (stickyNavLayout != null) {
                        // 处理BGAStickyNavLayout中findLastCompletelyVisibleItemPosition失效问题
                        View lastCompletelyVisibleChild = layoutManager.getChildAt(layoutManager.findLastCompletelyVisibleItemPosition());
                        if (lastCompletelyVisibleChild == null) {
                            return true;
                        } else {
                            // 0表示x,1表示y
                            int[] location = new int[2];
                            lastCompletelyVisibleChild.getLocationOnScreen(location);
                            int lastChildBottomOnScreen = location[1] + lastCompletelyVisibleChild.getMeasuredHeight();
                            stickyNavLayout.getLocationOnScreen(location);
                            int stickyNavLayoutBottomOnScreen = location[1] + stickyNavLayout.getMeasuredHeight();
                            return lastChildBottomOnScreen <= stickyNavLayoutBottomOnScreen;
                        }
                    } else {
                        return true;
                    }
                }
            } else if (manager instanceof StaggeredGridLayoutManager) {
                StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager;

                int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null);
                int lastPosition = layoutManager.getItemCount() - 1;
                for (int position : out) {
                    if (position == lastPosition) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public static BGAStickyNavLayout getStickyNavLayout(View view) {
        ViewParent viewParent = view.getParent();
        while (viewParent != null) {
            if (viewParent instanceof BGAStickyNavLayout) {
                return (BGAStickyNavLayout) viewParent;
            }
            viewParent = viewParent.getParent();
        }
        return null;
    }
}
Activity类

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;

/**
 * Created by Sick on 2016/8/8.
 */
public class TestActivity extends Activity {
    private RecyclerView rvCustomList;
    private ArrayList data;
    private RVAdapter adapter;

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

    }

    private void initData() {
        data = new ArrayList();
        for (int i = 0; i <20; i++) {
            data.add("测试"+i);
        }
    }

    private void initView() {
        rvCustomList = (RecyclerView) findViewById(R.id.rv_custom_list);
        rvCustomList.setLayoutManager(new LinearLayoutManager(this));
        adapter = new RVAdapter();
        rvCustomList.setAdapter(adapter);
    }


    public class RVAdapter extends RecyclerView.Adapter{

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            MyHolderView holder = new MyHolderView(LayoutInflater.from(TestActivity.this).inflate(R.layout.item_data,parent,false));
            return holder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof MyHolderView){
                ((MyHolderView) holder).tvData.setText(data.get(position));
            }
        }

        @Override
        public int getItemCount() {
            return data.size();
        }
        private class MyHolderView extends RecyclerView.ViewHolder{
            TextView tvData;
            public MyHolderView(View itemView) {
                super(itemView);
                tvData = (TextView) itemView.findViewById(R.id.tv_data);
            }
        }
    }
}

xml文件




    

        


        
    
代码直接copy到你的项目里面就可以使用,大神勿喷,小弟只是一个beginner,你有更好的想法更健壮的代码可以联系我![email protected]

你可能感兴趣的:(android)