前言

第三篇下拉刷新的博客来的稍微有点晚,因为前两篇的博客访问量一直不是很高,所以博主花了点时间修改了整体的Demo效果,处理了很多极端下拉情况下的显示问题,给大家呈现一个完美的下拉刷新控件.因为本文不介绍贝塞尔曲线的实现,所以如有对贝塞尔曲线感兴趣的读者,可以阅读博主的上一篇博客( Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果)即可.好了,按照惯例,我们先来看下我们今天要实现的最终效果:

  

图片稍微有点大,还请读者们稍微等待一下-_-!,总的来说,效果和QQ还是很相似的,废话不多说了,下面就跟随博主去实现吧!

一、自定义LinearLayout并初始化布局

下拉头部的布局实现其实很简单,包含了一套重叠布局,即上层为贝塞尔小球,下层为我们的刷新成功的提示布局,通过对布局的隐藏和显示来动态显示状态,下面贴出我们的header的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="match_parent"android:gravity="center|top"android:background="#ffffff"><LinearLayoutandroid:id="@+id/ll_ok"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="horizontal"android:visibility="gone"><ImageViewandroid:id="@+id/iv_ok"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/pull_ok" /><TextViewandroid:id="@+id/tv_ok"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginLeft="10dp"android:gravity="center"android:text="刷新成功"android:textSize="14sp"android:textAppearance="?android:attr/textAppearance"android:textColor="#999999"android:textStyle="bold" /></LinearLayout><LinearLayoutandroid:id="@+id/ll_refresh"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center|top"><ProgressBarandroid:id="@+id/pb_refresh"style="?android:attr/progressBarStyleSmall"android:layout_width="15dp"android:layout_height="15dp"android:layout_centerVertical="true"android:layout_gravity="center"android:indeterminate="true"android:indeterminateDrawable="@drawable/pulling"android:visibility="gone" /><com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.YPXBezierViewandroid:id="@+id/bview"android:layout_width="match_parent"android:layout_height="match_parent" /></LinearLayout>
</RelativeLayout>

其中的YPXBezierView就是我们的贝塞尔小球,主要原理是通过自定义View来绘制路径得到的(上一篇博客有介绍),可以看到,我们的贝塞尔小球什么都不需要设置,宽高全部填满父控件即可.

接下来,就是自定义LinearLayout了,首先我们看一下申明的变量:

    /*** 下拉刷新状态*/public static final int REFRESH_BY_PULLDOWN = 0;/*** 松开刷新状态*/public static final int REFRESH_BY_RELEASE = 1;/*** 正在刷新状态*/public static final int REFRESHING = 2;/*** 刷新成功状态*/public static final int REFRESHING_SUCCESS = 3;/*** 刷新失败状态*/public static final int REFRESHING_FAILD = 4;/*** 收回到刷新位置状态*/public static final int TAKEBACK_REFRESH = -1;/*** 收回到初始位置状态*/public static final int TAKEBACK_RESET = -2;/*** 从头收到尾,不考虑中间状态*/public static final int TAKEBACK_ALL = -3;private int refreshTargetTop=dp(-60);//刷新头部高度ObjectAnimator anim;//下拉刷新布局private View refreshView;LinearLayout ll_ok;LinearLayout ll_refresh;ImageView iv_ok;TextView tv_ok;ProgressBar pb_refresh;YPXBezierView bezierView;private RefreshListener refreshListener;private int lastY;private int lastTop;/*** 刷新状态*/int refreshState = REFRESH_BY_PULLDOWN;/*** 收回状态*/int takeBackState = TAKEBACK_RESET;/*** 是否可刷新标记*/private boolean isRefreshEnabled = true;private float topCircleRadius;//默认上面圆形半径private float topCircleX;//默认上面圆形xprivate float topCircleY;//默认上面圆形yprivate int refreshMaxHeight;//刷新小球可滑动的最大距离boolean bezierLock = false;private Context mContext;public QQRefreshView(Context context) {this(context, null);}public QQRefreshView(Context context, AttributeSet attrs) {super(context, attrs);mContext = context;init();}

变量比较多,略过一些状态常量,咱们挑重点的说:

refreshTargetTop:要刷新的高度,即刷新头控件的高度,就是我们的topMagin,因为默认在View的顶部,用户看不到,所以我们设置为默认-60dp

lastTop:手指从开始触摸屏幕到滑动结束的高度

takeBacksState:刷新结束后动画应该收回到的高度状态

bezierLock:贝塞尔小球锁,因为用户移动会产生多次状态变化,为了不多执行,添加锁

最主要的变量就是这四个变量了,也是本文实现下拉刷新的关键字段.后面会着重介绍它们的作用.

介绍完我们的初始变量,接下来就是初始化我们的刷新header布局了:

    private void initRefreshView() {//刷新视图顶端的的viewrefreshView = LayoutInflater.from(mContext).inflate(R.layout.layout_qqrefresh_header, null);ll_ok = (LinearLayout) refreshView.findViewById(R.id.ll_ok);ll_refresh = (LinearLayout) refreshView.findViewById(R.id.ll_refresh);bezierView = (YPXBezierView) refreshView.findViewById(R.id.bview);iv_ok = (ImageView) refreshView.findViewById(R.id.iv_ok);tv_ok = (TextView) refreshView.findViewById(R.id.tv_ok);pb_refresh = (ProgressBar) refreshView.findViewById(R.id.pb_refresh);LayoutParams lp = new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, -refreshTargetTop);lp.topMargin = refreshTargetTop;addView(refreshView, lp);resetData();bezierView.setOnAnimResetListener(new YPXBezierView.OnAnimResetListener() {@Overridepublic void onReset() {animRefreshView(200, TAKEBACK_REFRESH);if (refreshListener != null && refreshState == REFRESH_BY_RELEASE) {refreshing();refreshListener.onRefresh();setRefreshState(REFRESHING);}}});}private  void resetData(){lastTop = refreshTargetTop;refreshMaxHeight=-refreshTargetTop;topCircleX=ScreenUtils.getScreenWidth(mContext)/2;topCircleY=-refreshTargetTop/2;topCircleRadius=-refreshTargetTop/4;bezierView.setTopCircleX(topCircleX);bezierView.setTopCircleY(topCircleY);bezierView.setTopCircleRadius(topCircleRadius);bezierView.setMaxHeight(refreshMaxHeight);bezierView.resetBottomCricle();}

我们把我们的刷新头高度设为我们的-refreshTargetTop,因为我们的刷新头默认在View的顶上,看不见,所以我们设置的是负值,高度就是-refreshTargetTop(负负得正),然后就是设置我们的刷新布局的topMargin,其实这里的topMargin就是我们的refreshTargetTop值,resetData中我们初始化了我们的贝塞尔View的一些参数,这些都可以交给用户去设置.最重要的是贝塞尔View的动画收回的回调监听了,主要是切换到刷新状态,具体的代码后面会给大家详细介绍.到此,我们基本可以实现了一个带贝塞尔View的刷新头布局.

接下来就是对应的刷新状态了:

    /*** 下拉刷新状态*/public void pullDownToRefresh() {setRefreshState(REFRESH_BY_PULLDOWN);ll_refresh.setVisibility(View.VISIBLE);ll_ok.setVisibility(View.GONE);pb_refresh.setVisibility(View.GONE);bezierView.setVisibility(View.VISIBLE);}/*** 正在刷新状态*/public void refreshing() {setRefreshState(REFRESHING);ll_refresh.setVisibility(View.VISIBLE);ll_ok.setVisibility(View.GONE);bezierView.setVisibility(View.GONE);pb_refresh.setVisibility(View.VISIBLE);}/*** 刷新成功状态*/public void refreshOK() {setRefreshState(REFRESHING_SUCCESS);ll_refresh.setVisibility(View.GONE);ll_ok.setVisibility(View.VISIBLE);tv_ok.setText("刷新成功");iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_ok));}/*** 刷新失败状态*/public void refreshFailed() {setRefreshState(REFRESHING_FAILD);ll_refresh.setVisibility(View.GONE);ll_ok.setVisibility(View.VISIBLE);tv_ok.setText("刷新失败");iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_failure));}

看过我之前博客的读者也许能发现以前的五种刷新状态布局现在就只有四种了,是状态减少了吗?其实并不是的,从上面的gif图片其实可以分析出,我们的刷新控件其实并没有松开刷新的状态,从贝塞尔小球被拉伸到触发刷新,中间过程的松开刷新状态其实和下拉刷新状态的显示是一样的,那么会有人问,如果显示的一样,我们就可以取消掉松开刷新的状态了吗?去除掉REFRESH_BY_RELEASE的变量.答案是不可以的,虽然显示的一样,但是代表的意义大不相同,在后面的触发事件中我们就会知道下拉刷新和松开刷新其实是两种完全不相干的状态了.

二、刷新原理分析

其实在上上篇博客(Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件)中博主就已经介绍了关于通用下拉刷新的原理了,下面就贴一下上次分析的截图:

其实原理上相差不是很多,一开始我们依旧是通过改变topMargin来实现滑动,但是,因为我们的贝塞尔小球到了一定的位置后就需要触发曲线下拉,所以我们的topMargin并不能完全决定我们的控件的全部滑动,这时候,我们就需要定好几个滑动分界线:

第一状态:贝塞尔小球被拉出并完全显示:

 

对应的原理图如下:

可以看到,在第一状态中,我们的lastTop从refreshTargetTop(-60dp)到0点,对应的屏幕上的显示就是贝塞尔小球从隐藏到全部显示的过程,这个过程中,我们只需要改变我们的刷新头(refreshView)d的topMargin就可以,此时的刷新头高度不变,为-refreshTargetTop(负负得正).对应的代码如下:

 LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();lastTop += moveY * 0.5;if (lastTop < 0) {lp.topMargin = lastTop;lp.height = -refreshTargetTop;setRefreshState(REFRESH_BY_PULLDOWN);pullDownToRefresh();}

其中lastTop就是我们手指滑动的距离,在这里,我们给它设置了一个0.5的滑动阻力值,给人的感觉就会显得平滑一点.

第二状态:贝塞尔小球完全显示到拉伸最大距离

  

对应的原理图:

忽略左图,直接看右图,可以看到当我们的贝塞尔小球到了刷新临界点后,就开始被向下拉伸,当被拉伸到我们设置的最大拉伸距离后,开始收回触发刷新.在拉伸的过程中,我们通过改变刷新头(refreshView)的高度来动态更新我们的贝塞尔小球形状.其中我们需要得到刷新距离和最大拉伸距离的比例,即offset,这个offset就是小球的拉伸变化率了,从上一篇文章可以知道,当我们的offset越小的时候,代表我们的贝塞尔小球被拉伸的越远,几何意义上就是拉伸距离越接近于最大距离.了解完这些,我们再来看代码:

     lp.topMargin = 0;lp.height = lastTop - refreshTargetTop;float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0if (offset >= 0.2) {bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);bezierView.setOffset(offset);bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));}    lp.topMargin = 0;lp.height = lastTop - refreshTargetTop;float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0if (offset >= 0.2) {bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);bezierView.setOffset(offset);bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));}

其中判断是否到达回收状态的时候我们用offset是否小于0.2来判断,如果大于等于0.2,就把我们的贝塞尔小球拉伸,如果读者看过博主第二篇博客的话,可以发现我们这里的代码其实就是贝塞尔小球触摸事件的代码,如果没有看过,在这里我也贴上贝塞尔小球的触摸事件代码:

    @Overridepublic boolean dispatchTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:lastY = event.getRawY();break;case MotionEvent.ACTION_MOVE:delayY=event.getRawY() - lastY;//滑动高度的偏移量if(delayY<0){return true;}offset=1-delayY/maxHeight;//滑动的偏移量offset 范围 offset∈(1,0)//如果偏移量大于等于0.2的时候我们就让它开始重绘,// 这样可以给下面的圆留下一点可见半径,要不然offset为0的时候下面的圆就成了点if(offset>=0.2){bottomCircleRadius = defaultRadius * offset;bottomCircleX = topCircleX;bottomCircleY = topCircleY + delayY;topCircleRadius = (float) (defaultRadius * (Math.pow(offset, 1 / 3.0)));postInvalidate();}break;case MotionEvent.ACTION_UP:animToReset(false);break;case MotionEvent.ACTION_CANCEL:animToReset(false);break;}return true;}

怎么样,对比一下是不是发现一模一样.

接下来我们看一下最后的状态

第三状态:触发刷新回收状态

效果图在这里就不贴上了,就是收回我们的贝塞尔小球到刷新状态的显示.根据状态二我们知道,当我们的offset小于0.2的时候.我们就要收回我们的贝塞尔小球,收回的时候是触发了一段属性动画,在上一篇博客中已经介绍了我们的收回动画,接下来直接上状态三的代码:

         if(offset<0){//lastTop>refreshMaxHeightreturn;}if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {bezierView.animToReset(bezierLock);refreshState = REFRESH_BY_RELEASE;//松开刷新状态bezierLock = true;}      if(offset<0){//lastTop>refreshMaxHeightreturn;}if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {bezierView.animToReset(bezierLock);refreshState = REFRESH_BY_RELEASE;//松开刷新状态bezierLock = true;}

代码很简单,其中用到了bezierLock(贝塞尔锁),这个锁前面已经大概介绍过了,这里我们细说一下,当我们的手指触发屏幕的时候,其实会存在重复点,就会触发很多次我们的动画,就会造成运行内存多余,体现的效果肯定也非常糟糕,那么我们如何限制只执行一次呢,那就是定义一个布尔类型变量,在我们的状态没有发生变化时候就不更新它的值,只有在变化的时候才会改变,这样就可以保证一种状态下只执行一次我们的代码.这个思路其实很多地方都可以用到,比如当你想监听listview滑动的时候,上滑的时候触发一个函数,如果不加锁,你会发现我们的事件会执行多次,这个时候我们就可以引用这个思路.具体的我就不去赘述了,后面的博客里会着重介绍一下这个方法.

看完了收回动画代码,细心的朋友可能会发现,咦?下拉刷新在什么时候执行呢?在文章上面第一节中其实我们就已经贴出了我们的刷新完成代码,现在我们再回过头看一下代码:

 bezierView.setOnAnimResetListener(new YPXBezierView.OnAnimResetListener() {@Overridepublic void onReset() {animRefreshView(200, TAKEBACK_REFRESH);if (refreshListener != null && refreshState == REFRESH_BY_RELEASE) {refreshing();refreshListener.onRefresh();setRefreshState(REFRESHING);}}});

是不是清晰了很多.

到此,我们下拉刷新的三种状态已经介绍完成,下面上一下触摸事件的全部代码:

    @Overridepublic boolean onTouchEvent(MotionEvent event) {int y = (int) event.getRawY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN://记录下y坐标lastY = y;break;case MotionEvent.ACTION_MOVE://y移动坐标int m = y - lastY;doMovement(m);//记录下此刻y坐标this.lastY = y;break;case MotionEvent.ACTION_UP:fling();break;}return true;}/*** 下拉move事件处理** @param moveY*/private void doMovement(float moveY) {if ((refreshState != REFRESH_BY_RELEASE && refreshState != REFRESH_BY_PULLDOWN)||anim.isRunning()) {return;}LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();lastTop += moveY * 0.5;if (lastTop < 0) {lp.topMargin = lastTop;lp.height = -refreshTargetTop;setRefreshState(REFRESH_BY_PULLDOWN);pullDownToRefresh();} else {lp.topMargin = 0;lp.height = lastTop - refreshTargetTop;float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0if (offset >= 0.2) {bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);bezierView.setOffset(offset);bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));} else {if(offset<0){//lastTop>refreshMaxHeightreturn;}if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {bezierView.animToReset(bezierLock);refreshState = REFRESH_BY_RELEASE;//松开刷新状态bezierLock = true;}}bezierView.postInvalidate();}refreshView.setLayoutParams(lp);refreshView.invalidate();invalidate();}

代码比较长,但是核心部分我们刚刚都已经分析完毕,其中有很多细节的处理,比如在我们触发下拉之前,先要判读一下当前是否还没有刷新完毕,如果没有刷新完毕,那么我们的lastTop的默认高度就不对,是我们上一次刷新拉到的高度,就会产生很多问题,所以在这里,博主多了一些细节上的判断,让我们的控件流畅度和抗压度都得到了不错的提升.

三、收回原理分析

其实收回状态应该属于第二节的第四种状态才对,这里博主为什么要单独拿出来作为一个章节呢?因为收回的原理并不简单,从效果图上我们可以看到,我们的收回其实是分为两段式收回,从贝塞尔小球被拉伸到触发刷新到收回到刷新状态的时候,其实我们触发了

 
animRefreshView(200, TAKEBACK_REFRESH);

这个方法,这个方法是用来干嘛的呢?字面意思上就是给我们的刷新头布局添加动画,收回状态为TAKEBACK_REFRESH,即收回到刷新位置:

下面上一下我们刷新收回的几个位置点的效果图:

可以看到我们的收回状态其实有三种,每一种收回的起点或终点都有所改变,那么我们需要定义三个动画来收回吗?答案并不是这样的,一个动画的监听回调就好,下面我们来上一下最核心的收回动画代码:

 private void init() {initRefreshView();// initLoadMoreView();anim = ObjectAnimator.ofFloat(refreshView, "ypx", 0.0f, 1.0f);anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {float cVal = (Float) valueAnimator.getAnimatedValue();LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();switch (takeBackState) {case TAKEBACK_REFRESH:lp.height = lp.height + (int) (cVal * (-refreshTargetTop - lp.height));lp.topMargin = 0;break;case TAKEBACK_RESET:lp.topMargin = lp.topMargin + (int) (cVal * (refreshTargetTop - lp.topMargin));lp.height = -refreshTargetTop;break;case TAKEBACK_ALL:lp.topMargin = lp.topMargin + (int) (cVal * (refreshTargetTop - lp.topMargin));lp.height = lp.height + (int) (cVal * (-refreshTargetTop - lp.height));//bezierView.reset((float) Math.pow(cVal, 2 / 5.0));break;}refreshView.setLayoutParams(lp);refreshView.invalidate();invalidate();if (lp.height == -refreshTargetTop&& lp.topMargin == refreshTargetTop) {//动画完成resetRefreshView();}}});}

代码很简单,就是对三种收回状态单独判断,如果是收回到刷新位(TAKEBACK_REFRESH),那么我们的高度就需要动态的收回到刷新位(-refreshTargetHeight)的高度,topMargin就是0点,当前的几何意义就是我们的刷新头完整显示.剩下的两种状态我就不带着大家分析了,因为原理都一样,如果你对于为什么会有收回状态三有所疑问,因为我们正常的刷新收回是先回到刷新位,再从刷新位回到初始位置,但是,我们还遗漏了一个状态,就是下拉到触发贝塞尔小球变形但是未触发到贝塞尔小球最大距离时手指离开的状态.简单的说,我们的贝塞尔小球如果没有触发到刷新状态,这时候手指离开就需要收回我们的小球,这就触发了收回状态三.

下面我们来看对于这种情况下的代码处理:

   /*** up事件处理*/private void fling() {LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();//当前状态不是下拉刷新状态且刷新头为初始状态,此时可以认为是已经触发了刷新完成后离开手指if (refreshState != REFRESH_BY_PULLDOWN|| (lp.topMargin == refreshTargetTop && lp.height == -refreshTargetTop)) {return;}bezierView.setTopCircleRadius(topCircleRadius);bezierView.resetBottomCricle();//没刷新收回状态animRefreshView(500, TAKEBACK_ALL);}

当我们的手指离开屏幕时,如果当前状态不是下拉刷新状态且刷新头为初始状态,此时可以认为是已经触发了刷新完成后离开手指,我们需要屏蔽掉这种情况的处理,因为我们已经完成了一套完整的下拉刷新.如果用户在触发刷新前松开手指,我们就把贝塞尔小球初始化,触发收回动画三.

到此我们的刷新控件基本已经完成了,接下来就是如何定制和使用我们的自定义控件了.

四、定制及使用

我们的自定义控件有了骨头,自然少不了"肉",我们来看一下ScrollView下的使用方法:

<?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="match_parent"android:orientation="vertical"><com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.QQRefreshViewandroid:id="@+id/refreshableView1"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:visibility="visible"><ScrollViewandroid:id="@+id/scrollView1"android:layout_width="match_parent"android:layout_height="match_parent"android:overScrollMode="never"><LinearLayoutandroid:id="@+id/ll_layout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical" ></LinearLayout></ScrollView></com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.QQRefreshView></RelativeLayout>

使用代码:

public class ScrollViewFragment extends Fragment{QQRefreshView refreshableView;LinearLayout layout;final int SUCCESS = 1;final int FAILED = 0;View view;@SuppressLint("HandlerLeak")Handler handler = new Handler() {public void handleMessage(android.os.Message msg) {switch (msg.what) {case SUCCESS:refreshableView.finishRefresh(true);TextView textView = new TextView(getActivity());textView.setTextColor(Color.parseColor("#666666"));textView.setTextSize(18);textView.setText("这是刷新的文本");textView.setPadding(dp(15),dp(10),dp(15),dp(10));layout.addView(textView,0);break;case FAILED:refreshableView.finishRefresh(false);break;default:break;}};};@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {view= LayoutInflater.from(getContext()).inflate(R.layout.fragment_scrollview,null);initView();initData();return view;}private void initView() {refreshableView = (QQRefreshView) view.findViewById(R.id.refreshableView1);layout = (LinearLayout) view.findViewById(R.id.ll_layout);//设置是否可以刷新,默认可以刷新refreshableView.setRefreshEnabled(true);//设置刷新头的高度,此高度会决定小球的默认半径和坐标/*   refreshableView.setRefreshViewHeight(refreshableView.dp(120));//设置刷新颜色,默认颜色值#999999refreshableView.setRefreshColor(Color.parseColor("#26B8F2"));//设置刷新图标,默认刷新图标refreshableView.setRefreshIcon(R.mipmap.ic_launcher);//设置刷新球最大拉伸距离,默认为刷新头部高度refreshableView.setRefreshMaxHeight(refreshableView.dp(150));//设置刷新球半径,默认15dprefreshableView.setTopCircleRadius(refreshableView.dp(30));//设置刷新球圆心X值,默认屏宽一半refreshableView.setTopCircleX(refreshableView.dp(50));//设置刷新球圆心Y值,默认30dprefreshableView.setTopCircleY(refreshableView.dp(30));  */}private void initData() {layout.removeAllViews();for (int i = 0; i < 50; i++) {final TextView textView = new TextView(getActivity());textView.setTextColor(Color.parseColor("#666666"));textView.setTextSize(18);textView.setPadding(dp(15),dp(10),dp(15),dp(10));textView.setText("这是第" + i + "个文本");textView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {Toast.makeText(getActivity(),textView.getText(),Toast.LENGTH_SHORT).show();}});layout.addView(textView);}refreshableView.setRefreshListener(new QQRefreshView.RefreshListener() {@Overridepublic void onRefresh() {handler.postDelayed(new Runnable() {@Overridepublic void run() {handler.sendEmptyMessage(SUCCESS);}}, 500);}});}public int dp(int dp) {return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());}
}

我们的控件其中包含的方法有很多,大致罗列了一下:

        refreshableView.setRefreshEnabled(true);//设置刷新头的高度,此高度会决定小球的默认半径和坐标refreshableView.setRefreshViewHeight(refreshableView.dp(120));//设置刷新颜色,默认颜色值#999999refreshableView.setRefreshColor(Color.parseColor("#26B8F2"));//设置刷新图标,默认刷新图标refreshableView.setRefreshIcon(R.mipmap.ic_launcher);//设置刷新球最大拉伸距离,默认为刷新头部高度refreshableView.setRefreshMaxHeight(refreshableView.dp(150));//设置刷新球半径,默认15dprefreshableView.setTopCircleRadius(refreshableView.dp(30));//设置刷新球圆心X值,默认屏宽一半refreshableView.setTopCircleX(refreshableView.dp(50));//设置刷新球圆心Y值,默认30dprefreshableView.setTopCircleY(refreshableView.dp(30));

注释写的很清楚,其中有一个方法是叫做setRefreshViewHeight,这个方法单独拿出来说一下,因为当用户设置死了我们的刷新头高度,我们的贝塞尔小球默认半径和拉动最大距离也会改变,如果想和原来的保持一致,我们就需要设置下面的几种方法.总的来说,用户可以自定义成任何样式的贝塞尔小球,包括拉动距离、以及半径等,下面上一个如上代码所产生的效果:

五、总结

总的来说,打造一个这样的下拉刷新控件思路其实并不复杂,重要的在于每一个状态的把握.当我们在自定义View的时候,我们应该先确定好我们的每一个状态变化,针对每种状态编码,就会使我们的效率大大提高.记得在实现这个效果前,我也做了不少的功课,也尝试了很多有趣的失败品,虽然效果不尽人意,但是过程中可以学到的确实很多很多.如果有人问博主你觉得你的下拉刷新是最完整的吗?答案肯定是否定的,因为我自己知道,其实还少了一些功能,比如上拉加载.虽然上拉加载的逻辑并不是很复杂,只要给滑动布局添加footView就好,但是本篇博客中并没有实现,所以接下来的博客,博主将会抽离出下拉刷新的主干框架,包含上拉加载,打造一些五颜六色,杂七杂八的刷新效果,比如淘宝、京东、Boss直聘、美团等等效果集于一身,还请读者们多多支持和关注哦~

感谢大家的支持,谢谢!喜爱本博客的读者们帮忙多多点赞哦~

作者:yangpeixing

QQ:313930500

下载地址:https://github.com/yangpeixing/YPXRefreshLayout/tree/develop

转载请注明出处~谢谢~

Android仿苹果版QQ下拉刷新实现(三)相关推荐

  1. Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发鼻涕下拉粘连效果

    前言 接着上一期 Android仿苹果版QQ下拉刷新实现(一) --打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果: 下面上一下本章需要实现的效果图: 大家看到这个效 ...

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

    前言: 因为公司人员变动原因,导致了博主四个月没有动安卓,一直在做IOS开发,如今接近年前,终于可以花一定的时间放在安卓上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下 ...

  3. android支付宝动态更新,Android仿支付宝首页下拉刷新

    题外话 学习了Behavior之后,发现效果都可以通过Behavior来实现,包括支付宝首页的下拉刷新效果,其重点效果指标在于下滑上部分的布局,同样能够进行下拉刷新,其下拉刷新的布局展开的位置在中间部 ...

  4. 打造Android微信朋友圈下拉刷新控件

    打造Android微信朋友圈下拉刷新控件> 转载于:https://www.cnblogs.com/zhujiabin/p/5707789.html

  5. vue 仿B站下拉刷新上拉加载

    vue 仿B站下拉刷新上拉加载 功能大部分都是跟B站一样的,还是有一些瑕疵和小bug的,φ(>ω<*) 先上demo连接和gitHub项目地址吧 demo展示 https://github ...

  6. html js微信朋友圈下拉刷新效果,仿朋友圈下拉刷新动画(基础动画)

    示意图: 2.0.gif demo地址:仿朋友圈下拉刷新动画 动画的起源源于好奇 因为刚开是学动画,恨不得把所有的都实现一遍,试了一下微信朋友圈的下拉刷新动画. 如果ViewController的第一 ...

  7. Android自定义控制(五)仿新浪微博的下拉刷新

    网上有很多很有名的开源框架,这里就来拉拉PullToRefresh这个框架,也就是我们平时用的下拉刷新啦,当然你问我这个有什么用啊?别人已经写好了,这里主要是学习以及练习,练习的次数多了,一切就顺其自 ...

  8. Android自定义下拉刷新动画--仿百度外卖下拉刷新

    好久没写博客了,小编之前一段时间一直在找工作,从天津来到了我们的大帝都,感觉还不错.好了废话不多说了,开始我们今天的主题吧.现如今的APP各式各样,同样也带来了各种需求,一个下拉刷新都能玩出花样了,前 ...

  9. 仿支付宝首页下拉刷新

    声明:转载请注明本文地址 DEMO相关 APK下载地址 https://fir.im/ckh1 Github源码,欢迎大家指正,以及star,谢谢 https://github.com/JmStefa ...

最新文章

  1. 怎么用计算机不会卡,电脑卡顿不流畅怎么解决?
  2. 大数据的3V和三个层面
  3. Algs4-1.4.7统计算术运算与比较次数
  4. DDIC和SAP*被锁定后如何解锁或重置密码
  5. Jquery中进行post请求时同步与异步的区别(从实例入手学习)
  6. python递归算法经典实例-Python递归算法详解
  7. 三维数据平滑处理_你该如何正确的处理思看科技三维扫描仪得到的数据?
  8. virt-v2v 使用指南
  9. 大数据架构中使用JSON-RPC好,还是RESTful API好?
  10. 移动端中如何检测设备方向的变化?
  11. android 应用自动重启,Android 应用崩溃后自动重启的方法
  12. 亚马逊服务器维护,Amazon EC2 维护帮助页面
  13. VMware NSX组件构建矩阵
  14. css垂直居中怎么设置?文字上下居中和图片垂直居中
  15. 数据分享 — 国内外常用夜间灯光数据产品介绍
  16. 2021-08-13
  17. PAYPAL支付开发简介
  18. 【笔记】研究生的早期科研之路(作者:中国人民大学 赵鑫)
  19. Git Tower 3.2 - 最好用的代码管理工具
  20. 树莓派手动固定无线网络的IP地址

热门文章

  1. vi与vim编辑器使用
  2. 服务器(IBM/DELL/HP)保修时间查询地址汇总
  3. appium开启两个服务端口链接两个模拟机,但每次都只运行一台设备。终于解决。
  4. mysql完全删除文件_MySQL完全删除教程
  5. 机器学习之路——KNN+交叉验证
  6. 史上最全 | 单目相机测距测速方法大盘点!
  7. Oracle SQL Trace、Tkprof和10046事件
  8. 购买前如何确保相机或镜头正常工作
  9. 分巧克力 c/c++
  10. 圆弧中点坐标值求解(二维平面三维空间)(3.1增加三维部分)-①