Android自定义控件实战——实现仿IOS下拉刷新上拉加载 PullToRefreshLayout
2016-05-18 14:32
666 查看
下拉刷新控件,网上有很多版本,有自定义Layout布局的,也有封装控件的,各种实现方式的都有。但是很少有人告诉你具体如何实现的,今天我们就来一步步实现自己封装的 PullToRefreshLayout 完美的解决下拉刷新,上拉加载问题。
首先来分析一下原理,为什么一下拉就可以拉出来一个布局,请看下图,从图中可以看到整个屏幕来说有可见部分,有隐藏部分,当我们手指在屏幕上下拉的时候滑动距离到一定程度了就会拉出 下拉头布局,这样就达到了下拉效果。那么具体代码如何实现待我慢慢像大家解析。
1、想要实现 PullToRefreshLayout 下拉刷新控件那么我们就必须要有个容器,也就是如上图的容器,知道了需要什么那么我们就开始自定义一个容器。
这里如果不会自定义控件的同学可以参考博客 http://blog.csdn.net/cscfas/article/details/51330505
2、有了容器,接下来就拉实现下拉头,上拉脚。LinearLayout 我们都用过线性布局嘛,在这里要注意 android:orientation=“vertical” 只能是垂直布局。这里重写了该控件,目的是在代码中动态添加布局到控件中,实现组合控件,就是PullToRefreshLayout ,这里调用了LinearLayout 的addView()
方法将布局添加到PullToRefreshLayout中。
(1)、添加头部布局,这里也就是下拉头
(2)、添加脚部布局,这里也就是 上拉脚
3、知道了 下拉头,上拉脚 怎么实现了,接下来就看在哪里加入到 PullToRefreshLayout控件中,又是如何实现动画的。请看下面代码。
(1)、这里动画实现的是刷新箭头的方向旋转,最后一行 addHeaderView() 实现了头部的添加。
(2)、知道了头部如何加入PullToRefreshLayout中,那么底部是如何添加的呢!其实底部的加入是有技巧的,接下来请看代码。onFihishInflate()
看到@Override 你就知道这个函数是 LinearLayout 提供,那么它有何作用呢,它的作用就是在所有的XML和头部布局都添加了的情况下加入 脚部布局。
(3)、在上面的代码中你会看到 initContentAdapterView() 这个函数,你会想它又是什么鬼,它有什么作用呢?请看代码。
如果你有了解过,我的上一篇博客:http://blog.csdn.net/cscfas/article/details/51330505 ;那么你就知道在自定义控件中,如果XML布局中引入了控件,会加载该自定义控件的第二个构造函数,那么addHeaderView() 会被加载到布局中,PullToRefreshLayout
在xml 中加入的布局也会被添加到控件中。该布局可以包裹 ListView 和 GridView 及 ScrollView 控件。
4、接下来看下项目中用到的常量和变量注释,这对阅读后续代码有帮助。
5、了解了布局如何实现,接下来就到了手势如何实现,也就是我们下拉为什么可以拉出 下拉头,这里涉及到手势相关的概念,如果不了解手可以参考博客:http://blog.csdn.net/cscfas/article/details/51372342
这里就不讲事件是如何拦截,如何分发的了,我们重点来看如下代码,这里在 ACTION_DOWN时并没有拦截事件只是记录下了 Y轴坐标,为什么呢?因为PullToRefreshLayout 是属于
ViewGroup 容器型的控件,如果ACTION_DOWN
直接被拦截了那么 ListVeiw 和 GridView 中的 item点击事件及 ScrollView中点击事件和长按事件将无法触发。
细心的同学会发现在 ACTION_MOVE 中有调用 isRefreshViewScroll() 函数,那么它又有什么功能呢!仔细看代码会发现它返回了一个 boolean 类型的值,是它控制这事件是否拦截,看到这里你是不是觉得它至关重要,那么就来分析一下它的结构吧!
mAdapterView 这个控件从何而来,有认真看过上面代码你就应该知道了。那么它是何方神圣呢?它就是 适配器填充控件后得到的结果,AdapterView 是适配器和控件的组合,这里主要是拿到AdapterView中的子控件,也就是ListView或
GridView中的Item,通过获取子控件的状态来动态设置 是否要拦截手势,以及设置 mPullState 状态。
mScrollView 控件也是同理,拿到子控件的状态来判断是否要拦截事件。具体代码都有注释请看代码,这里就不详解了。
6、接下来就要见证奇迹了,就是具体如何实现 下拉刷新上拉加载更多效果的业务了,还记得上面我们有讲到手势拦截吧!如果你了解手势就知道被拦截后会执行什么函数,那就是 onTouchEvent() 函数了。
(1)、首先看下 ACTION_MOVE 这里我们来计算用户手指在屏幕上的滑动距离,还记得在 onInterceptTounchEvent()中已经对 mPullState 状态做过改变,这里开始就通过判断当前状态是下拉还是上拉来处理 HeaderView 和 FootView的显示及动画效果。
(2)、处理下拉或上拉布局被拉出效果,接下来看 headPrepareToRefresh() 和 footerPrepareToRefresh() 这两个函数实现了上拉及下拉效果 ,这里要注意 mHeaderState、mFooterState 的状态改变,它决定这是否释放刷新
(3)、仔细阅读上面代码,会发现所有的判断跟随着这个 headerPrepareToRefresh() 函数的返回值决定,接下来看下这个函数。判断当前 mPullState 状态 及 拉动距离是否大于设置距离,动态返回 TopMargin 及拉出的距离
(4)、ACTION_UP、ACTION_CANCEL 处理释放刷新和取消执行刷新,首先拿到 topMargin 既拉动的距离,通过判断拉动距离和 mPullState 状态来决定是释放刷新还是取消执行刷新。
(5)、headerRefreshing() 、footerRefreshing() 释放刷新,这里将 Runnable 添加到UI线程中,延迟1500 毫秒达到,下拉头或上拉脚停顿效果,这里主要回调监听接口,该接口是调用 PullToRefreshLayout 控件的 Activity或FrangMent 中实现。
(5)、注意在刷新失败的时候会执行 setHeaderMargin() 该函数作用主要是实现布局的隐藏
7、以上步骤基本实现了整个下拉刷新,上拉加载的功能,但是美中不足,刷新完成后我们还需要隐藏我们的布局,下面的代码是更新完后恢复初始化状态
8、以上基本实现了下拉刷新上拉加载,博客也写累了,剩余的功能我就不贴代码了,可以参看Demo
下载地址:http://download.csdn.net/detail/cscfas/9524306
首先来分析一下原理,为什么一下拉就可以拉出来一个布局,请看下图,从图中可以看到整个屏幕来说有可见部分,有隐藏部分,当我们手指在屏幕上下拉的时候滑动距离到一定程度了就会拉出 下拉头布局,这样就达到了下拉效果。那么具体代码如何实现待我慢慢像大家解析。
1、想要实现 PullToRefreshLayout 下拉刷新控件那么我们就必须要有个容器,也就是如上图的容器,知道了需要什么那么我们就开始自定义一个容器。
这里如果不会自定义控件的同学可以参考博客 http://blog.csdn.net/cscfas/article/details/51330505
/** * Created by ZQY on 2016/5/17. * <p/> * 这个是上拉加载和下拉刷新的 View * <p/> * 注:这里的 android:orientation="vertical" 只能为这个值 */ public class PullToRefreshLayout extends LinearLayout { public PullToRefreshLayout(Context context) { super(context); initAnim(); } public PullToRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); initAnim(); } public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAnim(); } }
2、有了容器,接下来就拉实现下拉头,上拉脚。LinearLayout 我们都用过线性布局嘛,在这里要注意 android:orientation=“vertical” 只能是垂直布局。这里重写了该控件,目的是在代码中动态添加布局到控件中,实现组合控件,就是PullToRefreshLayout ,这里调用了LinearLayout 的addView()
方法将布局添加到PullToRefreshLayout中。
(1)、添加头部布局,这里也就是下拉头
private void addHeaderView() { mHeaderView = mInflater.inflate(R.layout.refresh_header, this, false); mHeaderImageView = (ImageView) mHeaderView .findViewById(R.id.pull_to_refresh_image); mHeaderTextView = (TextView) mHeaderView .findViewById(R.id.pull_to_refresh_text); mHeaderUpdateTextView = (TextView) mHeaderView .findViewById(R.id.pull_to_refresh_updated_at); mHeaderUpdateTextView.setText(DataUtil.getRefreshCompleteTime()); mHeaderProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress); measureView(mHeaderView); mHeaderViewHeight = mHeaderView.getMeasuredHeight(); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderViewHeight); //设置 topMargin 的值为负的 header View 高度,即将其隐藏在最上方 params.topMargin = -(mHeaderViewHeight); //添加头部到布局 addView(mHeaderView, params); }
(2)、添加脚部布局,这里也就是 上拉脚
private void addFooterView() { mFooterView = mInflater.inflate(R.layout.refresh_footer, this, false); mFooterImageView = (ImageView) mFooterView .findViewById(R.id.pull_to_load_image); mFooterTextView = (TextView) mFooterView .findViewById(R.id.pull_to_load_text); mFooterProgressBar = (ProgressBar) mFooterView .findViewById(R.id.pull_to_load_progress); // 底部布局 measureView(mFooterView); mFooterViewHeight = mFooterView.getMeasuredHeight(); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mFooterViewHeight); /** * int top = getHeight(); params.topMargin=getHeight();//在这里getHeight()==0,但在onInterceptTouchEvent()方法里getHeight()已经有值了,不再是0; getHeight()什么时候会赋值,稍候再研究一下 由于是线性布局可以直接添加,只要AdapterView的高度是MATCH_PARENT,那么footer view就会被添加到最后,并隐藏 */ addView(mFooterView, params); }看完以上代码你肯定会想就这么简单嘛!当然不是,细心的同学会发现两个函数都有调用 measureView()函数,它是干嘛的呢!下面就来看下这个函数,这个函数看起来代码和注释很多,这里的功能无非就是计算子控件在父控件中的大小。
private void measureView(View child) { /** * child.getLayoutParams(); * * 返回 该视图的布局参数 * * 此视图的父视图指定如何安排它的供应参数 * */ ViewGroup.LayoutParams p = child.getLayoutParams(); if (p == null) { /** * 用指定的 宽度和高度 创建一组新的布局参数 * * @param width 宽度,或者 {@link #WRAP_CONTENT}, * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in * API Level 8),或一个固定大小的像素 * @param height 高度,或者 {@link #WRAP_CONTENT}, * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in * API Level 8), 或一个固定大小的像素 */ p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } /** * 是否measureChildren困难的部分:搞清楚MeasureSpec传递给特定的子控件。这种方法计算出正确的MeasureSpec一个子视图中的一维(高度或宽度)。 * 目标是信息从我们MeasureSpec与子控件的的LayoutParams结合,以获得最佳的可能结果。例如,如果这个观点知道它的大小(因为它MeasureSpec有整整模式), * 子控件在其的LayoutParams已经表示,它想成为的尺寸与父控件一样,父控件应让子控件布置给精确的尺寸。 * @param spec 该视图的要求 * @param padding 该视图为当前维的填充和利润(如果适用) * * @param childDimension 希望为子控件设置的尺寸 * @return MeasureSpec 一个MeasureSpec整数为孩子 * */ int childWidthSpec=ViewGroup.getChildMeasureSpec(0,0+0,p.width); int lpHeight = p.height; int childHeightSpec; if (lpHeight > 0) { /** * 创建基于所提供的大小和模式的量度规范。该模式必须是下列之一: UNSPECIFIED EXACTLY AT_MOST * @param size 该措施说明书的大小 * @param mode 该措施规范的模式 * @return 基于规模和模式的措施规范 */ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } /** * 这就是所谓的大一个视图应该如何。父控件 约束信息的宽度和高度参数。 一个视图的实际测量工作是在onMeasure(int,int),称为该方法。因此,只有onMeasure(int,int)可以而且必须由子类重写。 @param widthMeasureSpec 横向空间的需求添加到的父控件大小 @param heightMeasureSpec 垂直间距需求添加到的父控件大小 */ child.measure(childWidthSpec, childHeightSpec); }
3、知道了 下拉头,上拉脚 怎么实现了,接下来就看在哪里加入到 PullToRefreshLayout控件中,又是如何实现动画的。请看下面代码。
(1)、这里动画实现的是刷新箭头的方向旋转,最后一行 addHeaderView() 实现了头部的添加。
/** * 初始化动画 */ private void initAnim() { //加载所有的动画,我们需要的代码,而不是通过 XML mFlipAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); //设置动画 均速 mFlipAnimation.setInterpolator(new LinearInterpolator()); /** * 动画应该持续多久,持续时间不能为负 * @param durationMillis * @throws java.lang.IllegalArgumentException 如果 durationMillis < 0 * @attr 参考 R.styleable #Animation_duration */ mFlipAnimation.setDuration(250); /** * 如果 fillafter 是 true ,这个动画进行改造将坚持当它完成。 * 默认为 false ,如果不设置。 * *请注意,这适用于个别动画,当使用 {@link android.view.animation.AnimationSet AnimationSet} 链动画 * * @param fillAfter 如果动画结束后,动画应该应用它的转换 * @attr ref android.R.styleable#Animation_fillAfter * * @see #setFillEnabled(boolean) */ mFlipAnimation.setFillAfter(true); /** *构造函数使用时建立一个rotateanimation 对象 * * * @param fromDegrees 在动画开始时应用旋转偏移。 * * @param toDegrees 在动画结束时应用旋转偏移。 * * @param pivotXType 指定如何pivotxvalue应解释。什么之中的一个 * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or * Animation.RELATIVE_TO_PARENT. * * @param pivotXValue X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。 * * * @param pivotYType 指定如何pivotyvalue应解释。什么之中的一个 * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or * Animation.RELATIVE_TO_PARENT. * * @param pivotYValue X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。 */ mReverseFlipAnimation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); //设置此动画的加速曲线。默认为线性插值。 这里是匀速 mReverseFlipAnimation.setInterpolator(new LinearInterpolator()); mReverseFlipAnimation.setDuration(250); mReverseFlipAnimation.setFillAfter(true); mInflater = LayoutInflater.from(getContext()); // header view 在此添加,保证是第一个添加到linearlayout的最上端 addHeaderView(); }
(2)、知道了头部如何加入PullToRefreshLayout中,那么底部是如何添加的呢!其实底部的加入是有技巧的,接下来请看代码。onFihishInflate()
看到@Override 你就知道这个函数是 LinearLayout 提供,那么它有何作用呢,它的作用就是在所有的XML和头部布局都添加了的情况下加入 脚部布局。
/** * 完成 填充 XML格式的视图。这就是所谓的 UI填充 的最后阶段,所有子视图已被添加之后。 即使子类覆盖onFinishInflate,他们应始终确保调用超级方法,使我们得到调用。 既必须调用 super.onFinishInflate(); */ @Override protected void onFinishInflate() { super.onFinishInflate(); // footer view 在此添加保证添加到linearlayout中的最后 addFooterView(); initContentAdapterView(); }
(3)、在上面的代码中你会看到 initContentAdapterView() 这个函数,你会想它又是什么鬼,它有什么作用呢?请看代码。
如果你有了解过,我的上一篇博客:http://blog.csdn.net/cscfas/article/details/51330505 ;那么你就知道在自定义控件中,如果XML布局中引入了控件,会加载该自定义控件的第二个构造函数,那么addHeaderView() 会被加载到布局中,PullToRefreshLayout
在xml 中加入的布局也会被添加到控件中。该布局可以包裹 ListView 和 GridView 及 ScrollView 控件。
/** * * 初始化 adapterview像ListView,GridView等;或init ScrollView */ private void initContentAdapterView(){ int count=getChildCount(); if (count<3) throw new IllegalArgumentException( "this layout must contain 3 child views,and AdapterView or ScrollView must in the second position!"); View view=null; for (int i=0;i<count-1;++i){ view=getChildAt(i); if (view instanceof AdapterView<?>){ System.out.println("the type is AdapterView"); mAdapterView=(AdapterView<?>)view; } if (view instanceof ScrollView){ System.out.println("thie type is ScrollView"); mScrollView= (ScrollView) view; } } if (mAdapterView==null&&mScrollView==null){ throw new IllegalArgumentException( "must contain a AdapterView or ScrollView in this layout!"); } }
4、接下来看下项目中用到的常量和变量注释,这对阅读后续代码有帮助。
/** * 下拉刷新 */ private static final int PULL_TO_REFRESH = 2; /** * 释放刷新 */ private static final int RELEASE_TO_REFRESH = 3; /** * 刷新 */ private static final int REFRESHING = 4; /** * 上拉加载 */ private static final int PULL_UP_STATE = 10; /** * 下拉刷新 */ private static final int PULL_DOWN_STATE = 11; /** * 最后Y轴距离 */ private int mLastMotionY; /** * 锁定 */ private boolean mLock;
5、了解了布局如何实现,接下来就到了手势如何实现,也就是我们下拉为什么可以拉出 下拉头,这里涉及到手势相关的概念,如果不了解手可以参考博客:http://blog.csdn.net/cscfas/article/details/51372342
这里就不讲事件是如何拦截,如何分发的了,我们重点来看如下代码,这里在 ACTION_DOWN时并没有拦截事件只是记录下了 Y轴坐标,为什么呢?因为PullToRefreshLayout 是属于
ViewGroup 容器型的控件,如果ACTION_DOWN
直接被拦截了那么 ListVeiw 和 GridView 中的 item点击事件及 ScrollView中点击事件和长按事件将无法触发。
/** * 事件拦截 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int y = (int) ev.getRawY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //手指按下时记录 Y轴坐标 // 首先拦截down事件,记录y坐标 mLastMotionY = y; break; case MotionEvent.ACTION_MOVE: //滑动时 拿到移动距离 判断是否拦截手势 // deltaY > 0 是向下运动,< 0是向上运动 int deltaY = y - mLastMotionY; if (isRefreshViewScroll(deltaY)) { // System.out.println("正在移动:返回true"); return true; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return false; }
细心的同学会发现在 ACTION_MOVE 中有调用 isRefreshViewScroll() 函数,那么它又有什么功能呢!仔细看代码会发现它返回了一个 boolean 类型的值,是它控制这事件是否拦截,看到这里你是不是觉得它至关重要,那么就来分析一下它的结构吧!
mAdapterView 这个控件从何而来,有认真看过上面代码你就应该知道了。那么它是何方神圣呢?它就是 适配器填充控件后得到的结果,AdapterView 是适配器和控件的组合,这里主要是拿到AdapterView中的子控件,也就是ListView或
GridView中的Item,通过获取子控件的状态来动态设置 是否要拦截手势,以及设置 mPullState 状态。
mScrollView 控件也是同理,拿到子控件的状态来判断是否要拦截事件。具体代码都有注释请看代码,这里就不详解了。
/** * 是否应该到了父View,即PullToRefreshView滑动 * * @param deltaY * , deltaY > 0 是向下运动,< 0是向上运动 * @return */ private boolean isRefreshViewScroll(int deltaY) { // 当头部状态是 刷新 或 底部状态是刷新时 返回 false 不拦截 if (mHeaderState == REFRESHING || mFooterState == REFRESHING) { return false; } //对于ListView和GridView if (mAdapterView != null) { // 子view(ListView or GridView)滑动到最顶端 if (deltaY > 0) { View child = mAdapterView.getChildAt(0); if (child == null) { //设置状态为下拉刷新 mPullState = PULL_DOWN_STATE; //设置状态为拦截 return true; } // 适配中 第一个控件高度为 0 且 第一个控件可见 if (mAdapterView.getFirstVisiblePosition() == 0 && child.getTop() == 0) { //设置状态为下拉刷新 mPullState = PULL_DOWN_STATE; return true; } int top = child.getTop(); int padding = mAdapterView.getPaddingTop(); if (mAdapterView.getFirstVisiblePosition() == 0 && Math.abs(top - padding) <= 8) {//这里之前用3可以判断,但现在不行,还没找到原因 mPullState = PULL_DOWN_STATE; return true; } } else if (deltaY < 0) { //如果移动的距离为 负值 //获取适配中最后一个控件 View lastChild = mAdapterView.getChildAt(mAdapterView .getChildCount() - 1); if (lastChild == null) { mPullState = PULL_UP_STATE; // 如果mAdapterView中没有数据,不拦截 return true; } // 最后一个子view的Bottom小于父View的高度说明mAdapterView的数据没有填满父view, // 等于父View的高度说明mAdapterView已经滑动到最后 if (lastChild.getBottom() <= getHeight() && mAdapterView.getLastVisiblePosition() == mAdapterView .getCount() - 1) { mPullState = PULL_UP_STATE; return true; } } } // 对于ScrollView if (mScrollView != null) { // 子scroll view滑动到最顶端 View child = mScrollView.getChildAt(0); //当移动距离为 正值 且滚动条没有滚动 if (deltaY > 0 && mScrollView.getScrollY() == 0) { mPullState = PULL_DOWN_STATE; //设置状态为下拉刷新 return true; } else if (deltaY < 0 && child.getMeasuredHeight() <= getHeight() + mScrollView.getScrollY()) { mPullState = PULL_UP_STATE; //设置为上拉加载 return true; } } return false; }
6、接下来就要见证奇迹了,就是具体如何实现 下拉刷新上拉加载更多效果的业务了,还记得上面我们有讲到手势拦截吧!如果你了解手势就知道被拦截后会执行什么函数,那就是 onTouchEvent() 函数了。
(1)、首先看下 ACTION_MOVE 这里我们来计算用户手指在屏幕上的滑动距离,还记得在 onInterceptTounchEvent()中已经对 mPullState 状态做过改变,这里开始就通过判断当前状态是下拉还是上拉来处理 HeaderView 和 FootView的显示及动画效果。
/* * 如果在onInterceptTouchEvent()方法中没有拦截(即onInterceptTouchEvent()方法中 return false) * * 则由PullToRefreshView 的子View来处理;否则由下面的方法来处理(即由PullToRefreshView自己来处理) */ @Override public boolean onTouchEvent(MotionEvent event) { if (mLock) { //当处于锁定状态时 return true; } //拿到Y轴坐标 int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //手指按下时触发 ACTION_DOWN // onInterceptTouchEvent已经记录 // mLastMotionY = y; break; case MotionEvent.ACTION_MOVE: //手指在屏幕上滑动时触发 ACTION_MOVE //拿到用户滑动的距离 int deltaY = y - mLastMotionY; if (mPullState == PULL_DOWN_STATE) { //如果当前状态处于下拉刷新 PULL_DOWN_STATE 那么执行 headerPrepareToRefresh() 函数实现刷新效果 // PullToRefreshView执行下拉 Log.i(TAG, " pull down!parent view move!"); headerPrepareToRefresh(deltaY); // setHeaderPadding(-mHeaderViewHeight); } else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载 PULL_UP_STATE if (pullUpLoad) { //判断用户是否启用上拉加载 // PullToRefreshView执行上拉 Log.i(TAG, "pull up!parent view move!"); footerPrepareToRefresh(deltaY); } } mLastMotionY = y; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: //当事件被取消时 //获取当前header view 的topMargin 值 int topMargin = getHeaderTopMargin(); if (mPullState == PULL_DOWN_STATE) { //如果当前状态是下拉刷新 if (topMargin >= 0) { // 开始刷新 headerRefreshing(); } else { // 还没有执行刷新,重新隐藏 setHeaderTopMargin(-mHeaderViewHeight); } } else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载 if (pullUpLoad) { if (Math.abs(topMargin) >= mHeaderViewHeight + mFooterViewHeight) { // 开始执行footer 刷新 footerRefreshing(); } else { // 还没有执行刷新,重新隐藏 setHeaderTopMargin(-mHeaderViewHeight); } } } break; } return super.onTouchEvent(event); }
(2)、处理下拉或上拉布局被拉出效果,接下来看 headPrepareToRefresh() 和 footerPrepareToRefresh() 这两个函数实现了上拉及下拉效果 ,这里要注意 mHeaderState、mFooterState 的状态改变,它决定这是否释放刷新
/** * header 准备刷新,手指移动过程,还没有释放 * * @param deltaY * ,手指滑动的距离 */ private void headerPrepareToRefresh(int deltaY) { int newTopMargin = changingHeaderViewTopMargin(deltaY); // 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态 if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) { mHeaderTextView.setText(R.string.pull_to_refresh_release_label); mHeaderUpdateTextView.setVisibility(View.VISIBLE); mHeaderImageView.clearAnimation(); mHeaderImageView.startAnimation(mFlipAnimation); //改变状态为释放刷新 mHeaderState = RELEASE_TO_REFRESH; } else if (newTopMargin < 0 && newTopMargin > -mHeaderViewHeight) {// 拖动时没有释放 mHeaderImageView.clearAnimation(); mHeaderImageView.startAnimation(mFlipAnimation); mHeaderTextView.setText(R.string.pull_to_refresh_pull_label); mHeaderState = PULL_TO_REFRESH; } }
/** * footer 准备刷新,手指移动过程,还没有释放 移动footer view高度同样和移动header view * 高度是一样,都是通过修改header view的topmargin的值来达到 * * @param deltaY * ,手指滑动的距离 */ private void footerPrepareToRefresh(int deltaY) { int newTopMargin = changingHeaderViewTopMargin(deltaY); // 如果header view topMargin 的绝对值大于或等于header + footer 的高度 // 说明footer view 完全显示出来了,修改footer view 的提示状态 if (Math.abs(newTopMargin) >= (mHeaderViewHeight + mFooterViewHeight) && mFooterState != RELEASE_TO_REFRESH) { mFooterTextView .setText(R.string.pull_to_refresh_footer_release_label); mFooterImageView.clearAnimation(); mFooterImageView.startAnimation(mFlipAnimation); mFooterState = RELEASE_TO_REFRESH; } else if (Math.abs(newTopMargin) < (mHeaderViewHeight + mFooterViewHeight)) { mFooterImageView.clearAnimation(); mFooterImageView.startAnimation(mFlipAnimation); mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label); mFooterState = PULL_TO_REFRESH; } }
(3)、仔细阅读上面代码,会发现所有的判断跟随着这个 headerPrepareToRefresh() 函数的返回值决定,接下来看下这个函数。判断当前 mPullState 状态 及 拉动距离是否大于设置距离,动态返回 TopMargin 及拉出的距离
/** * 修改Header view top margin的值 * * @description * @param deltaY */ private int changingHeaderViewTopMargin(int deltaY) { LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams(); float newTopMargin = params.topMargin + deltaY * 0.4f; //这里对上拉做一下限制,因为当前上拉后然后不释放手指直接下拉,会把下拉刷新给触发了 //表示如果是在上拉后一段距离,然后直接下拉 if(deltaY>0&&mPullState == PULL_UP_STATE&&Math.abs(params.topMargin) <= mHeaderViewHeight){ return params.topMargin; } //同样地,对下拉做一下限制,避免出现跟上拉操作时一样的bug if(deltaY<0&&mPullState == PULL_DOWN_STATE&&Math.abs(params.topMargin)>=mHeaderViewHeight){ return params.topMargin; } params.topMargin = (int) newTopMargin; mHeaderView.setLayoutParams(params); /** * 无效整个视图。如果视图是可见的, * * {@link #onDraw(android.graphics.Canvas)} 将在某个时候被调用 * * 这必须从UI线程调用。从非UI线程,致电致电 * * {@link #postInvalidate()}. */ invalidate(); return params.topMargin; }
(4)、ACTION_UP、ACTION_CANCEL 处理释放刷新和取消执行刷新,首先拿到 topMargin 既拉动的距离,通过判断拉动距离和 mPullState 状态来决定是释放刷新还是取消执行刷新。
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: //当事件被取消时 //获取当前header view 的topMargin 值 int topMargin = getHeaderTopMargin(); if (mPullState == PULL_DOWN_STATE) { //如果当前状态是下拉刷新 if (topMargin >= 0) { // 开始刷新 headerRefreshing(); } else { // 还没有执行刷新,重新隐藏 setHeaderTopMargin(-mHeaderViewHeight); } } else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载 if (pullUpLoad) { if (Math.abs(topMargin) >= mHeaderViewHeight + mFooterViewHeight) { // 开始执行footer 刷新 footerRefreshing(); } else { // 还没有执行刷新,重新隐藏 setHeaderTopMargin(-mHeaderViewHeight); } } } break;
(5)、headerRefreshing() 、footerRefreshing() 释放刷新,这里将 Runnable 添加到UI线程中,延迟1500 毫秒达到,下拉头或上拉脚停顿效果,这里主要回调监听接口,该接口是调用 PullToRefreshLayout 控件的 Activity或FrangMent 中实现。
/** * 下拉头释放刷新 * */ private void headerRefreshing() { mHeaderState = REFRESHING; setHeaderTopMargin(0); mHeaderImageView.setVisibility(View.GONE); mHeaderImageView.clearAnimation(); mHeaderImageView.setImageDrawable(null); mHeaderProgressBar.setVisibility(View.VISIBLE); mHeaderTextView.setText(R.string.pull_to_refresh_refreshing_label); if (mOnHeaderRefreshListener != null) { /** * 使Runnable被添加到消息队列,经过规定的时间之后运行。 * * 运行将运行在用户界面线程。既UI线程中 */ this.postDelayed(new Runnable() { @Override public void run() { mOnHeaderRefreshListener.onHeaderRefresh(PullToRefreshView.this); } }, 1500); } }
/** * 底部释放刷新 */ private void footerRefreshing() { mFooterState = REFRESHING; int top = mHeaderViewHeight + mFooterViewHeight; setHeaderTopMargin(-top); mFooterImageView.setVisibility(View.GONE); mFooterImageView.clearAnimation(); mFooterImageView.setImageDrawable(null); mFooterProgressBar.setVisibility(View.VISIBLE); mFooterTextView .setText(R.string.pull_to_refresh_footer_refreshing_label); if (mOnFooterRefreshListener != null) { this.postDelayed(new Runnable() { @Override public void run() { mOnFooterRefreshListener.onFooterRefresh(PullToRefreshView.this); } }, 1500); } }
(5)、注意在刷新失败的时候会执行 setHeaderMargin() 该函数作用主要是实现布局的隐藏
/** * 设置header view 的topMargin的值 * * @description * @param topMargin * ,为0时,说明header view 刚好完全显示出来; 为-mHeaderViewHeight时,说明完全隐藏了 */ private void setHeaderTopMargin(int topMargin) { LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams(); params.topMargin = topMargin; mHeaderView.setLayoutParams(params); invalidate(); }
7、以上步骤基本实现了整个下拉刷新,上拉加载的功能,但是美中不足,刷新完成后我们还需要隐藏我们的布局,下面的代码是更新完后恢复初始化状态
/** * header view 完成更新后恢复初始状态 * * @description hylin 2012-7-31上午11:54:23 */ public void onHeaderRefreshComplete() { setHeaderTopMargin(-mHeaderViewHeight); mHeaderImageView.setVisibility(View.VISIBLE); mHeaderImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow); mHeaderTextView.setText(R.string.pull_to_refresh_pull_label); mHeaderProgressBar.setVisibility(View.GONE); mHeaderState = PULL_TO_REFRESH; }
/** * footer view 完成更新后恢复初始状态 */ public void onFooterRefreshComplete() { setHeaderTopMargin(-mHeaderViewHeight); mFooterImageView.setVisibility(View.VISIBLE); mFooterImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow_up); mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label); mFooterProgressBar.setVisibility(View.GONE); mFooterState = PULL_TO_REFRESH; }
8、以上基本实现了下拉刷新上拉加载,博客也写累了,剩余的功能我就不贴代码了,可以参看Demo
下载地址:http://download.csdn.net/detail/cscfas/9524306
相关文章推荐
- Android.TextUtils 简单的工具类
- mac 系统开发android,真机调试解决方案(无数的坑之后吐血总结)
- Android内存优化之——static使用篇(使用MAT工具进行分析)
- 关于Android studio的使用设置
- Android内存优化之——static使用篇
- Bundle源码解析
- Android 设置背景边框
- Android MotionEvent详解
- 谷歌整合登入系统到Android应用
- Windows环境下Android Studio-------简单设置
- Android frameworks去掉熄屏前先变暗的功能
- Android Studio系列教程五--Gradle命令详解与导入第三方包
- 【团队分享】刀锋铁骑:常见Android Native崩溃及错误原因
- 关于JNI配置Error: Could not find class file for 'XXX'问题(MAC Andriod Studio)
- 史上最详细的Android Studio系列教程三--快捷键
- ConvenientBanner框架实现广告浏览
- 史上最详细的Android Studio系列教程四--Gradle基础
- Android Studio系列教程二--基本设置与运行
- 使用RecyclerView + ViewPager 实现轮播广告的两个Bug
- android 不能接收组播数据,但能够发送组播数据