Android打造通用的下拉刷新组件

还记得上一篇 blog 的内容吗?如果不记得建议先去了解一下,Android 事件处理全面剖析 ,因为下拉刷新需要用到手势的处理,而上一篇文章中,对事件处理做了很详细的说明,了解了事件的处理机制,对理解本篇文章有很大的帮助。好了,这里就当大家都已经对事件处理有了一定的了解,开始我们的下拉刷新征程。

还是老规矩,先上效果图,再根据效果图来分析实现的原理;

一 、分析原理

我们都知道,listView 控件为我们提供了 addHeaderView、和 addFootView 的方法,我们通过此方法可以很方便的实现下拉刷新效果;但不是所有的控件都有 addHeaderView 方法,比如,scrollView、TextView 等都没有addHeaderView 方法,所以这些控件就需要我们自己通过其他方式实现下拉刷新的效果,一个项目中,为了通用性和复用性,往往也不会把 listView 控件单独分离出来实现下拉刷新的效果,这时,就需要一个能对所有的控件达到通用的下拉刷新效果。

这里很容易想到用自定义 ViewGroup 来实现,让自定义的 ViewGroup 包含两个控件,一个是下拉刷新的headerView、 另一个是需要展示数据的控件contentView,contentView可以是任何控件;headerView 和 contentView 垂直布局,并且初始状态让 headerView 滚动到看不到的位置。基本的思路就是这样,接下来就是对事件处理。

二、代码实现

代码实现可以分为四个小点:

1、自定义 ViewGroup 的实现

在前面我的 blog 中有一篇写的是关于自定义 View 的内容,Android自定义View,你必须知道的几点 如果对这篇 blog 了解的同学相信对你来说,自定义 ViewGroup 也没什么难度,自定义 ViewGroup 相对自定义 View 还是较容易的。

自定义 ViewGroup 需要重写的两个方法是

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int l, int t, int r, int b)

onMeasure方法相对较简单,只需要对子 View 进行测量即可,这里贴出onMeasure的代码,注解也比较详细。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*获取 ViewGroup 的宽度*/
        int width = MeasureSpec.getSize(widthMeasureSpec) ;
        /*获取 ViewGroup 的高度*/
        int height = MeasureSpec.getSize(heightMeasureSpec) ;
        /*这里不懂的同学可以去参考我前面写的一篇blog 自定义View*/
        /*测量 refreshView 的宽高,这里把高度设为固定值*/
        measureChild(mHeaderView,widthMeasureSpec,MeasureSpec.makeMeasureSpec(mHeaderHeight ,MeasureSpec.EXACTLY));
        Log.v("zgy","==========mHeaderView============"+mHeaderView.getMeasuredHeight()) ;
        /*测量 mContentView 的宽高,高度为最大值只能为ViewGroup 的高度*/
        measureChild(mContentView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
        mRefreshHeight = mTextView.getMeasuredHeight() ;
        /*千万别忘记调用测量方法*/
        setMeasuredDimension(width,height);
    }

onLayout方法就是根据我们测量子 View 的宽高,来布局子 View,前面我们分析原理的时候说到了,这里需要用到垂直布局,也就是先布局 headerVeiw,再在 headerView 下面布局 contentView,这里 headerView 的隐藏操作也放在 onLayout 方法中,所以就得判断是否是第一次,防止重复隐藏。具体实现代码如下

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        /*布局刷新的头部 headerView*/
mHeaderView.layout(0,0,mHeaderView.getMeasuredWidth(),mHeaderView.getMeasuredHeight());
        /*布局内容区域 contentView*/   mContentView.layout(0,mHeaderView.getMeasuredHeight(),mContentView.getMeasuredWidth(),             mHeaderView.getMeasuredHeight()+mContentView.getMeasuredHeight());
        if (isFirst){
            /*第一次把 headerView隐藏*/
            scrollTo(0,mHeaderView.getMeasuredHeight());
        }
        isFirst = false ;
    }

上面讲到了两个 View,一个是 headerView ,另一个是 contentView,讲了这么久,相信大家都会问,这两个 View 从何而来?先来分析 headerView, headerView 它是一个固定的、不会变的。所以这里我们可以直接通过 xml 来定义,然后再代码中通过 addView 方法把 headerView 添加进去。

headerView xml 中的代码

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="130dp"
                android:background="@mipmap/refresh_bg">

    <TextView
        android:id="@+id/id_txt_header"
        android:gravity="center"
        android:text="下拉可以刷新"
        android:layout_width="match_parent"
        android:layout_marginTop="70dp"
        android:layout_height="60dp"/>

    <ImageView
        android:id="@+id/id_anim_header"
        android:layout_width="match_parent"
        android:scaleType="centerCrop"
        android:layout_height="60dp"
        android:layout_marginTop="70dp"
        android:src="@drawable/refresh_anim"
        />
</RelativeLayout>

在初始化 ViewGroup 的时候调用 addView

        mHeaderView = mInflater.inflate(R.layout.refresh_header_view,null) ;
        mTextView = mHeaderView.findViewById(R.id.id_txt_header) ;
        mAnimView = (ImageView) mHeaderView.findViewById(R.id.id_anim_header);
        mAnimDrawable = (AnimationDrawable) mAnimView.getDrawable();
        mHeaderHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,130,
                getResources().getDisplayMetrics()) ;
        addView(mHeaderView);

再来分析 contentView,我们知道 contentView 是变化的,根据不同的界面展示不同的 contentView,所以可以在界面的 xml 中通过 把需要展示的 View 放入自定义的容器 ViewGroup 中。

    <moon.pullrefresh.RefreshView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ListView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/id_listview"></ListView>
    </moon.pullrefresh.RefreshView>

那么问题来了,怎么在自定义的 ViewGroup 中获取 contentView 呢?可以自己先考虑下,我们都知道,ViewGroup 有这样一个方法 addView;

    public void addView(View child) {
        addView(child, -1);
    }

    public void addView(View child, int index) {
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }

    public void addView(View child, int width, int height) {
        final LayoutParams params = generateDefaultLayoutParams();
        params.width = width;
        params.height = height;
        addView(child, -1, params);
    }

    public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }

    public void addView(View child, int index, LayoutParams params) {
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

所以我们可以在 addView 的时候,获取 contentView,但又有一个问题,参数个数不同,addView 的调用也不同,我们在add content的时候已经先 add 了一个 headerView,所以这里肯定是调用含有一个 int index 参数的方法,再看 xml 中 viewGroup 包含

android:layout_width="match_parent"
android:layout_height="wrap_content"

所以可以断定这里调用的是

public void addView(View child, int index, LayoutParams params) {
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

所以我们可以通过重写带有三个参数的 addView 方法来获取 contentView

    @Override
    public void addView(View child, int index, LayoutParams params) {
        mContentView = child ;
        /*
        * 这里判断是否是 listView 的 AdapterView
        * 如果是 scrollView,也需要在此判断,
        * 这里可以扩展任意的contentView
        *  这也是关键代码之一
        * */
        if (mContentView instanceof AdapterView){
            mAdapter = (AdapterView)  mContentView;
        }
        super.addView(child, index, params);
    }

2、事件拦截的实现

通过上一篇blog我们知道了事件传递的顺序,所以想要在 ViewGroup 中相应 onTouchEvent 事件则需要在 onInterceptTouchEvent中对事件进行拦截。 具体拦截代码如下

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.v("zgy","====onInterceptTouchEvent====");
        /*是否已经拖拽,也就是是否已经拦截的意思,如果还在拦截中,继续拦截*/
        if (mIsBeginDrag){
            return true ;
        }
        if(ev.getAction() == MotionEvent.ACTION_DOWN){
            mDownY = (int) ev.getY();
        }
        if (ev.getAction() == MotionEvent.ACTION_MOVE){
            int currentY = (int) ev.getY();
            if (isIntercept(currentY-mDownY)){
                ev.setAction(MotionEvent.ACTION_DOWN);
                onTouchEvent(ev) ;
                requestDisallowInterceptTouchEvent(true);
                mIsBeginDrag = true ;
                return true ;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

具体判断拦截操作是在isIntercept方法中,进入此方法看看

    private boolean isIntercept(int distance){
        if(distance > 0){
            Log.v("zgy","====mAdapter===="+mAdapter);
            if(mAdapter != null){
                Log.v("zgy","====mAdapter===="+mAdapter);
                View firstChild =  mAdapter.getChildAt(0);
                if(firstChild != null){
                    if (firstChild.getTop() == 0){
                        return true ;
                    }
                }
            }
        }
        return false ;
    }

代码也很简单,因为这里只处理了一种控件,为了达到通用,则需要在此方法中加入判断,判断 contentView是否是 scrollView、TextView 等,根据不同的控件设置不同的拦截条件。

3、事件处理的实现

这里为了方便起见,我把事件转化成了GestureDetector的 onTouchEvent 来处理,这里面只要对一下几个方法操作即可

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }
    @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) {
        return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

还是根据上一篇 blog 的知识可以知道,在onDown方法中,必须方法 true

    @Override
    public boolean onDown(MotionEvent e) {
         /*根据我前面所讲的Android事件处理全面剖析可知,这里应该返回true*/
        return true;
    }

然后就是对onScroll的处理

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        /*这里是让下拉的View 越拉越紧,给人的感觉时越要用力*/
        distanceY = distanceY *  (0.8f * (getScrollY() * 1.0f / mHeaderHeight));
        /*设置界限,滚动的距离不能低于0,也不能高于 headerView 的高度*/
        int scrollY = cling(0, mHeaderHeight, getScrollY()+(int) distanceY) ;
        Log.v("zgy","=======onScroll===="+distanceY+",scrollY=="+scrollY+",getScrollY()="+getScrollY());
        scrollTo(0,scrollY);
        /*如果达到了下拉刷新的界限,值改变下拉刷新的状态*/
        if (scrollY < mHeaderHeight-mRefreshHeight){
            ((TextView)mTextView).setText("松开可以刷新");
            STATUS = STATUS_REFRESH ;
        }else{
            ((TextView)mTextView).setText("下拉可以刷新");
            STATUS = STATUS_HIDE ;
        }
        return true;
    }

在手指抬起的时候,需要释放拦截事件,并且根据当前状态来执行相应的操作,如果可以刷新则刷新,未达到刷新的条件这回复原位

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_UP||event.getAction() == MotionEvent.ACTION_CANCEL){
            mIsBeginDrag = false ;
            scrollNormal() ;
        }
        return mGesture.onTouchEvent(event);
    }
    private void scrollNormal(){
        if (STATUS == STATUS_REFRESH){
            STATUS = STATUS_HIDE ;
            int scroll = mHeaderHeight - mRefreshHeight -getScrollY() ;
            int currentDuration = (int) (mDuration*0.6f* scroll/(mHeaderHeight - mRefreshHeight));
            mScroller.startScroll(0,getScrollY(),0,scroll,currentDuration);
            /*测试*/
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopRefresh() ;
                }
            },1000) ;
            if(mListener != null){
                mListener.onRefresh();
            }
            mAnimView.setVisibility(VISIBLE);
            mAnimDrawable.start();
            invalidate();
        }else if(STATUS == STATUS_HIDE){
            STATUS = STATUS_NORMAL ;
            int scroll = mHeaderHeight - getScrollY() ;
            int currentDuration = mDuration* scroll/mHeaderHeight ;
            mScroller.startScroll(0,getScrollY(),0,scroll,currentDuration);
            mAnimView.setVisibility(View.INVISIBLE);
            mAnimDrawable.stop();
            invalidate();
        }
    }

这里还用到了mScroller.startScroll(0,getScrollY(),0,scroll,currentDuration);知识点,可以参考我的 blog Scroller 的运用案例(一)

4、定义刷新回调接口

    /**
     * 定义下拉刷新刷新回调接口
     */
    public interface OnRefreshListener{
        /**
         * 开始刷新
         */
        void onRefresh() ;
    }

在开始刷新的时候执行回调函数

       if (STATUS == STATUS_REFRESH){
            STATUS = STATUS_HIDE ;
            int scroll = mHeaderHeight - mRefreshHeight -getScrollY() ;
            int currentDuration = (int) (mDuration*0.6f* scroll/(mHeaderHeight - mRefreshHeight));
            mScroller.startScroll(0,getScrollY(),0,scroll,currentDuration);
            /*测试*/
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopRefresh() ;
                }
            },1000) ;
            if(mListener != null){
                mListener.onRefresh();
            }
            mAnimView.setVisibility(VISIBLE);
            mAnimDrawable.start();
            invalidate();
        }

以上就是通用型下拉刷新的实现过程。

三、总结

我喜欢在写blog 的后面加些总结,可以说是对本篇 blog 所涉及到的知识的一次巩固、并对内容的提炼,从而对本篇 blog 有一个较深的理性认知,希望大家通过我的 blog 不单单能掌握blog 中所实现的内容,更应该掌握实现内容所用到的知识点,从而扩展到其他功能。

1、自定义 ViewGroup 的实现

a,重写 onMeasure 方法,主要是测量子 View的大小

b、重写 onLayout 方法,根据需求布局子 View

2、事件拦截onInterceptTouchEvent,请参考Android 事件处理全面剖析

3、事件处理 onTouchEvent,请参考Android 事件处理全面剖析

4、GestureDetector类事件转换

5、Scroller 的运用,请参考Scroller 的运用案例(一)

说好了是通用型的下拉刷新,但是好像没实现啊,那么再来看一张TextView 的下拉刷新效果,contentView 只是一个 TextView

先看 xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <moon.pullrefresh.RefreshView
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
       <TextView
           android:gravity="center"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="@string/hello_world"/>
    <!--<ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/id_listview"></ListView>-->
</moon.pullrefresh.RefreshView>
</RelativeLayout>

再修改 RefreshView

    private boolean isIntercept(int distance){
        if(distance > 0){
            Log.v("zgy","====mAdapter===="+mAdapter);
            if(mAdapter != null){
                Log.v("zgy","====mAdapter===="+mAdapter);
                View firstChild =  mAdapter.getChildAt(0);
                if(firstChild != null){
                    if (firstChild.getTop() == 0){
                        return true ;
                    }
                }
            }else {
                if (mContentView.getTop() == 0){
                    return true ;
                }
            }
        }
        return false ;
    }

只是在原来的基础上加了两句话

else {
      if (mContentView.getTop() == 0){
          return true ;
      }
}

效果图

源码下载地址 Android打造通用的下拉刷新组件

时间: 2024-12-26 15:00:33

Android打造通用的下拉刷新组件的相关文章

打造通用的Android下拉刷新组件(适用于ListView、GridView等各类View)

前言 最近在做项目时,使用了一个开源的下拉刷新ListView组件,极其的不稳定,bug还多.稳定的组件又写得太复杂了,jar包较大.在我的一篇博客中也讲述过下拉刷新的实现,即Android打造(ListView.GridView等)通用的下拉刷新.上拉自动加载的组件.但是这种通过修改Margin的形式感觉不是特别的流畅,因此在这漫长的国庆长假又花了点时间用另外的原理实现了一遍,特此分享出来. 基本原理 原理就是自定义一个ViewGroup,将Header View, Content View,

Android打造(ListView、GridView等)通用的下拉刷新、上拉自动加载的组件

前言 下拉刷新组件在开发中使用率是非常高的,基本上联网的APP都会采用这种方式.对于开发效率而言,使用获得大家认可的开源库必然是效率最高的,但是不重复发明轮子的前提是你得自己知道轮子是怎么发明出来的,并且自己能够实现这些功能.否则只是知道其原理,并没有去实践那也就是纸上谈兵了.做程序猿,动手做才会遇到真正的问题,否则就只是自以为是的认为自己懂了.今天这篇文章就是以自己重复发明轮子这个出发点而来的,通过实现经典.使用率较高的组件来提高自己的认识.下面我们就一起来学习吧. 整体布局结构      

Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件

前言: 因为公司人员变动原因,导致了博主四个月没有动安卓,一直在做IOS开发,如今接近年前,终于可以花一定的时间放在安卓上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下目标效果以及demo效果:      因为此效果实现的步骤较多,所以今天博主要实现以上效果的第一步——打造一个通用的下拉刷新控件,具体效果如下: GIF图片比较大,还希望读者能耐心等待一下下从效果图中可以看出,我们的下拉刷新的滑动还是很流畅的,可能大多数开发者用的是XListview或者PullTo

Android内置下拉刷新组件SwipeRefreshLayout

也许下拉刷新之前,你可能会使用一些第三方的开源库,例如PullToRefresh, ActionBar-PullToRefresh等待,但现在有的正式组成部分---SwipeRefreshLayout,SwipeRefreshLayout是Google在support v4 19.1版本号的library更新的一个下拉刷新组件,使用起来非常方便,能够非常方便的实现Google Now的刷新效果. 使用官方自带的控件能够保证通用性以及风格.SwipeRefreshLayout是继承ViewGrou

React Native控件之PullToRefreshViewAndroid下拉刷新组件讲解

转载请标明出处: http://blog.csdn.net/developer_jiangqq/article/details/50664323 本文出自:[江清清的博客] (一)前言 今天我们一起来看一下PullToRefreshViewAndroid下拉刷新组件讲解以及使用实例 刚创建的React Native技术交流群(282693535),欢迎各位大牛,React Native技术爱好者加入交流!同时博客左侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送! 该PullToRefr

Google自己的下拉刷新组件SwipeRefreshLayout

SwipeRefreshLayout SwipeRefreshLayout字面意思就是下拉刷新的布局,继承自ViewGroup,在support v4兼容包下,但必须把你的support library的版本升级到19.1. 提到下拉刷新大家一定对ActionBarPullToRefresh比较熟悉,而如今google推出了更官方的下拉刷新组件,这无疑是对开发者来说比较好的消息.利用这个组件可以很方便的实现Google Now的刷新效果,见下图: 主要方法 setOnRefreshListene

自定义View——利用下拉刷新组件实现上拉加载

注:本文demo已经提交github,地址完整代码如下,demo工程已经上传至GitHub, github地址https://github.com/wsclwps123/UpLoadSwipeRefreshLayout 感谢大家支持! 在Android开发中,我们经常会用到列表下拉刷新和上拉加载的功能. Google在support.v4包中提供了一个组件可以用来进行下来刷新,这个组件是SwipeRefreshLayout. 下面我们来看一下这个组件的使用: 在布局文件中加上xml代码 <and

SuperSwipeRefreshLayout 一个功能强大的自定义下拉刷新组件

SuperSwipeRefreshLayout 一个功能强大的自定义下拉刷新组件. Why? 下拉刷新这种控件,想必大家用的太多了,比如使用很多的XListView等.最近,项目中很多列表都是使用ReyclerView实现的,代替了原有的ListView,原有下拉刷新方式遭到挑战.本来Google推出的SwipeRefreshLayout已经能够满足大部分的需求了.然而,由于其定制性较差,下拉刷新的样式无法修改,而且被嵌套的View也无法跟随手指的滑动而滑动.基于以上考虑,定制自己强大的Supe

SuperSwipeRefreshLayout 一个功能强大的自己定义下拉刷新组件

SuperSwipeRefreshLayout 一个功能强大的自己定义下拉刷新组件. Why? 下拉刷新这样的控件.想必大家用的太多了,比方使用非常多的XListView等. 近期.项目中非常多列表都是使用ReyclerView实现的.取代了原有的ListView,原有下拉刷新方式遭到挑战.本来Google推出的SwipeRefreshLayout已经能够满足大部分的需求了. 然而,因为其定制性较差.下拉刷新的样式无法改动.并且被嵌套的View也无法尾随手指的滑动而滑动.基于以上考虑,定制自己强