Android自定义SwipeRefreshLayout高仿微信朋友圈下拉刷新
上一篇文章里把swiperefreshlayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:
上一篇里最后提到,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方法决定的,看看源码:
@override protected void onlayout(boolean changed, int left, int top, int right, int bottom) { final int width = getmeasuredwidth(); final int height = getmeasuredheight(); if (getchildcount() == 0) { return; } if (mtarget == null) { ensuretarget(); } if (mtarget == null) { return; } final view child = mtarget; final int childleft = getpaddingleft(); final int childtop = getpaddingtop(); final int childwidth = width - getpaddingleft() - getpaddingright(); final int childheight = height - getpaddingtop() - getpaddingbottom(); child.layout(childleft, childtop, childleft + childwidth, childtop + childheight); int circlewidth = mcircleview.getmeasuredwidth(); int circleheight = mcircleview.getmeasuredheight(); mcircleview.layout((width / 2 - circlewidth / 2), mcurrenttargetoffsettop, (width / 2 + circlewidth / 2), mcurrenttargetoffsettop + circleheight); }
其中的mtarget就是主布局也就是recyclerview,而mcircleview就是转载进度圈的view,因此应该把最后一句注释掉,改为:
// mcircleview.layout((width / 2 - circlewidth / 2), mcurrenttargetoffsettop, // (width / 2 + circlewidth / 2), mcurrenttargetoffsettop + circleheight); // 修改进度圈的x坐标使之位于左边 mcircleview.layout(childleft, mcurrenttargetoffsettop, childleft+circlewidth, mcurrenttargetoffsettop + circleheight);
这样你就会很高兴地发现进度圈已经调到左边了。
2. 实现拖动反弹效果
接下来先修改recyclerview的拖动反弹效果,swiperefreshlayout默认的效果是不拖动的,如果要修改其实也很简单,无非就是记录下手指运动的距离并让recyclerview设置translation就好了,那么找到ontouchevent方法,修改action_move和action_up的部分:
case motionevent.action_move: { pointerindex = motioneventcompat.findpointerindex(ev, mactivepointerid); if (pointerindex < 0) { log.e(log_tag, "got action_move event but have an invalid active pointer id."); return false; } final float y = motioneventcompat.gety(ev, pointerindex); // 记录手指移动的距离,minitialmotiony是初始的位置,drag_rate是拖拽因子,默认为0.5。 final float overscrolltop = (y - minitialmotiony) * drag_rate; // 赋值给mtarget的top使之产生拖动效果 mtarget.settranslationy(overscrolltop); if (misbeingdragged) { if (overscrolltop > 0) { movespinner(overscrolltop); } else { return false; } } break; } case motionevent.action_up: { // 手指松开时启动动画回到头部 mtarget.animate().translationy(0).setduration(200).start(); pointerindex = motioneventcompat.findpointerindex(ev, mactivepointerid); if (pointerindex < 0) { log.e(log_tag, "got action_up event but don't have an active pointer id."); return false; } final float y = motioneventcompat.gety(ev, pointerindex); final float overscrolltop = (y - minitialmotiony) * drag_rate; misbeingdragged = false; finishspinner(overscrolltop); mactivepointerid = invalid_pointer; return false; }
不相关的我都略过了,修改的地方我也注释了,很清晰。这样就解决了拖动反弹的问题,得益于swiperefreshlayout的框架,不用考虑冲突问题,修改起来还是很简单的。
3. 修改图标和拖动时的动画
接下来就是比较麻烦的图标和动画了。修改图标其实不难,因为circleview是继承imageview的,完全可以通过反射取到circleview的实例变量,然后setbitmap将你的图标传进去。但是这样的话就没有动画了,显然也是没啥意义的。读者可以大致看看materialprogressdrawable的源码,要实现默认的动画还是比较复杂的,我这里要改为微信的效果,就一个圈圈转啊转,还是比较简单的,下面就结合上篇文章所解析的流程看看如何修改。
首先新建一个customprogressdrawable类,并继承自materialprogressdrawable(需要将源码复制出来),还需要在swiperefreshlayout添加set方法,方便把自定义的类传进去。
public void setprogressview(materialprogressdrawable mprogress){ this.mprogress = mprogress; mcircleview.setimagedrawable(mprogress); }
要在customprogressdrawable中绘制自定义的图标,就需要暴露一个setbitmap的方法以便绘制。上篇文章提到,手指移动时会调用movespinner方法,并把移动的距离传进去,该方法内首先会经过一堆数学的处理得出一个rotation,再把它传入mprogress的setprogressrotation,也就是说setprogressrotation方法是通过传入的角度来转圈圈的。朋友圈的效果就是一直让中心转,所以很容易改写:
private float rotation; private bitmap mbitmap; public void setbitmap(bitmap mbitmap) { this.mbitmap = mbitmap; } @override public void setprogressrotation(float rotation) { // 取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。 this.rotation = -rotation*rotation_factor; invalidateself(); } @override public void draw(canvas c) { rect bound = getbounds(); c.rotate(rotation,bound.exactcenterx(),bound.exactcentery()); rect src = new rect(0,0,mbitmap.getwidth(),mbitmap.getheight()); c.drawbitmap(mbitmap,src,bound,paint); }
就是不断旋转canvas再绘制bitmap。这样你就会很高兴地发现下拉的时候圈圈也转起来了。
4. 设置进度圈下拉界限和实现加载时的动画
此时正在刷新的时候圈圈是不会转的,而且圈圈默认是跟着手指拖动的,没有界限,而朋友圈的效果是圈圈在下拉到一个位置后就不再继续下拉了,先来解决下拉位置的问题。
在movespinner方法中,调用完setprogressrotation方法来转圈后,就会调用settargetoffsettopandbottom来改变mprogress的位置,代码就不贴了。既然我们要限定下拉的位置,那就应该在这里加以限制,当下移到刷新的位置时就不再下移了,代码如下:
private void movespinner(float overscrolltop) { … // settargetoffsettopandbottom(targety - mcurrenttargetoffsettop, true /* requires update */); // 最终刷新的位置 int endtarget; if (!musingcustomstart) { // 没有修改使用默认的值 endtarget = (int) (mspinnerfinaloffset - math.abs(moriginaloffsettop)); } else { // 否则使用定义的值 endtarget = (int) mspinnerfinaloffset; } if(targety>=endtarget){ // 下移的位置超过最终位置后就不再下移,第一个参数为偏移量 settargetoffsettopandbottom(0, true /* requires update */); }else{ // 否则继续继续下移 settargetoffsettopandbottom(targety - mcurrenttargetoffsettop, true /* requires update */); } }
这里先计算出一个endtarget,就是最终的位置,其他注释的比较详细不说了,这样就限制住了下移的位置。
接下来要让刷新的时候圈圈继续转,那就需要知道刷新时是执行哪里的动画。上篇文章也提到了,转圈的动画是在mprogress的start方法里的,来看看源码:
@override public void start() { manimation.reset(); mring.storeoriginals(); // already showing some part of the ring if (mring.getendtrim() != mring.getstarttrim()) { mfinishing = true; manimation.setduration(animation_duration/2); // 将转圈圈的动画传入 mparent.startanimation(manimation); } else { mring.setcolorindex(0); mring.resetoriginals(); manimation.setduration(animation_duration); // 将转圈圈的动画传入 mparent.startanimation(manimation); } }
主要其实就最后一句,将转圈圈的动画传入,manimation就是默认的转动动画,感兴趣可以自己去看看,我们只需要自定义转圈圈的动画并传入该方法就可以了。有了刚才的setprogressrotation方法,只需要定义一个动画并不断改变rotation的值并执行这个方法就好了,代码如下:
private void setupanimation() { // 初始化旋转动画 manimation = new animation(){ @override protected void applytransformation(float interpolatedtime, transformation t) { setprogressrotation(-interpolatedtime); } }; manimation.setduration(5000); // 无限重复 manimation.setrepeatcount(animation.infinite); manimation.setrepeatmode(animation.restart); // 均匀转速 manimation.setinterpolator(new linearinterpolator()); } @override public void start() { mparent.startanimation(manimation); }
这样就ok了!
5. 修改加载完毕的动画
现在已经基本完成了,最后还有一个结束的动画,默认是scale动画,而微信的是向上运动至消失,最后的动画是通过执行swiperefreshlayout的startscaledownanimation方法完成的,在方法内部定义了一个scale动画,我们只需要注释掉并自己定义一个动画就好了:
private void startscaledownanimation(animation.animationlistener listener) { // mscaledownanimation = new animation() { // @override // public void applytransformation(float interpolatedtime, transformation t) { // setanimationprogress(1 - interpolatedtime); // } // }; // 最终的偏移量就是mcircleview距离顶部的高度 final int deltay = -mcircleview.getbottom(); mscaledownanimation = new translateanimation(0,0,0,deltay); // mscaledownanimation.setduration(scale_down_duration); mscaledownanimation.setduration(500); mcircleview.setanimationlistener(listener); mcircleview.clearanimation(); mcircleview.startanimation(mscaledownanimation); }
也就是一个偏移动画~
在activity中进行一些设置,传入朋友圈的图标后就能得到开头的效果了:
customprogressdrawable drawable = new customprogressdrawable(this,mrefreshlayout); bitmap bitmap = bitmapfactory.decoderesource(getresources(), r.drawable.moments_refresh_icon); drawable.setbitmap(bitmap); mrefreshlayout.setprogressview(drawable); mrefreshlayout.setbackgroundcolor(color.black); mrefreshlayout.setprogressbackgroundcolorschemecolor(color.black); mrefreshlayout.setonrefreshlistener(new customswiperefreshlayout.onrefreshlistener(){ @override public void onrefresh() { final handler handler = new handler(){ @override public void handlemessage(message msg) { super.handlemessage(msg); mrefreshlayout.setrefreshing(false); } }; new thread(new runnable() { @override public void run() { try { // 在子线程睡眠三秒后发送消息停止刷新。 thread.sleep(3000); } catch (interruptedexception e) { e.printstacktrace(); } handler.sendemptymessage(0); } }).start(); } });
以上就基本通过修改swiperefreshlayout的源码仿照了朋友圈的下拉刷新效果了。从源码可以看出swiperefreshlayout确实是写得比较封闭的,不修改源码是基本没法自定义样式的,不过这样跟着源码过了一遍思路就比较清晰了。以后如果有机会再试着封装一下吧~
最后再附上customprogressdrawable的完整代码吧。swiperefreshlayout的太长就不发了,该改的地方应该都提到了。
public class customprogressdrawable extends materialprogressdrawable{ // 旋转因子,调整旋转速度 private static final int rotation_factor = 5*360; // 加载时的动画 private animation manimation; private view mparent; private bitmap mbitmap; // 旋转角度 private float rotation; private paint paint; public customprogressdrawable(context context, view parent) { super(context, parent); mparent = parent; paint = new paint(); setupanimation(); } private void setupanimation() { // 初始化旋转动画 manimation = new animation(){ @override protected void applytransformation(float interpolatedtime, transformation t) { setprogressrotation(-interpolatedtime); } }; manimation.setduration(5000); // 无限重复 manimation.setrepeatcount(animation.infinite); manimation.setrepeatmode(animation.restart); // 均匀转速 manimation.setinterpolator(new linearinterpolator()); } @override public void start() { mparent.startanimation(manimation); } public void setbitmap(bitmap mbitmap) { this.mbitmap = mbitmap; } @override public void setprogressrotation(float rotation) { // 取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。 this.rotation = -rotation*rotation_factor; invalidateself(); } @override public void draw(canvas c) { rect bound = getbounds(); c.rotate(rotation,bound.exactcenterx(),bound.exactcentery()); rect src = new rect(0,0,mbitmap.getwidth(),mbitmap.getheight()); c.drawbitmap(mbitmap,src,bound,paint); } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: 详解PHP处理密码的几种方式