欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Scroller的使用

程序员文章站 2022-07-01 15:10:27
自定义ViewGroup自定义ViewGroup是另外一种重要的自定义View形式,当我们需要自定义子视图的排列方式时,通常下幼通过这种形式实现。例如,最常用的下拉刷新组件,实现下拉刷新、上拉加载跟他更多的原理就是自定义了一个ViewGroup,将HeaderView、ContentView、FooterView从上到下依次布局,如图2-16所示(红色区域为屏幕的显示区域运行时可看到色彩)。然后再初始时通过Scroller滚动使用该组件在y轴上滚动HeaderView的高度,这样当以来该ViewGroup...

自定义ViewGroup

自定义ViewGroup是另外一种重要的自定义View形式,当我们需要自定义子视图的排列方式时,通常下幼通过这种形式实现。例如,最常用的下拉刷新组件,实现下拉刷新、上拉加载跟他更多的原理就是自定义了一个ViewGroup,将HeaderView、ContentView、FooterView从上到下依次布局,如图2-16所示(红色区域为屏幕的显示区域运行时可看到色彩)。然后再初始时通过Scroller滚动使用该组件在y轴上滚动HeaderView的高度,这样当以来该ViewGroup显示在用户眼前时候HeaderView就被隐藏掉了,如图2-17所示。而Content View的高度和宽度都是match_parent的,因此,此时屏幕上只显示CotnentView,HeaderView和FooterView都被隐藏在屏幕之外。当contentView被滚动到顶部,此时如果用户继续下拉,那么该下拉刷新组件将拦截触摸事件,然后根据用户的触摸事件获取到手指滑动的y轴距离,并通过Scroller将该下拉刷新组件在y轴上滚动手指滑动的距离,实现HeaderView的显示与隐藏,从而达到下拉的效果,如图所示。当用户胡到那个到最底部时候会触发加载更多的操作,此时会通过Scroller滚动该下拉刷新组件,将FooterView显示出来,实现加载更多的效果。

Scroller的使用

Scroller的使用

为了更好的理解下拉刷新的实现,我们要先了解Scroller的作用以及如何使用。这里我们将做一个简单的示例来说明。
Scroller是一个帮助View滚动的辅助类,在使用它之前,用户需要通过startScroll来设置滚动的参数,即起始点坐标和(x,y)轴上要滚动的距离。Scroller它封装了滚动时间、要滚动的目标x轴和y轴,以及在每个时间内View应该滚动到的(x,y)轴的坐标点,这样用户就可以在有效的滚动周期内通过Scroller的getCurX()和getCurY()来获取当前时刻View应该滚动的位置,然后通过调用View的scrollTo或者ScrollBy方法进行滚动。那么如何判断滚动是否结束呢?我们只需要覆写View类的computScroll方法,该方法会在View绘制时被调用,在里面调用Scroller的computeScrollOffset来判断滚动是否完成,如果返回true表明滚动未完成,否则滚动完成。上述说的scrollTo或者scrollBy的调用就是在computeScrollOffset为true的情况下调用,并且最后还要调用目标View的postInvalidate()或者invalidate()以实现View的重绘。View的重绘又会导致computeScroll方法被调用,从而继续整个滚动过程,直至computeScrollOffset返回false,即滚动结束。整个过程有点儿绕,我们看一个实例。


public class ScrollLayout extends LinearLayout {
    private String TAG = ScrollLayout.class.getSimpleName();
    private Scroller mScroller;


    public ScrollLayout(@NonNull Context context) {
        super(context);
        initScroller(context);
    }

    public ScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initScroller(context);
    }

    public ScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initScroller(context);
    }

    void initScroller(Context context){
        mScroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
//        super.computeScroll();
        if(mScroller.computeScrollOffset()) {
            //滚动到此,View应该滚动到的x,y坐标上
            this.scrollTo(mScroller.getCurrX(),  mScroller.getCurrY());
            //请求重新绘制该View,从而又会倒是computeScroll调用,然后继续滚动
            // 直到computeScrollOffset返回false
            this.postInvalidate();
        }
    }

    //调用这个方法进行滚动,这里我们只滚动竖直方向
    public void scrollTo(int y){
        // 参数1和参数2分别为滚动的起始点水平、竖直方向的滚动偏移量
        // 参数3和参数4为水平和竖直方向上滚动的距离
        mScroller.startScroll(getScrollX(), getScrollY(), 0, y);
        this.invalidate();
    }

}


ScrollLayout scrollView = new ScrollLayout(getContext());
scrollView.scrollTo(100);

通过上面这段代码会让scrollView在y轴上向下滚动100哥像素点。我们结合代码来分析以下。首先调用scrollTo(inty)方法,然后再该方法中通过mScroller.startScroll()方法来设置滚动的参数,再调用invalidate()方法使得该View重绘。重绘时调用computeScroll方法,再该方法中通过mScroller.computeScrollOffset()判断滚动是否完成,如果返回true,代表没有滚动完成,此时把该View滚动到此刻View应该滚动到的x、y位置,这个位置通过mScroller的getCurrX和getCurrY获得。然后继续调用重绘方法,继续执行滚动过程,直至滚动完成。‘
了解了Scroller原理后,我们继续看通用的下拉刷新组件的实现吧。

下拉刷新的实现

代码量不多,但是也挺有用的,我们这里只拿出重要的点来分析,完整的源码请发文gitee
https://gitee.com/WhatINeed/SmartChart/tree/master/app/src/main/java/com/bin/david/smartchart/scroller
以下知识重要的代码段

public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener {
    public interface OnRefreshListener {
        void onRefresh();
    }

    public interface OnLoadListener {
        void onLoadMore();
    }


    //  滚动控制器
    protected Scroller mScroller;
    // 下拉刷新时显示的header view
    protected View mHeaderView;
    // 上拉加载更多时显示的footer View
    protected View mFooterView;
    //  本次触摸滑动y坐标上的偏移量
    protected int mYOffset;
    // 内容视图,即用户触摸导致下拉刷新、上拉加载的主视图,如ListView、GridView等
    protected T mContentView;
    //  最初的滚动位置,第一次布局时滚动header高度的距离
    protected int mInitScrollY = 0;
    // 最后一次触摸事件的Y轴坐标
    protected int mLastY = 0;
    // 空闲状态
    public static final int STATUS_IDLE = 0;
    // 下拉或者上拉状态,还没有到达可刷新的状态
    public static final int STATUS_PULL_TO_REFRESH = 1;
    // 下拉或者上拉状态
    public static final int STATUS_RELEASE_TO_REFRESH = 2;
    // 刷新中
    public static final int STATUS_REFRESHING = 3;
    // Loading中
    public static final int STATUS_LOADING = 4;
    // 当前状态
    public int mCurrentStatus = STATUS_IDLE;
    // header 中的箭头图标
    private ImageView mArrowImageView;
    //  箭头是否向上
    private boolean isArrowUp;
    //  header中的文本标签
    private TextView mTipsTextView;
    //  header中的时间标签
    private TextView mTimeTextView;
    // header中的进度条
    private ProgressBar mProgressBar;
    //屏幕的高度
    private int mScreenHeight;
    //  header的高度
    private int mHeaderHeight;
    // 下拉刷新回调
    private OnRefreshListener mOnRefreshListener;
    // 加载更多的回调
    private OnLoadListener mLoadListener;


    public void setOnRefreshListener(OnRefreshListener mOnRefreshListener) {
        this.mOnRefreshListener = mOnRefreshListener;
    }

    public void setOnLoadListener(OnLoadListener mLoadListener) {
        this.mLoadListener = mLoadListener;
    }

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

    public RefreshLayoutBase(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
    }

    public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化Scroller对象
        mScroller = new Scroller(context);
        // 获取屏幕高度
        mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
        //header的高度为屏幕高度的1/4
        mHeaderHeight = mScreenHeight / 4;
        //初始化整个布局
        initLayout(context);
    }

    // 初始化整个布局,从上到下分别为header、内容视图、footer
    private void initLayout(Context context) {
        //设置header view
        setupHeaderView(context);
        //设置内容视图
        setupContentView(context);
        //设置布局参数
        setDefaultContentLayoutParams();
        //添加内容视图,如ListView、GridView等
        addView(mContentView);
        //footer view
        setupFooterView(context);
    }

    /**
     * 设置Content View的默认布局参数
     */
    protected void setDefaultContentLayoutParams() {
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
        );
        mContentView.setLayoutParams(params);
    }

    // 初始化header view
    protected void setupHeaderView(Context context) {
        mHeaderView = LayoutInflater.from(context).inflate(
                R.layout.pull_to_refresh_header, this, false
        );
        mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT, mHeaderHeight
        ));
        mHeaderView.setBackgroundColor(Color.RED);
        // header的高度为1/4的屏幕高度,但是,它只有100px是有效的显示区域
        // 取余paddingTop,这样是为了达到下拉的效果
        mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
        addView(mHeaderView);

        //初始化header view中的子视图
        mArrowImageView = mHeaderView.findViewById(R.id.pull_to_refresh_image);
        mTipsTextView = mHeaderView.findViewById(R.id.pull_to_refresh_text);
        mTimeTextView = mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
        mProgressBar = mHeaderView.findViewById(R.id.pull_to_refresh_progress);
    }

    // 初始化ContentView, 子类复写
    protected abstract void setupContentView(Context context);

    //初始化footerview
    protected void setupFooterView(Context context) {
        mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_fotter, this, false);
        addView(mFooterView);
    }

    //是否已经到了最顶部,子类需要复写该方法,使得mContentView滑动到最顶端时返回true
    //如果达到最顶端用户继续下拉则拦截事件
    protected abstract boolean isTop();

    //是否已经到了最底部,子类需要复写该方法,使得mContentiew滑动到最底端时返回false
    //从而出发自动加载更多的操作
    protected abstract boolean isBottom();

    /**
     * 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header、content view、footer这三个子控件高度之和
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // MeasureSpec中的宽度值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //子视图的个数
        int childCount = getChildCount();
        // 最终的高度
        int finalHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //测量每个子视图的尺寸
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //所有子视图的高度和就是该下拉率新组件的总高度
            finalHeight += child.getMeasuredHeight();
        }
        //设置该下拉刷新组件的尺寸
        setMeasuredDimension(width, finalHeight);
    }

    /**
     * 布局函数,将header、content view、footer这3个View从上到下布局。
     * 布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度+本视图的paddingTop,从而达到隐藏header的效果
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCound = getChildCount();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < childCound; i++) {
            View child = getChildAt(i);
            child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
            top += child.getMeasuredHeight();
        }
        //计算初始化滑动的y轴距离
        mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
        //滑动header view高度的位置,从而达到隐藏header view的效果
        scrollTo(0, mInitScrollY);
    }

    /**
     * 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,并且是下拉时拦截触摸事件,否则不拦截,交给child、view来处理
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //  获取触摸事件的类型
        final int action = ev.getActionMasked();
        // 取消事件和抬起事件则直接返回false
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            return false;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = (int) ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                mYOffset = (int) ev.getRawY() - mLastY;
                //如果拉到了顶部,并且是下拉,则拦截触摸事件
                //从而转到onTouchEvent来处理下拉刷新事件
                if (isTop() && mYOffset > 0) {
                    return true;
                }
                break;
        }
        //默认不拦截触摸事件,使得该控件的子视图得到处理机会
        return false;
    }

    /**
     * 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //滑动事件
            case MotionEvent.ACTION_MOVE:
                //获取手指触摸的当前y坐标
                int currentY = (int) event.getRawY();
                //当前坐标减去按下时的y坐标得到y轴上的偏移量
                mYOffset = currentY - mLastY;
                if (mCurrentStatus != STATUS_LOADING) {
                    //在y轴方向上滚动该控件
                    changeScrollY(mYOffset);
                }
                //旋转Header中的箭头图标
                rotateHeaderArrow();
                //修改Header中的文本信息
                changeTips();
                //mLastY设置为这次的y轴坐标
                mLastY = currentY;
                break;
            case MotionEvent.ACTION_UP:
                //下拉刷新的具体操作
                doRefresh();
                break;
            default:
                break;
        }
        // 返回true, 消费该事件
        return true;
    }

    private void rotateHeaderArrow() {

    }

    private void changeTips() {

    }

    /**
     * 修改y轴上的滚动值,从而实现Header被下拉的效果
     *
     * @param distance 这次触摸事件的y轴与上一次的y轴的差值
     */
    private void changeScrollY(int distance) {
        //最大值为scrollY(header隐藏),最小值为0(Header完全显示)
        int curY = getScrollY();
        //下拉
        if (distance > 0 && curY - distance > getPaddingTop()) {
            scrollBy(0, -distance);
        } else if (distance < 0 && curY - distance <= mInitScrollY) {
            //上拉过程
            scrollBy(0, -distance);
        }

        curY = getScrollY();
        int slop = mInitScrollY / 2;
        if (curY > 0 && curY < slop) {
            mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
        } else if (curY > 0 && curY > slop) {
            mCurrentStatus = STATUS_PULL_TO_REFRESH;
        }
    }

    //执行下拉刷新
    private void doRefresh() {
        changeHeaderViewStatus();
        //执行刷新操作
        if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
            mOnRefreshListener.onRefresh();
        }
    }

    /**
     * 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作
     * 如果下拉的距离超过Header view的1/2
     * 那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态
     */
    private void changeHeaderViewStatus() {
        int curScrollY = getScrollY();
        // 超过1/2则认为是有效的下拉刷新,否则还原
        if (curScrollY < mInitScrollY / 2) {
            // 滚动到能够正常显示Header的位置
            mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop() - curScrollY);
            mCurrentStatus = STATUS_REFRESHING;
            mTipsTextView.setText("refreshing");
            mArrowImageView.clearAnimation();
            mArrowImageView.setVisibility(View.GONE);
            mProgressBar.setVisibility(View.VISIBLE);
        } else {
            mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
            mCurrentStatus = STATUS_IDLE;
        }
        invalidate();
    }

    /**
     * 加载结束,恢复状态
     */
    public void loadCompelte() {
        // 隐藏footer
        startScroll(mInitScrollY - getScrollY());
        mCurrentStatus = STATUS_IDLE;
    }

    /**
     * 刷新结束,恢复状态
     */
    public void refreshComplete() {
        mCurrentStatus = STATUS_IDLE;
        //隐藏Header view
        mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
        invalidate();
        updateHeaderTimeStamp();

        //  200毫秒后处理arrow和progressbar,免得太突兀
        this.postDelayed(new Runnable() {
            @Override
            public void run() {
                mArrowImageView.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);
            }
        }, 100);
    }

    private void updateHeaderTimeStamp() {

    }


    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //用户发设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多
        if (mLoadListener != null
                && isBottom()
                && mScroller.getCurrY() <= mInitScrollY
                && mYOffset <= 0
                && mCurrentStatus == STATUS_IDLE
        ) {
            // 显示Footer View
            showFooterView();
            //调用加载更多
            doLoadMore();
        }
    }


    /**
     * 设置滚动的参数
     */
    private void startScroll(int yOffset) {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
        invalidate();
    }

    //显示footer view
    private void showFooterView() {
        startScroll(mFooterView.getMeasuredHeight());
        mCurrentStatus = STATUS_LOADING;
    }

    // 执行下拉(自动)加载更多的操作
    private void doLoadMore() {
        if (mLoadListener != null) {
            mLoadListener.onLoadMore();
        }
    }
}


在构造函数中调用initLayout函数初始化整个布局,从上到下分别为Header view、内容视图、Footer view,我们先看这3部分的相关函数:

    // 初始化整个布局,从上到下分别为header、内容视图、footer
    private void initLayout(Context context) {
        //设置header view
        setupHeaderView(context);
        //设置内容视图
        setupContentView(context);
        //设置布局参数
        setDefaultContentLayoutParams();
        //添加内容视图,如ListView、GridView等
        addView(mContentView);
        //footer view
        setupFooterView(context);
    }

    // 初始化header view
    protected void setupHeaderView(Context context) {
        mHeaderView = LayoutInflater.from(context).inflate(
                R.layout.pull_to_refresh_header, this, false
        );
        mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(
                LayoutParams.MATCH_PARENT, mHeaderHeight
        ));
        mHeaderView.setBackgroundColor(Color.RED);
        // header的高度为1/4的屏幕高度,但是,它只有100px是有效的显示区域
        // 取余paddingTop,这样是为了达到下拉的效果
        mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
        addView(mHeaderView);

        //初始化header view中的子视图
        mArrowImageView = mHeaderView.findViewById(R.id.pull_to_refresh_image);
        mTipsTextView = mHeaderView.findViewById(R.id.pull_to_refresh_text);
        mTimeTextView = mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
        mProgressBar = mHeaderView.findViewById(R.id.pull_to_refresh_progress);
    }

    // 初始化ContentView, 子类复写
    protected abstract void setupContentView(Context context);

    //初始化footerview
    protected void setupFooterView(Context context) {
        mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_fotter, this, false);
        addView(mFooterView);
    }

其中header view 和footer view都是从默认的布局中加载,因此,它们是固定的。但是,最中间的内容视图是可变的,例如,我们显示内容的控件可能是ListView、GridView、TextView等,因此,这部分是未知的,所以setContentView留给子类去具体化。还有两外两个抽象函数,分别为判断是否下拉到顶部已经上拉到底部的函数,因为不同内容视图判断是否滚动到顶部、底部的实现代码也是不一样,因此,也需要抽象化。函数定义如下:

    //是否已经到了最顶部,子类需要复写该方法,使得mContentView滑动到最顶端时返回true
    //如果达到最顶端用户继续下拉则拦截事件
    protected abstract boolean isTop();

    //是否已经到了最底部,子类需要复写该方法,使得mContentiew滑动到最底端时返回false
    //从而出发自动加载更多的操作
    protected abstract boolean isBottom();

初始化这3部分视图之后,接下来的第一个关键步骤就是视图测量与布局,也就是我们自定义ViewGroup中必备的两个步骤。上文我们已经说过,header view、内容视图、footer是纵向布局的,因此,需要将它们从上到下布局。在布局之前还要测量各个子视图的尺寸以下拉刷新组件自身的尺寸。代码如下:


 /**
     * 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header、content view、footer这三个子控件高度之和
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // MeasureSpec中的宽度值
        int width = MeasureSpec.getSize(widthMeasureSpec);
        //子视图的个数
        int childCount = getChildCount();
        // 最终的高度
        int finalHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            //测量每个子视图的尺寸
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //所有子视图的高度和就是该下拉率新组件的总高度
            finalHeight += child.getMeasuredHeight();
        }
        //设置该下拉刷新组件的尺寸
        setMeasuredDimension(width, finalHeight);
    }

    /**
     * 布局函数,将header、content view、footer这3个View从上到下布局。
     * 布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度+本视图的paddingTop,从而达到隐藏header的效果
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCound = getChildCount();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < childCound; i++) {
            View child = getChildAt(i);
            child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
            top += child.getMeasuredHeight();
        }
        //计算初始化滑动的y轴距离
        mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
        //滑动header view高度的位置,从而达到隐藏header view的效果
        scrollTo(0, mInitScrollY);
    }

在onMeasure中我们测量了该组件自身的大小以及所有子视图的大小,并且将该控件设置为所有的子视图高度之和,在这里也就是header、content view、footer的高度之和,这样在布局时我们才有足够的控件竖向放置子视图。
在onLayout时,会将Header view、内容视图、Footer view从上到下布局,即Header view实际上显示该viewGroup向上滚动HeaderView 的高度,使得Header View变得不可见,如上文的图所示。当用户向下拉时候,该组件判断内容视图滑到了顶部,此时又通过Scroller将该组件向下滚动,使得Header View慢慢显示出来。实现这些功能就需要我们处理该控件的触摸事件,通过内容视图滚动到了顶部或者底部来判断是否需要拦截触摸事件。相关代码如下:

/**
     * 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,并且是下拉时拦截触摸事件,否则不拦截,交给child、view来处理
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //  获取触摸事件的类型
        final int action = ev.getActionMasked();
        // 取消事件和抬起事件则直接返回false
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            return false;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = (int) ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                mYOffset = (int) ev.getRawY() - mLastY;
                //如果拉到了顶部,并且是下拉,则拦截触摸事件
                //从而转到onTouchEvent来处理下拉刷新事件
                if (isTop() && mYOffset > 0) {
                    return true;
                }
                break;
        }
        //默认不拦截触摸事件,使得该控件的子视图得到处理机会
        return false;
    }

onnterceptTouchEvent是ViewGroup中对触摸事件进行拦截的函数,当返回true时后续的触摸事件就会被该ViewGroup拦截,此时子视图将不会获得触摸事件。相应地,返回false则表示不进行拦截。例如在上述onInterceptTouchEvent函数中,我们在ACTION_DOWN事件(手指第一次按下)时记录了y轴的坐标,当用户的手指在屏幕上滑动时就会产生ACTION_MOVE事件,此时我们y轴坐标,并且与最初ACTION_DOWN事件的y轴相减。如果mYOffset大于0,那么表示用户的手指是从上到下滑动,如果此时内容视图已经是到了顶部,例如,ListView的第一个可见元素就是第一项,那么则返回true,也就是将后续的触摸事件拦截。此时,后续的ACTION_MOVE、ACTION_UP等事件就会又该组件进行处理,处理函数为onTouchEvent函数,代码如下:


/**
     * 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //滑动事件
            case MotionEvent.ACTION_MOVE:
                //获取手指触摸的当前y坐标
                int currentY = (int) event.getRawY();
                //当前坐标减去按下时的y坐标得到y轴上的偏移量
                mYOffset = currentY - mLastY;
                if (mCurrentStatus != STATUS_LOADING) {
                    //在y轴方向上滚动该控件
                    changeScrollY(mYOffset);
                }
                //旋转Header中的箭头图标
                rotateHeaderArrow();
                //修改Header中的文本信息
                changeTips();
                //mLastY设置为这次的y轴坐标
                mLastY = currentY;
                break;
            case MotionEvent.ACTION_UP:
                //下拉刷新的具体操作
                doRefresh();
                break;
            default:
                break;
        }
        // 返回true, 消费该事件
        return true;
    }

在onTouchEvent函数中,我们会判断触摸事件的类型,如果还是ACTION_MOVE事件,那么计算当前触摸事件的y坐标与ACTION_DOWN时的y坐标的差值,然后调用changeScrollY函数在y轴上滚动该控件。如果用户一直向下滑动手指,那么mYOffset值将不断增大,那么此时该控件将不断地往上滚,Header View的可见高度也就越来越大。我们看看changeScrollY函数的实现

/**
 * 修改y轴上的滚动值,从而实现Header被下拉的效果
 *
 * @param distance 这次触摸事件的y轴与上一次的y轴的差值
 */
private void changeScrollY(int distance) {
    //最大值为scrollY(header隐藏),最小值为0(Header完全显示)
    int curY = getScrollY();
    //下拉
    if (distance > 0 && curY - distance > getPaddingTop()) {
        scrollBy(0, -distance);
    } else if (distance < 0 && curY - distance <= mInitScrollY) {
        //上拉过程
        scrollBy(0, -distance);
    }
    curY = getScrollY();
    int slop = mInitScrollY / 2;
    if (curY > 0 && curY < slop) {
        mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
    } else if (curY > 0 && curY > slop) {
        mCurrentStatus = STATUS_PULL_TO_REFRESH;
    }
}

从上述程序中可以看到,changeSrollY函数实际上就是根据这一次与上一次y轴的差值来滚动当前控件,由于两次触摸事件的差值最小,因此,滚动起来相对比较流畅。当distance小于0时,则是向上滚动,此时Header View的可见范围越来越小,最后完全隐藏。当distrance大于0时则是向下滚动,此时Header View的可见乏味越来越大,这样一来也就实现了下拉时显示Header View效果。当然在下拉过来过程中,我们也会修改HeaderView布局中的一些控件状态,例如箭头的ImageView、文本信息等。
HeaderView 显示之后,当我们的额手指离开屏幕时,如果在y轴上的滚动高度大于HeaderView有效区域高度的二分之一,那么就会触发刷新操作,否则就会日通过Scroller将HeaderView再次隐藏起来。相关代码为ACTION_UP触摸事件中调用的doRefresh函数:

    //执行下拉刷新
    private void doRefresh() {
        changeHeaderViewStatus();
        //执行刷新操作
        if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
            mOnRefreshListener.onRefresh();
        }
    }
        /**
     * 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作
     * 如果下拉的距离超过Header view的1/2
     * 那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态
     */
    private void changeHeaderViewStatus() {
        int curScrollY = getScrollY();
        // 超过1/2则认为是有效的下拉刷新,否则还原
        if (curScrollY < mInitScrollY / 2) {
            // 滚动到能够正常显示Header的位置
            mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop() - curScrollY);
            mCurrentStatus = STATUS_REFRESHING;
            mTipsTextView.setText("refreshing");
            mArrowImageView.clearAnimation();
            mArrowImageView.setVisibility(View.GONE);
            mProgressBar.setVisibility(View.VISIBLE);
        } else {
            mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
            mCurrentStatus = STATUS_IDLE;
        }
        invalidate();
    }

在changeHeaderViewStatus函数中,当判断为满足下拉刷新的条件时,就会设置当前组件的状态为STATUS_REFRESHING状态,并且设置正好显示HeaderView区域,最后调用OnRefreshListener实现用户设置的下拉刷新操作。刷新操作执行完成之后,用户需要调用refreshComplete函数告知当前控件刷新完毕,此时当前控件会将HeaderView隐藏。相关代码如下:

     * 刷新结束,恢复状态
     */
    public void refreshComplete() {
        mCurrentStatus = STATUS_IDLE;
        //隐藏Header view
        mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
        invalidate();
        updateHeaderTimeStamp();

        //  200毫秒后处理arrow和progressbar,免得太突兀
        this.postDelayed(new Runnable() {
            @Override
            public void run() {
                mArrowImageView.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);
            }
        }, 100);
    }

在refreshComplete中将重置控件的状态,并且将HeaderView滚动到屏幕之外。此时,整个下拉刷新操作就完成了。滚动到底部时加载更多比下拉刷新就要简单一些,只需要判断是否滚动到底部,如果已经到底部那么直接触发加载更多,因此,当前控件需要舰艇内容视图的滚动事件:

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //用户发设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多
        if (mLoadListener != null
                && isBottom()
                && mScroller.getCurrY() <= mInitScrollY
                && mYOffset <= 0
                && mCurrentStatus == STATUS_IDLE
        ) {
            // 显示Footer View
            showFooterView();
            //调用加载更多
            doLoadMore();
        }
    }


    /**
     * 设置滚动的参数
     */
    private void startScroll(int yOffset) {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
        invalidate();
    }

    //显示footer view
    private void showFooterView() {
        startScroll(mFooterView.getMeasuredHeight());
        mCurrentStatus = STATUS_LOADING;
    }

    // 执行下拉(自动)加载更多的操作
    private void doLoadMore() {
        if (mLoadListener != null) {
            mLoadListener.onLoadMore();
        }
    }
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener

在onScroll中监听内容视图的滚动事件,当内容视图滚动到底部时显示FooterView,并且调用OnLoadListener回掉执行加载更多的操作。当操作执行完毕后用户需要调用loadComplete函数告知当前控件加载完毕,下拉刷新徐建此时隐藏FooterView并且设置为STATUS_IDLE状态。
这就是整个REgreshLayoutBase类的核心逻辑,下面我们看看具体实现类,例如内容视图时ListeView的实现

Scroller的使用

RefreshListView复写了RefreshLayotuBase的3个函数,分别设置内容视图、判断是否滚动到顶部、判断是否时滚动到底部。需要注意的时,在setcontentView函数中,我们将mContentView(在这里也就是ListView)的onScrollListener设置为this,这是因为需要监听ListView的滚动状态,当滚动到最后一项时触发加载个恩多操作。因为RefreshLayoutBase实现了onScrollListener接口,而判断是否调用加载更多的代码被封装在了RefreshLayoutBase类中,因此,在这里直接调用onContentView对象的SetOn Scroll Listener(this)即可。使用示例代码如下:

Scroller的使用

Scroller的使用

本文地址:https://blog.csdn.net/AdrianAndroid/article/details/107114921