修改源码自定义SwipeRefreshLayout样式——高仿微信朋友圈下拉刷新

转自:http://blog.csdn.net/u011443509/article/details/52025019

上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://blog.csdn.net/u011443509/article/details/52017355


上一篇里最后提到,SwipeRefreshLayout的可定制性是比较差的,看源码会发现跟样式相关的几个类都是private的而且方法是写死的,只暴露出了几个颜色设置的方法。这样使得SwipeRefreshLayout的使用比较简单,主要就是设置一个监听器在onRefresh方法里完成刷新逻辑。讲道理SwipeRefreshLayout的样式是挺美观的,如果以后都用这种下拉刷新样式的话,程序员就清静了,但这也是不太可能的。如果就想用官方的SwipeRefreshLayout,不想用第三方的控件,又想定制样式,该怎么办?基本上只能改源码了。下面就从修改源码的角度出发,给出自定义样式的思路。

首先需要将SwipeRefreshLayout以及内部使用到的CircleImageView和MaterialProgressDrawable的源码都拷贝出来,放到一个包里,方便修改。从源码可以知道,SwipeRefreshLayout中跟样式相关的类主要有两个:

一. CircleImageView,继承imageview,源码就不贴了,主要是绘制背景的,进度圈就是绘制在这上面,如果要修改进度圈的位置,就应该修改CircleImageView的位置。

二. MaterialProgressDrawable,继承Drawable实现Animatable接口,内部还定义了一个Ring类,主要是绘制进度圈的,如果要修改进度圈的图片和动画,就应该从这里开刀。

下面就以社交APP的BOSS微信为例,仿照朋友圈的下拉刷新效果。

先上效果图,可以跟手机里的微信比较一下,整体感觉还是可以的。第一次录gif,录了太长,处理的时候删了一些中间的帧)


这段时间在高仿微信,图方便就把整体的效果也展示了,读者关注刷新页面即可。布局主要就是一个SwipeRefreshLayout内嵌一个RecyclerView,滑动到顶端向下拖动时,出来的进度圈是朋友圈的那个彩虹圈,位置在左边,而且随着向下拖动会不断绕中心转啊转,此外,进度圈在到达某个位置后就不会再往下了。跟默认效果不同的还有recyclerview,默认是主布局是不会跟着拖动的,而微信的有一个拖动反弹效果,背景是黑色。开始刷新后,主布局反弹到头部,进度圈在那里转啊转,刷新完毕后进度圈就消失了,整个过程就是这样。那么就一步一步来.

1. 调整进度圈位置

首先要将进度圈调整到左边,根据View的绘制原理,进度圈的位置应该是由父布局也就是SwipeRefreshLayout里的onLayout方法决定的,看看源码:

[java]  view plain  copy
  1. @Override  
  2. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  3.     final int width = getMeasuredWidth();  
  4.     final int height = getMeasuredHeight();  
  5.     if (getChildCount() == 0) {  
  6.         return;  
  7.     }  
  8.     if (mTarget == null) {  
  9.         ensureTarget();  
  10.     }  
  11.     if (mTarget == null) {  
  12.         return;  
  13.     }  
  14.     final View child = mTarget;  
  15.     final int childLeft = getPaddingLeft();  
  16.     final int childTop = getPaddingTop();  
  17.     final int childWidth = width - getPaddingLeft() - getPaddingRight();  
  18.     final int childHeight = height - getPaddingTop() - getPaddingBottom();  
  19.     child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);  
  20.     int circleWidth = mCircleView.getMeasuredWidth();  
  21.     int circleHeight = mCircleView.getMeasuredHeight();  
  22.     mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,  
  23.             (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);  
  24. }  
其中的mTarget就是主布局也就是recyclerview,而mCircleView就是转载进度圈的View,因此应该把最后一句注释掉,改为:
[java]  view plain  copy
  1. //            mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,  
  2. //                    (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);  
  3. //            修改进度圈的X坐标使之位于左边  
  4.             mCircleView.layout(childLeft, mCurrentTargetOffsetTop,  
  5.                     childLeft+circleWidth, mCurrentTargetOffsetTop + circleHeight);  

这样你就会很高兴地发现进度圈已经调到左边了。

2. 实现拖动反弹效果

接下来先修改recyclerview的拖动反弹效果,SwipeRefreshLayout默认的效果是不拖动的,如果要修改其实也很简单,无非就是记录下手指运动的距离并让recyclerview设置translation就好了,那么找到onTouchEvent方法,修改ACTION_MOVE和ACTION_UP的部分:
[java]  view plain  copy
  1. case MotionEvent.ACTION_MOVE: {  
  2.     pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);  
  3.     if (pointerIndex < 0) {  
  4.         Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");  
  5.         return false;  
  6.     }  
  7.   
  8.   
  9.     final float y = MotionEventCompat.getY(ev, pointerIndex);  
  10.       记录手指移动的距离,mInitialMotionY是初始的位置,DRAG_RATE是拖拽因子,默认为0.5。  
  11.     final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;  
  12.       赋值给mTarget的top使之产生拖动效果  
  13.     mTarget.setTranslationY(overscrollTop);  
  14.     if (mIsBeingDragged) {  
  15.         if (overscrollTop > 0) {  
  16.             moveSpinner(overscrollTop);  
  17.         } else {  
  18.             return false;  
  19.         }  
  20.     }  
  21.     break;  
  22. }  
  23. case MotionEvent.ACTION_UP: {  
  24.       手指松开时启动动画回到头部  
  25.     mTarget.animate().translationY(0).setDuration(200).start();  
  26.   
  27.     pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);  
  28.     if (pointerIndex < 0) {  
  29.         Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");  
  30.         return false;  
  31.     }  
  32.   
  33.   
  34.     final float y = MotionEventCompat.getY(ev, pointerIndex);  
  35.     final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;  
  36.     mIsBeingDragged = false;  
  37.     finishSpinner(overscrollTop);  
  38.     mActivePointerId = INVALID_POINTER;  
  39.     return false;  
  40. }  

不相关的我都略过了,修改的地方我也注释了,很清晰。这样就解决了拖动反弹的问题,得益于SwipeRefreshLayout的框架,不用考虑冲突问题,修改起来还是很简单的。

3. 修改图标和拖动时的动画

接下来就是比较麻烦的图标和动画了。修改图标其实不难,因为CircleView是继承ImageView的,完全可以通过反射取到CircleView的实例变量,然后setBitmap将你的图标传进去。但是这样的话就没有动画了,显然也是没啥意义的。读者可以大致看看MaterialProgressDrawable的源码,要实现默认的动画还是比较复杂的,我这里要改为微信的效果,就一个圈圈转啊转,还是比较简单的,下面就结合上篇文章所解析的流程看看如何修改。

首先新建一个CustomProgressDrawable类,并继承自MaterialProgressDrawable(需要将源码复制出来),还需要在SwipeRefreshLayout添加set方法,方便把自定义的类传进去。
[java]  view plain  copy
  1. public void setProgressView(MaterialProgressDrawable mProgress){  
  2.     this.mProgress = mProgress;  
  3.     mCircleView.setImageDrawable(mProgress);  
  4. }  
要在CustomProgressDrawable中绘制自定义的图标,就需要暴露一个setBitmap的方法以便绘制。上篇文章提到,手指移动时会调用moveSpinner方法,并把移动的距离传进去,该方法内首先会经过一堆数学的处理得出一个rotation,再把它传入mProgress的setProgressRotation,也就是说setProgressRotation方法是通过传入的角度来转圈圈的。朋友圈的效果就是一直让中心转,所以很容易改写:
[java]  view plain  copy
  1. private float rotation;  
  2.     private Bitmap mBitmap;  
  3.   
  4.   
  5.     public void setBitmap(Bitmap mBitmap) {  
  6.         this.mBitmap = mBitmap;  
  7.     }  
  8.   
  9.   
  10.     @Override  
  11.     public void setProgressRotation(float rotation) {  
  12. //        取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。  
  13.         this.rotation = -rotation*ROTATION_FACTOR;  
  14.         invalidateSelf();  
  15.     }  
  16.   
  17.   
  18.     @Override  
  19.     public void draw(Canvas c) {  
  20.         Rect bound = getBounds();  
  21.         c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());  
  22.         Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());  
  23.         c.drawBitmap(mBitmap,src,bound,paint);  
  24.     }  

就是不断旋转canvas再绘制bitmap。这样你就会很高兴地发现下拉的时候圈圈也转起来了。

4. 设置进度圈下拉界限和实现加载时的动画

此时正在刷新的时候圈圈是不会转的,而且圈圈默认是跟着手指拖动的,没有界限,而朋友圈的效果是圈圈在下拉到一个位置后就不再继续下拉了,先来解决下拉位置的问题。

在moveSpinner方法中,调用完setProgressRotation方法来转圈后,就会调用setTargetOffsetTopAndBottom来改变mProgress的位置,代码就不贴了。既然我们要限定下拉的位置,那就应该在这里加以限制,当下移到刷新的位置时就不再下移了,代码如下:
[java]  view plain  copy
  1. private void moveSpinner(float overscrollTop) {  
  2. …  
  3. //            setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);  
  4. //            最终刷新的位置  
  5.             int endTarget;  
  6.             if (!mUsingCustomStart) {  
  7. //                没有修改使用默认的值  
  8.                 endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop));  
  9.             } else {  
  10. //                否则使用定义的值  
  11.                 endTarget = (int) mSpinnerFinalOffset;  
  12.             }  
  13.             if(targetY>=endTarget){  
  14. //                下移的位置超过最终位置后就不再下移,第一个参数为偏移量  
  15.                 setTargetOffsetTopAndBottom(0true /* requires update */);  
  16.             }else{  
  17. //                否则继续继续下移  
  18.                 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);  
  19.             }  
  20. }  
这里先计算出一个endTarget,就是最终的位置,其他注释的比较详细不说了,这样就限制住了下移的位置。
接下来要让刷新的时候圈圈继续转,那就需要知道刷新时是执行哪里的动画。上篇文章也提到了,转圈的动画是在mProgress的start方法里的,来看看源码:

[java]  view plain  copy
  1. @Override  
  2. public void start() {  
  3.     mAnimation.reset();  
  4.     mRing.storeOriginals();  
  5.     // Already showing some part of the ring  
  6.     if (mRing.getEndTrim() != mRing.getStartTrim()) {  
  7.         mFinishing = true;  
  8.         mAnimation.setDuration(ANIMATION_DURATION/2);  
  9. //  将转圈圈的动画传入  
  10.         mParent.startAnimation(mAnimation);  
  11.     } else {  
  12.         mRing.setColorIndex(0);  
  13.         mRing.resetOriginals();  
  14.         mAnimation.setDuration(ANIMATION_DURATION);  
  15. //  将转圈圈的动画传入  
  16.         mParent.startAnimation(mAnimation);  
  17.     }  
  18. }  
主要其实就最后一句,将转圈圈的动画传入,mAnimation就是默认的转动动画,感兴趣可以自己去看看,我们只需要自定义转圈圈的动画并传入该方法就可以了。有了刚才的setProgressRotation方法,只需要定义一个动画并不断改变rotation的值并执行这个方法就好了,代码如下:
[java]  view plain  copy
  1. private void setupAnimation() {  
  2. //        初始化旋转动画  
  3.         mAnimation = new Animation(){  
  4.             @Override  
  5.             protected void applyTransformation(float interpolatedTime, Transformation t) {  
  6.                 setProgressRotation(-interpolatedTime);  
  7.             }  
  8.         };  
  9.         mAnimation.setDuration(5000);  
  10. //        无限重复  
  11.         mAnimation.setRepeatCount(Animation.INFINITE);  
  12.         mAnimation.setRepeatMode(Animation.RESTART);  
  13. //        均匀转速  
  14.         mAnimation.setInterpolator(new LinearInterpolator());  
  15.     }  
  16.   
  17.   
  18.     @Override  
  19.     public void start() {  
  20.         mParent.startAnimation(mAnimation);  
  21.     }  

这样就OK了!

5. 修改加载完毕的动画

现在已经基本完成了,最后还有一个结束的动画,默认是scale动画,而微信的是向上运动至消失,最后的动画是通过执行SwipeRefreshLayout的startScaleDownAnimation方法完成的,在方法内部定义了一个scale动画,我们只需要注释掉并自己定义一个动画就好了:
[java]  view plain  copy
  1. private void startScaleDownAnimation(Animation.AnimationListener listener) {  
  2. //            mScaleDownAnimation = new Animation() {  
  3. //                @Override  
  4. //                public void applyTransformation(float interpolatedTime, Transformation t) {  
  5. //                    setAnimationProgress(1 - interpolatedTime);  
  6. //                }  
  7. //            };  
  8.               
  9. //            最终的偏移量就是mCircleView距离顶部的高度  
  10.             final int deltaY = -mCircleView.getBottom();  
  11.             mScaleDownAnimation = new TranslateAnimation(0,0,0,deltaY);  
  12. //            mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);  
  13.             mScaleDownAnimation.setDuration(500);  
  14.             mCircleView.setAnimationListener(listener);  
  15.             mCircleView.clearAnimation();  
  16.             mCircleView.startAnimation(mScaleDownAnimation);  
  17.         }  
也就是一个偏移动画~
在activity中进行一些设置,传入朋友圈的图标后就能得到开头的效果了:
[java]  view plain  copy
  1. CustomProgressDrawable drawable = new CustomProgressDrawable(this,mRefreshLayout);  
  2.     Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.moments_refresh_icon);  
  3.     drawable.setBitmap(bitmap);  
  4.     mRefreshLayout.setProgressView(drawable);  
  5.     mRefreshLayout.setBackgroundColor(Color.BLACK);  
  6.     mRefreshLayout.setProgressBackgroundColorSchemeColor(Color.BLACK);  
  7.     mRefreshLayout.setOnRefreshListener(new CustomSwipeRefreshLayout.OnRefreshListener(){  
  8.         @Override  
  9.         public void onRefresh() {  
  10.             final Handler handler = new Handler(){  
  11.                 @Override  
  12.                 public void handleMessage(Message msg) {  
  13.                     super.handleMessage(msg);  
  14.                         mRefreshLayout.setRefreshing(false);  
  15.                 }  
  16.             };  
  17.             new Thread(new Runnable() {  
  18.                 @Override  
  19.                 public void run() {  
  20.                     try {  
  21. //          在子线程睡眠三秒后发送消息停止刷新。  
  22.                         Thread.sleep(3000);  
  23.                     } catch (InterruptedException e) {  
  24.                         e.printStackTrace();  
  25.                     }  
  26.                     handler.sendEmptyMessage(0);  
  27.                 }  
  28.             }).start();  
  29.         }  
  30.     });  
以上就基本通过修改SwipeRefreshLayout的源码仿照了朋友圈的下拉刷新效果了。从源码可以看出SwipeRefreshLayout确实是写得比较封闭的,不修改源码是基本没法自定义样式的,不过这样跟着源码过了一遍思路就比较清晰了。以后如果有机会再试着封装一下吧~

最后再附上CustomProgressDrawable的完整代码吧。SwipeRefreshLayout的太长就不发了,该改的地方应该都提到了。

项目地址是https://github.com/Gordon-H/ImitationOfWechat  这个项目是用MVP架构编写的高仿微信客户端,目前还在完善,想看源码的可以到包views中根据名字找对应的类。

[java]  view plain  copy
  1. public class CustomProgressDrawable extends MaterialProgressDrawable{  
  2.   
  3. //    旋转因子,调整旋转速度  
  4.     private static final int ROTATION_FACTOR = 5*360;  
  5. //    加载时的动画  
  6.     private Animation mAnimation;  
  7.     private View mParent;  
  8.     private Bitmap mBitmap;  
  9. //    旋转角度  
  10.     private float rotation;  
  11.     private Paint paint;  
  12.   
  13.   
  14.     public CustomProgressDrawable(Context context, View parent) {  
  15.         super(context, parent);  
  16.         mParent = parent;  
  17.         paint = new Paint();  
  18.         setupAnimation();  
  19.     }  
  20.   
  21.     private void setupAnimation() {  
  22. //        初始化旋转动画  
  23.         mAnimation = new Animation(){  
  24.             @Override  
  25.             protected void applyTransformation(float interpolatedTime, Transformation t) {  
  26.                 setProgressRotation(-interpolatedTime);  
  27.             }  
  28.         };  
  29.         mAnimation.setDuration(5000);  
  30. //        无限重复  
  31.         mAnimation.setRepeatCount(Animation.INFINITE);  
  32.         mAnimation.setRepeatMode(Animation.RESTART);  
  33. //        均匀转速  
  34.         mAnimation.setInterpolator(new LinearInterpolator());  
  35.     }  
  36.   
  37.   
  38.     @Override  
  39.     public void start() {  
  40.         mParent.startAnimation(mAnimation);  
  41.     }  
  42.     public void setBitmap(Bitmap mBitmap) {  
  43.         this.mBitmap = mBitmap;  
  44.     }  
  45.   
  46.     @Override  
  47.     public void setProgressRotation(float rotation) {  
  48. //        取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。  
  49.         this.rotation = -rotation*ROTATION_FACTOR;  
  50.         invalidateSelf();  
  51.     }  
  52.   
  53.     @Override  
  54.     public void draw(Canvas c) {  
  55.         Rect bound = getBounds();  
  56.         c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());  
  57.         Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());  
  58.         c.drawBitmap(mBitmap,src,bound,paint);  
  59.     }  
  60. }  

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值