Android控件RefreshableView实现下拉刷新
需求:自定义一个viewgroup,实现可以下拉刷新的功能。下拉一定距离后(下拉时显示的界面可以自定义任何复杂的界面)释放手指可以回调刷新的功能,用户处理完刷新的内容后,可以调用方法oncompleterefresh()通知刷新完毕,然后回归正常状态。效果如下:
源代码:refreshableview(https://github.com/wangjiegulu/refreshableview)
分析:
我们的目的是不管什么控件,只要在xml中外面包一层标签,那这个标签下面的所有子标签所在的控件都被支持可以下拉刷新了。所以,我们可以使用viewgroup来实现,这里我选择的是继承linearlayout,当然其他的(framelayout等)也一样了。
因为根据手指下滑,需要有一个刷新的view被显示出来,所以这里需要添加一个子view(称为refreshheaderview),并放置在最顶部,使用linearlayout的好处是可以设置为vertical,这样可以直接“this.addview(refreshheaderview, 0);”搞定了。然后就要根据手指滑动的距离,动态地去改变refreshheaderview的高度。同时检测是否到达了可以刷新的高度了,如果达到了,更新当前的刷新状态。手指放开时,根据之前移动的刷新状态,执行刷新或者回归正常状态。
refreshableview代码如下:
/** * 下拉刷新控件 * author: wangjie * email: tiantian.china.2@gmail.com * date: 12/13/14. */ public class refreshableview extends linearlayout { private static final string tag = refreshableview.class.getsimplename(); public refreshableview(context context) { super(context); init(context); } public refreshableview(context context, attributeset attrs) { super(context, attrs); typedarray a = context.obtainstyledattributes(attrs, r.styleable.refreshableview); for (int i = 0, len = a.length(); i < len; i++) { int attrindex = a.getindex(i); switch (attrindex) { case r.styleable.refreshableview_interceptallmoveevents: interceptallmoveevents = a.getboolean(i, false); break; } } a.recycle(); init(context); } public refreshableview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); init(context); } /** * 刷新状态 */ public static final int state_refresh_normal = 0x21; public static final int state_refresh_not_arrived = 0x22; public static final int state_refresh_arrived = 0x23; public static final int state_refreshing = 0x24; private int refreshstate; // 刷新状态监听 private refreshablehelper refreshablehelper; public void setrefreshablehelper(refreshablehelper refreshablehelper) { this.refreshablehelper = refreshablehelper; } private context context; /** * 刷新的view */ private view refreshheaderview; /** * 刷新的view的真实高度 */ private int originrefreshheight; /** * 有效下拉刷新需要达到的高度 */ private int refresharrivedstateheight; /** * 刷新时显示的高度 */ private int refreshingheight; /** * 正常未刷新高度 */ private int refreshnormalheight; /** * 默认不允许拦截(即,往子view传递事件),该属性只有在interceptallmoveevents为false的时候才有效 */ private boolean disallowintercept = true; /** * xml中可设置它的值为false,表示不把移动的事件传递给子控件 */ private boolean interceptallmoveevents; private void init(context context) { this.context = context; this.setorientation(vertical); // log.d(tag, "[init]originrefreshheight: " + refreshheaderview.getmeasuredheight()); } @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); if (null != refreshablehelper) { refreshheaderview = refreshablehelper.oninitrefreshheaderview(); } // refreshheaderview = layoutinflater.from(context).inflate(r.layout.refresh_head, null); if (null == refreshheaderview) { log.e(tag, "refreshheaderview is null!"); return; } this.removeview(refreshheaderview); this.addview(refreshheaderview, 0); // 计算refreshheadview尺寸 int width = view.measurespec.makemeasurespec(0, view.measurespec.unspecified); int expandspec = view.measurespec.makemeasurespec(integer.max_value >> 2, view.measurespec.at_most); refreshheaderview.measure(width, expandspec); log.d(tag, "[onsizechanged]w: " + w + ", h: " + h); log.d(tag, "[onsizechanged]oldw: " + oldw + ", oldh: " + oldh); log.d(tag, "[onsizechanged]child counts: " + this.getchildcount()); originrefreshheight = refreshheaderview.getmeasuredheight(); boolean isusedefault = true; if (null != refreshablehelper) { isusedefault = refreshablehelper.oninitrefreshheight(originrefreshheight); } // 初始化各个高度 if (isusedefault) { refresharrivedstateheight = originrefreshheight; refreshingheight = originrefreshheight; refreshnormalheight = 0; } log.d(tag, "[onsizechanged]refreshheaderview origin height: " + originrefreshheight); changeviewheight(refreshheaderview, refreshnormalheight); // 初始化为正常状态 setrefreshstate(state_refresh_normal); } @override public boolean dispatchtouchevent(motionevent ev) { log.d(tag, "[dispatchtouchevent]ev action: " + ev.getaction()); return super.dispatchtouchevent(ev); } @override public boolean onintercepttouchevent(motionevent ev) { log.d(tag, "[onintercepttouchevent]ev action: " + ev.getaction()); if (!interceptallmoveevents) { return !disallowintercept; } // 如果设置了拦截所有move事件,即interceptallmoveevents为true if (motionevent.action_move == ev.getaction()) { return true; } return false; } @override public void requestdisallowintercepttouchevent(boolean disallowintercept) { if (this.disallowintercept == disallowintercept) { return; } this.disallowintercept = disallowintercept; super.requestdisallowintercepttouchevent(disallowintercept); } private float downy = float.max_value; @override public boolean ontouchevent(motionevent event) { // super.ontouchevent(event); log.d(tag, "[ontouchevent]ev action: " + event.getaction()); switch (event.getaction()) { case motionevent.action_down: downy = event.gety(); log.d(tag, "down --> downy: " + downy); requestdisallowintercepttouchevent(true); // 保证事件可往下传递 break; case motionevent.action_move: float cury = event.gety(); float deltay = cury - downy; // 是否是有效的往下拖动事件(则需要显示加载header) boolean isdropdownvalidate = float.max_value != downy; /** * 修改拦截设置 * 如果是有效往下拖动事件,则事件需要在本viewgroup中处理,所以需要拦截不往子控件传递,即不允许拦截设为false * 如果是有效往下拖动事件,则事件传递给子控件处理,所以不需要拦截,并往子控件传递,即不允许拦截设为true */ requestdisallowintercepttouchevent(!isdropdownvalidate); downy = cury; log.d(tag, "move --> deltay(cury - downy): " + deltay); int curheight = refreshheaderview.getmeasuredheight(); int exceptheight = curheight + (int) (deltay / 2); // 如果当前没有处在正在刷新状态,则更新刷新状态 if (state_refreshing != refreshstate) { if (curheight >= refresharrivedstateheight) { // 达到可刷新状态 setrefreshstate(state_refresh_arrived); } else { // 未达到可刷新状态 setrefreshstate(state_refresh_not_arrived); } } if (isdropdownvalidate) { changeviewheight(refreshheaderview, math.max(refreshnormalheight, exceptheight)); } else { // 防止从子控件修改拦截后引发的downy为float.max_value的问题 changeviewheight(refreshheaderview, math.max(curheight, exceptheight)); } break; case motionevent.action_up: downy = float.max_value; log.d(tag, "up --> downy: " + downy); requestdisallowintercepttouchevent(true); // 保证事件可往下传递 // 如果是达到刷新状态,则设置正在刷新状态的高度 if (state_refresh_arrived == refreshstate) { // 达到了刷新的状态 startheightanimation(refreshheaderview, refreshheaderview.getmeasuredheight(), refreshingheight); setrefreshstate(state_refreshing); } else if (state_refreshing == refreshstate) { // 正在刷新的状态 startheightanimation(refreshheaderview, refreshheaderview.getmeasuredheight(), refreshingheight); } else { // 执行动画后回归正常状态 startheightanimation(refreshheaderview, refreshheaderview.getmeasuredheight(), refreshnormalheight, normalanimatorlistener); } break; case motionevent.action_cancel: log.d(tag, "cancel"); break; } return true; } /** * 刷新完毕后调用此方法 */ public void oncompleterefresh() { if (state_refreshing == refreshstate) { setrefreshstate(state_refresh_normal); startheightanimation(refreshheaderview, refreshheaderview.getmeasuredheight(), refreshnormalheight); } } /** * 修改当前的刷新状态 * * @param expectrefreshstate */ private void setrefreshstate(int expectrefreshstate) { if (expectrefreshstate != refreshstate) { refreshstate = expectrefreshstate; if (null != refreshablehelper) { refreshablehelper.onrefreshstatechanged(refreshheaderview, refreshstate); } } } /** * 改变某控件的高度 * * @param view * @param height */ private void changeviewheight(view view, int height) { log.d(tag, "[changeviewheight]change height: " + height); viewgroup.layoutparams lp = view.getlayoutparams(); lp.height = height; view.setlayoutparams(lp); } /** * 改变某控件的高度动画 * * @param view * @param fromheight * @param toheight */ private void startheightanimation(final view view, int fromheight, int toheight) { startheightanimation(view, fromheight, toheight, null); } private void startheightanimation(final view view, int fromheight, int toheight, animator.animatorlistener animatorlistener) { if (toheight == view.getmeasuredheight()) { return; } valueanimator heightanimator = valueanimator.ofint(fromheight, toheight); heightanimator.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator valueanimator) { integer value = (integer) valueanimator.getanimatedvalue(); if (null == value) return; changeviewheight(view, value); } }); if (null != animatorlistener) { heightanimator.addlistener(animatorlistener); } heightanimator.setinterpolator(new linearinterpolator()); heightanimator.setduration(300/*ms*/); heightanimator.start(); } animatorlisteneradapter normalanimatorlistener = new animatorlisteneradapter() { @override public void onanimationend(animator animation) { super.onanimationend(animation); setrefreshstate(state_refresh_normal); // 回归正常状态 } }; public void setrefresharrivedstateheight(int refresharrivedstateheight) { this.refresharrivedstateheight = refresharrivedstateheight; } public void setrefreshingheight(int refreshingheight) { this.refreshingheight = refreshingheight; } public void setrefreshnormalheight(int refreshnormalheight) { this.refreshnormalheight = refreshnormalheight; } public int getoriginrefreshheight() { return originrefreshheight; }
其中:
originrefreshheight,表示头部刷新view的实际高度。
refresharrivedstateheight,表示下拉多少距离可以执行刷新了
refreshingheight,表示刷新的时候显示的高度时多少
refreshnormalheight,表示正常状态下refreshheaderview显示的高度是多少
主要的核心代码应该是在ontouchevent方法中,先简单分析里面的主要代码:在action_down的时候,纪录当前手指下落的y坐标,然后再action_move的时候,去计算滑动的距离,并且判断如果滑动的距离大于refresharrivedstateheight,更新处于已经达到可刷新的状态,反之就更新处于未达到可刷新的状态。然后再action_up中,如果已经达到了可刷新的状态,则更新当前状态为正在刷新状态,并且回调状态改变的方法。
如果里面有scrollview等可以滚动的控件的时候,应该怎么处理里面的事件呢?
就以https://github.com/wangjiegulu/refreshableview 为例
xml布局如下:
<com.wangjie.refreshableview.refreshableview xmlns:rv="http://schemas.android.com/apk/res/com.wangjie.refreshableview" android:id="@+id/main_refresh_view" android:layout_width="match_parent" android:layout_height="match_parent" rv:interceptallmoveevents="false" > <com.wangjie.refreshableview.nestscrollview android:layout_width="match_parent" android:layout_height="wrap_content" android:fillviewport="true" > <textview android:id="@+id/main_tv" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="20dp" android:textsize="18sp" android:text="drop down for refresh\n\n\n\n\n\n\n\n\n\n\ndrop down for refresh\ndrop down for refresh\n\n\n\n\n\n\n\n\n\n\ndrop down for refresh\ndrop down for refresh\n\n\n\n\n\n\n\n\n\n\ndrop down for refresh\ndrop down for refresh\n\n\n\n\n\n\n\n\n\n\ndrop down for refresh" /> </com.wangjie.refreshableview.nestscrollview> </com.wangjie.refreshableview.refreshableview>
如上,最外面是一个refreshableview,然后里面是一个nestscrollview,nestscrollview里面是textview,其中textview中因为文字较多,所以使用nestscrollview来实现滚动(nestscrollview扩展自scrollview,下面会讲到)。这个时候的逻辑应该是,当nestscrollview处于顶部的时候,手指向下滑动,这时这个touch事件应该交给refreshableview处理;当手指向上滑动时,也就是scrollview向下滚动,这时,需要把touch事件给refreshableview来处理。
下面分析里面的代码:
refreshableview的interceptallmoveevents表示是否需要refreshableview阻止所有move的事件(也就是说由refreshableview自己处理所有move事件),如果自控件中没有scrollview等需要处理move事件的控件,则可以设置为true;如果有类似scrollview等控件,则需要设置为false,这样的话,refreshableview会把move事件传递给子类,由子类去处理。显然,现在例子中的情况是需要把interceptallmoveevents设置为false。设置的方法可以看上面的xml文件,使用rv:interceptallmoveevents="false"这个属性即可。
onintercepttouchevent()方法中,我们返回的是disallowintercept,这个disallowintercept是根据requestdisallowintercepttouchevent()方法的调用来动态变化的,这样可以做到切换touch事件的处理对象。
在手指落下的时候,先调用requestdisallowintercepttouchevent()方法,保证当前的事件可以正常往子控件传递,也就是现在的scrollview。然后手指会开始移动,在action_move中,先计算出当前滑动的距离。
如果是有效往下拖动事件,则事件需要在refreshableview中处理,所以需要拦截不往子控件传递,即不允许拦截设为false;如果不是有效往下拖动事件,则事件传递给子控件处理,所以不需要拦截,并往子控件传递,即不允许拦截设为true。
怎么去判断是否有效呢?根据downy,如果downy是原来的初始值float.max_value,说明,这个move事件刚开始down的时候是被子控件处理的,而不是refreshableview处理的,说明对于refreshableview来说,是一个无效的往下拖动事件;如果downy不是原来的初始值float.max_value,说明,这个move事件在down的时候就已经是refreshableview处理的了,所以是有效的。
然后,计算refreshheaderview的高度,根据滑动的差量对refreshheaderview的高度进行变换。
如果当前的状态是正在刷新,那move事件直接无效。
否则,再去判断当前的高度是不是达到了可刷新状态,或者没有达到可刷新状态,更新状态值。
在up的时候,还是先保证事件往下传递。并重置downy。然后根据当前的状态,如果达到了刷新的状态,则开始刷新,并更新当前的额状态时正在刷新状态;如果没有达到刷新状态,则执行动画返回到正常状态;如果本来就是正在刷新状态,也执行动画回归到正在刷新的高度。
然后分析下nestscrollview:
/** * author: wangjie * email: tiantian.china.2@gmail.com * date: 12/13/14. */ public class nestscrollview extends scrollview { private static final string tag = nestscrollview.class.getsimplename(); public nestscrollview(context context) { super(context); } public nestscrollview(context context, attributeset attrs) { super(context, attrs); } public nestscrollview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); } @override public boolean dispatchtouchevent(motionevent ev) { log.d(tag, "___[dispatchtouchevent]ev action: " + ev.getaction()); return super.dispatchtouchevent(ev); } @override public boolean onintercepttouchevent(motionevent ev) { super.onintercepttouchevent(ev); log.d(tag, "___[onintercepttouchevent]ev action: " + ev.getaction()); if (motionevent.action_move == ev.getaction()) { return true; } return false; } float lastdowny; @override public boolean ontouchevent(motionevent event) { super.ontouchevent(event); switch (event.getaction()) { case motionevent.action_down: lastdowny = event.gety(); parentrequestdisallowintercepttouchevent(true); // 保证事件可往下传递 log.d(tag, "___down"); return true; // break; case motionevent.action_move: log.d(tag, "___move, this.getscrolly(): " + this.getscrolly()); boolean istop = event.gety() - lastdowny > 0 && this.getscrolly() == 0; if (istop) { // 允许父控件拦截,即不允许父控件拦截设为false parentrequestdisallowintercepttouchevent(false); return false; } else { // 不允许父控件拦截,即不允许父控件拦截设为true parentrequestdisallowintercepttouchevent(true); return true; } // break; case motionevent.action_up: log.d(tag, "___up, this.getscrolly(): " + this.getscrolly()); parentrequestdisallowintercepttouchevent(true); // 保证事件可往下传递 break; case motionevent.action_cancel: log.d(tag, "___cancel"); break; } return false; } /** * 是否允许父控件拦截事件 * @param disallowintercept */ private void parentrequestdisallowintercepttouchevent(boolean disallowintercept) { viewparent vp = getparent(); if (null == vp) { return; } vp.requestdisallowintercepttouchevent(disallowintercept); } }
如上所示,也需要重写onintercepttouchevent()方法,它需要把除了move事件外的所有事件传递下去,这样最里面的textview才有onclick等事件。
在ontouchevent方法中,在action_down的时候,先纪录down的y坐标,然后保证parent(即,refreshableview)的事件可以传递过来,所以需要调用getparent().requestdisallowintercepttouchevent()方法。因为下拉刷新只能发生在scrollview滚动条在顶部的时候,所以在move中,如果当前状态在顶部,那就需要让父控件(refreshableview)拦截,然后直接返回false,让当前的事件传递到refreshableview中的ontouchevent方法中处理。如果不是在top,那就屏蔽调用父控件(refreshableview)的处理,直接自己处理。最后在up的时候再确保事件可以传递到scrollview这里来。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。