轉載請聲明出處http://blog.csdn.net/zhongkejingwang/article/details/38340701
說到下拉重新整理控件,網上版本有很多,很多軟體也都有下拉重新整理功能。有一個叫xlistview的,我看别人用過,沒看過是咋實作的,看這名字估計是繼承自listview修改的,不過效果看起來挺醜的,也沒什麼擴充性,太單調了。看了qq2014的清單下拉重新整理,發現挺好看的,我喜歡,貼一下圖看一下qq的下拉重新整理效果:
不錯吧?嗯,是的。一看就知道實作方式不一樣。咱們今天就來實作一個下拉重新整理控件。由于有時候不僅僅是listview需要下拉重新整理,expandablelistview和gridview也有這個需求,由于listview,gridview都是abslistview的子類,expandablelistview是listview的子類是以也是abslistview的子類。是以我的思路是自定義一個對所有abslistview的子類通用的下拉管理布局,叫pulltorefreshlayout,如果需要gridview,隻需要在布局檔案裡将listview換成gridview就行了,expandablelistview也一樣,不需要再繼承什麼gridview啊listview啊亂七八糟的。
看上圖,主要工作就是定義黑色大布局,紅色部分是不下拉的時候的可見部分,可以是任意的abslistview的子類(gridview,listview,expandablelistview等等)。其實我已經寫好了,先看一下效果圖:
正常拉法:
強迫症拉法:
上面是listview的,下面是gridview的
再來看一下expandablelistview的下拉重新整理效果:
可以看到,點選事件和長按事件都能正常觸發而不會誤觸發,在使用expandablelistview的時候需要注意禁止展開時自動滾動,否則會出現bug。後面會提供demo源碼下載下傳,可以根據自己的需求去修改。
下面講解pulltorefreshlayout的實作,在貼完整的源碼之前先了解整個類的大概思路:
[java] view
plaincopy
public class pulltorefreshlayout extends relativelayout implements ontouchlistener
{
// 下拉的距離
public float movedeltay = 0;
// 是否可以下拉
private boolean canpull = true;
private void hidehead()
{
// 在這裡開始異步隐藏下拉頭,在松手的時候或這重新整理完畢的時候隐藏
}
public void refreshfinish(int refreshresult)
// 完成重新整理操作,顯示重新整理結果
private void changestate(int to)
// 改變目前所處的狀态,有四個狀态:下拉重新整理、釋放重新整理、正在重新整理、重新整理完成
/*
* (非 javadoc)由父控件決定是否分發事件,防止事件沖突
*
* @see android.view.viewgroup#dispatchtouchevent(android.view.motionevent)
*/
@override
public boolean dispatchtouchevent(motionevent ev)
switch (ev.getactionmasked())
{
case motionevent.action_down:
/*手指按下的時候,無法判斷是否将要下拉,是以這時候break讓父類把down事件分發給子view
記錄按下的坐标*/
break;
case motionevent.action_move:
/*如果往上滑動且movedetay==0則說明不在下拉,break繼續将move事件分發給子view
如果往下拉,則計算下拉的距離movedeltay,根據movedeltay重新layout子控件。但是
由于down事件傳到了子view,如果不清除子view的事件,會導緻子view誤觸發長按事件和點選事件。是以在這裡清除子view的事件回調。
下拉超過一定的距離時,改變目前狀态*/
case motionevent.action_up:
//根據目前狀态執行重新整理操作或者hidehead
default:
}
// 事件分發交給父類
return super.dispatchtouchevent(ev);
* (非 javadoc)繪制陰影效果,顔色值可以修改
* @see android.view.viewgroup#dispatchdraw(android.graphics.canvas)
protected void dispatchdraw(canvas canvas)
//在這裡用一個漸變繪制分界線陰影
protected void onlayout(boolean changed, int l, int t, int r, int b)
//這個方法就是重新layout子view了,根據movedeltay來定位子view的位置
public boolean ontouch(view v, motionevent event)
//這個是ontouchlistener的方法,隻判斷abslistview的狀态來決定是否canpull,除此之外不做其他處理
}
可以看到,這裡複寫了viewgroup的dispatchtouchevent,這樣就可以掌控事件的分發,如果不了解這個方法可以看一下這篇android事件分發、view事件listener全解析。之是以要控制事件分發是因為我們不可能知道手指down在abslistview上之後将往上滑還是往下拉,是以down事件會分發給abslistview的,但是在move的時候就需要看情況了,因為我們不想在下拉的同時abslistview也在滑動,是以在下拉的時候不分發move事件,但這樣問題又來了,前面abslistview已經接收了down事件,如果這時候不分發move事件給它,它會觸發長按事件或者點選事件,是以在這裡還需要清除abslistview消息清單中的callback。
onlayout用于重新布置下拉頭和abslistview的位置的,這個不難了解。
了解了大概思路之後,看一下pulltorefreshlayout完整的源碼吧~
package com.jingchen.pulltorefresh;
import java.lang.reflect.field;
import java.util.timer;
import java.util.timertask;
import android.content.context;
import android.graphics.canvas;
import android.graphics.lineargradient;
import android.graphics.paint;
import android.graphics.paint.style;
import android.graphics.rectf;
import android.graphics.shader.tilemode;
import android.os.handler;
import android.os.message;
import android.util.attributeset;
import android.util.log;
import android.view.motionevent;
import android.view.view;
import android.view.view.ontouchlistener;
import android.view.viewgroup;
import android.view.animation.animationutils;
import android.view.animation.linearinterpolator;
import android.view.animation.rotateanimation;
import android.widget.abslistview;
import android.widget.relativelayout;
import android.widget.textview;
/**
* 整個下拉重新整理就這一個布局,用來管理兩個子控件,其中一個是下拉頭,另一個是包含内容的contentview(可以是abslistview的任何子類)
*
* @author 陳靖
*/
public static final string tag = "pulltorefreshlayout";
// 下拉重新整理
public static final int pull_to_refresh = 0;
// 釋放重新整理
public static final int release_to_refresh = 1;
// 正在重新整理
public static final int refreshing = 2;
// 重新整理完畢
public static final int done = 3;
// 目前狀态
private int state = pull_to_refresh;
// 重新整理回調接口
private onrefreshlistener mlistener;
// 重新整理成功
public static final int refresh_succeed = 0;
// 重新整理失敗
public static final int refresh_fail = 1;
// 下拉頭
private view headview;
// 内容
private view contentview;
// 按下y坐标,上一個事件點y坐标
private float downy, lasty;
// 釋放重新整理的距離
private float refreshdist = 200;
private timer timer;
private mytimertask mtask;
// 復原速度
public float move_speed = 8;
// 第一次執行布局
private boolean islayout = false;
// 在重新整理過程中滑動操作
private boolean istouchinrefreshing = false;
// 手指滑動距離與下拉頭的滑動距離比,中間會随正切函數變化
private float radio = 2;
// 下拉箭頭的轉180°動畫
private rotateanimation rotateanimation;
// 均勻旋轉動畫
private rotateanimation refreshinganimation;
// 下拉的箭頭
private view pullview;
// 正在重新整理的圖示
private view refreshingview;
// 重新整理結果圖示
private view stateimageview;
// 重新整理結果:成功或失敗
private textview statetextview;
/**
* 執行自動復原的handler
handler updatehandler = new handler()
@override
public void handlemessage(message msg)
// 回彈速度随下拉距離movedeltay增大而增大
move_speed = (float) (8 + 5 * math.tan(math.pi / 2 / getmeasuredheight() * movedeltay));
if (state == refreshing && movedeltay <= refreshdist && !istouchinrefreshing)
{
// 正在重新整理,且沒有往上推的話則懸停,顯示"正在重新整理..."
movedeltay = refreshdist;
mtask.cancel();
}
if (canpull)
movedeltay -= move_speed;
if (movedeltay <= 0)
// 已完成回彈
movedeltay = 0;
pullview.clearanimation();
// 隐藏下拉頭時有可能還在重新整理,隻有目前狀态不是正在重新整理時才改變狀态
if (state != refreshing)
changestate(pull_to_refresh);
// 重新整理布局,會自動調用onlayout
requestlayout();
};
public void setonrefreshlistener(onrefreshlistener listener)
mlistener = listener;
public pulltorefreshlayout(context context)
super(context);
initview(context);
public pulltorefreshlayout(context context, attributeset attrs)
super(context, attrs);
public pulltorefreshlayout(context context, attributeset attrs, int defstyle)
super(context, attrs, defstyle);
private void initview(context context)
timer = new timer();
mtask = new mytimertask(updatehandler);
rotateanimation = (rotateanimation) animationutils.loadanimation(context, r.anim.reverse_anim);
refreshinganimation = (rotateanimation) animationutils.loadanimation(context, r.anim.rotating);
// 添加勻速轉動動畫
linearinterpolator lir = new linearinterpolator();
rotateanimation.setinterpolator(lir);
refreshinganimation.setinterpolator(lir);
if (mtask != null)
mtask.cancel();
mtask = null;
timer.schedule(mtask, 0, 5);
* 完成重新整理操作,顯示重新整理結果
refreshingview.clearanimation();
refreshingview.setvisibility(view.gone);
switch (refreshresult)
case refresh_succeed:
// 重新整理成功
stateimageview.setvisibility(view.visible);
statetextview.settext(r.string.refresh_succeed);
stateimageview.setbackgroundresource(r.drawable.refresh_succeed);
case refresh_fail:
// 重新整理失敗
statetextview.settext(r.string.refresh_fail);
stateimageview.setbackgroundresource(r.drawable.refresh_failed);
// 重新整理結果停留1秒
new handler()
@override
public void handlemessage(message msg)
state = pull_to_refresh;
hidehead();
}.sendemptymessagedelayed(0, 1000);
state = to;
switch (state)
case pull_to_refresh:
// 下拉重新整理
stateimageview.setvisibility(view.gone);
statetextview.settext(r.string.pull_to_refresh);
pullview.clearanimation();
pullview.setvisibility(view.visible);
case release_to_refresh:
// 釋放重新整理
statetextview.settext(r.string.release_to_refresh);
pullview.startanimation(rotateanimation);
case refreshing:
// 正在重新整理
refreshingview.setvisibility(view.visible);
pullview.setvisibility(view.invisible);
refreshingview.startanimation(refreshinganimation);
statetextview.settext(r.string.refreshing);
downy = ev.gety();
lasty = downy;
if (mtask != null)
/*
* 觸碰的地方位于下拉頭布局,由于我們沒有對下拉頭做事件響應,這時候它會給咱傳回一個false導緻接下來的事件不再分發進來。
* 是以我們不能交給父類分發,直接傳回true
*/
if (ev.gety() < movedeltay)
return true;
// canpull這個值在底下ontouch中會根據listview是否滑到頂部來改變,意思是是否可下拉
// 對實際滑動距離做縮小,造成用力拉的感覺
movedeltay = movedeltay + (ev.gety() - lasty) / radio;
if (movedeltay < 0)
movedeltay = 0;
if (movedeltay > getmeasuredheight())
movedeltay = getmeasuredheight();
if (state == refreshing)
{
// 正在重新整理的時候觸摸移動
istouchinrefreshing = true;
}
lasty = ev.gety();
// 根據下拉距離改變比例
radio = (float) (2 + 2 * math.tan(math.pi / 2 / getmeasuredheight() * movedeltay));
if (movedeltay <= refreshdist && state == release_to_refresh)
// 如果下拉距離沒達到重新整理的距離且目前狀态是釋放重新整理,改變狀态為下拉重新整理
changestate(pull_to_refresh);
if (movedeltay >= refreshdist && state == pull_to_refresh)
changestate(release_to_refresh);
if (movedeltay > 8)
// 防止下拉過程中誤觸發長按事件和點選事件
clearcontentviewevents();
if (movedeltay > 0)
// 正在下拉,不讓子控件捕獲事件
if (movedeltay > refreshdist)
// 正在重新整理時往下拉釋放後下拉頭不隐藏
istouchinrefreshing = false;
if (state == release_to_refresh)
changestate(refreshing);
// 重新整理操作
if (mlistener != null)
mlistener.onrefresh();
} else
hidehead();
* 通過反射修改字段去掉長按事件和點選事件
private void clearcontentviewevents()
try
field[] fields = abslistview.class.getdeclaredfields();
for (int i = 0; i < fields.length; i++)
if (fields[i].getname().equals("mpendingcheckforlongpress"))
// mpendingcheckforlongpress是abslistview中的字段,通過反射擷取并從消息清單删除,去掉長按事件
fields[i].setaccessible(true);
contentview.gethandler().removecallbacks((runnable) fields[i].get(contentview));
} else if (fields[i].getname().equals("mtouchmode"))
// touch_mode_rest = -1, 這個可以去除點選事件
fields[i].set(contentview, -1);
// 去掉焦點
((abslistview) contentview).getselector().setstate(new int[]
{ 0 });
} catch (exception e)
log.d(tag, "error : " + e.tostring());
super.dispatchdraw(canvas);
if (movedeltay == 0)
return;
rectf rectf = new rectf(0, 0, getmeasuredwidth(), movedeltay);
paint paint = new paint();
paint.setantialias(true);
// 陰影的高度為26
lineargradient lineargradient = new lineargradient(0, movedeltay, 0, movedeltay - 26, 0x66000000, 0x00000000, tilemode.clamp);
paint.setshader(lineargradient);
paint.setstyle(style.fill);
// 在movedeltay處往上變淡
canvas.drawrect(rectf, paint);
private void initview()
pullview = headview.findviewbyid(r.id.pull_icon);
statetextview = (textview) headview.findviewbyid(r.id.state_tv);
refreshingview = headview.findviewbyid(r.id.refreshing_icon);
stateimageview = headview.findviewbyid(r.id.state_iv);
if (!islayout)
// 這裡是第一次進來的時候做一些初始化
headview = getchildat(0);
contentview = getchildat(1);
// 給abslistview設定ontouchlistener
contentview.setontouchlistener(this);
islayout = true;
initview();
refreshdist = ((viewgroup) headview).getchildat(0).getmeasuredheight();
if (canpull)
// 改變子控件的布局
headview.layout(0, (int) movedeltay - headview.getmeasuredheight(), headview.getmeasuredwidth(), (int) movedeltay);
contentview.layout(0, (int) movedeltay, contentview.getmeasuredwidth(), (int) movedeltay + contentview.getmeasuredheight());
}else super.onlayout(changed, l, t, r, b);
class mytimertask extends timertask
handler handler;
public mytimertask(handler handler)
this.handler = handler;
public void run()
handler.sendmessage(handler.obtainmessage());
// 第一個item可見且滑動到頂部
abslistview alv = null;
alv = (abslistview) v;
log.d(tag, e.getmessage());
return false;
if (alv.getcount() == 0)
// 沒有item的時候也可以下拉重新整理
canpull = true;
} else if (alv.getfirstvisibleposition() == 0 && alv.getchildat(0).gettop() >= 0)
// 滑到abslistview的頂部了
} else
canpull = false;
return false;
代碼中的注釋已經寫的很清楚了。
既然pulltorefreshlayout已經寫好了,接下來就來使用這個layout實作下拉重新整理~
首先得寫個onrefreshlistener接口來回調重新整理操作:
public interface onrefreshlistener {
void onrefresh();
就一個重新整理操作的方法,待會兒讓activity實作這個接口就可以在activity中執行重新整理操作了。
看一下mainactivity的布局:
[html] view
<com.jingchen.pulltorefresh.pulltorefreshlayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresh_view"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<include layout="@layout/refresh_head" />
<!-- 支援abslistview的所有子類 -->
<listview
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:divider="@color/gray"
android:dividerheight="1dp" >
</listview>
</com.jingchen.pulltorefresh.pulltorefreshlayout>
pulltorefreshlayout隻能包含兩個子控件:refresh_head和content_view。
看一下refresh_head的布局:
<?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/head_view"
android:layout_height="match_parent"
android:background="@color/light_blue" >
<relativelayout
android:layout_height="wrap_content"
android:layout_alignparentbottom="true"
android:paddingbottom="20dp"
android:paddingtop="20dp" >
<relativelayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerinparent="true" >
<imageview
android:id="@+id/pull_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centervertical="true"
android:layout_marginleft="60dp"
android:background="@drawable/pull_icon_big" />
android:id="@+id/refreshing_icon"
android:background="@drawable/refreshing"
android:visibility="gone" />
<textview
android:id="@+id/state_tv"
android:layout_centerinparent="true"
android:text="@string/pull_to_refresh"
android:textcolor="@color/white"
android:textsize="16sp" />
android:id="@+id/state_iv"
android:layout_marginright="8dp"
android:layout_toleftof="@id/state_tv"
</relativelayout>
</relativelayout>
</relativelayout>
可以根據需要修改refresh_head的布局然後在pulltorefreshlayout中處理,但是相關view的id要和pulltorefreshlayout中用到的保持同步!
接下來是mainactivity的代碼:
import java.util.arraylist;
import java.util.list;
import android.app.activity;
import android.os.bundle;
import android.support.v4.view.pageradapter;
import android.support.v4.view.viewpager;
import android.support.v4.view.viewpager.onpagechangelistener;
import android.view.layoutinflater;
import android.view.view.onclicklistener;
import android.widget.adapterview;
import android.widget.adapterview.onitemclicklistener;
import android.widget.adapterview.onitemlongclicklistener;
import android.widget.baseexpandablelistadapter;
import android.widget.expandablelistview;
import android.widget.expandablelistview.onchildclicklistener;
import android.widget.expandablelistview.ongroupclicklistener;
import android.widget.listview;
import android.widget.toast;
* 除了下拉重新整理,在contenview為listview的情況下我給listview增加了footerview,實作點選加載更多
public class mainactivity extends activity implements onrefreshlistener, onclicklistener
private abslistview alv;
private pulltorefreshlayout refreshlayout;
private view loading;
private rotateanimation loadinganimation;
private textview loadtextview;
private myadapter adapter;
private boolean isloading = false;
protected void oncreate(bundle savedinstancestate)
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
init();
private void init()
alv = (abslistview) findviewbyid(r.id.content_view);
refreshlayout = (pulltorefreshlayout) findviewbyid(r.id.refresh_view);
refreshlayout.setonrefreshlistener(this);
initlistview();
loadinganimation = (rotateanimation) animationutils.loadanimation(this, r.anim.rotating);
loadinganimation.setinterpolator(lir);
* listview初始化方法
private void initlistview()
list<string> items = new arraylist<string>();
for (int i = 0; i < 30; i++)
items.add("這裡是item " + i);
// 添加head
view headview = getlayoutinflater().inflate(r.layout.listview_head, null);
((listview) alv).addheaderview(headview, null, false);
// 添加footer
view footerview = getlayoutinflater().inflate(r.layout.load_more, null);
loading = footerview.findviewbyid(r.id.loading_icon);
loadtextview = (textview) footerview.findviewbyid(r.id.loadmore_tv);
((listview) alv).addfooterview(footerview, null, false);
footerview.setonclicklistener(this);
adapter = new myadapter(this, items);
alv.setadapter(adapter);
alv.setonitemlongclicklistener(new onitemlongclicklistener()
public boolean onitemlongclick(adapterview<?> parent, view view, int position, long id)
toast.maketext(mainactivity.this, "longclick on " + parent.getadapter().getitemid(position), toast.length_short).show();
});
alv.setonitemclicklistener(new onitemclicklistener()
public void onitemclick(adapterview<?> parent, view view, int position, long id)
toast.maketext(mainactivity.this, " click on " + parent.getadapter().getitemid(position), toast.length_short).show();
* gridview初始化方法
private void initgridview()
* expandablelistview初始化方法
private void initexpandablelistview()
((expandablelistview) alv).setadapter(new expandablelistadapter(this));
((expandablelistview) alv).setonchildclicklistener(new onchildclicklistener()
public boolean onchildclick(expandablelistview parent, view v, int groupposition, int childposition, long id)
toast.maketext(mainactivity.this, " click on group " + groupposition + " item " + childposition, toast.length_short).show();
((expandablelistview) alv).setonitemlongclicklistener(new onitemlongclicklistener()
((expandablelistview) alv).setongroupclicklistener(new ongroupclicklistener()
public boolean ongroupclick(expandablelistview parent, view v, int groupposition, long id)
if (parent.isgroupexpanded(groupposition))
// 如果展開則關閉
parent.collapsegroup(groupposition);
} else
// 如果關閉則打開,注意這裡是手動打開不要預設滾動否則會有bug
parent.expandgroup(groupposition);
public void onrefresh()
// 下拉重新整理操作
refreshlayout.refreshfinish(pulltorefreshlayout.refresh_succeed);
}.sendemptymessagedelayed(0, 5000);
public void onclick(view v)
switch (v.getid())
case r.id.loadmore_layout:
if (!isloading)
loading.setvisibility(view.visible);
loading.startanimation(loadinganimation);
loadtextview.settext(r.string.loading);
isloading = true;
class expandablelistadapter extends baseexpandablelistadapter
private string[] groupsstrings;// = new string[] { "這裡是group 0",
// "這裡是group 1", "這裡是group 2" };
private string[][] groupitems;
private context context;
public expandablelistadapter(context context)
this.context = context;
groupsstrings = new string[8];
for (int i = 0; i < groupsstrings.length; i++)
groupsstrings[i] = new string("這裡是group " + i);
groupitems = new string[8][8];
for (int i = 0; i < groupitems.length; i++)
for (int j = 0; j < groupitems[i].length; j++)
groupitems[i][j] = new string("這裡是group " + i + "裡的item " + j);
public int getgroupcount()
return groupsstrings.length;
public int getchildrencount(int groupposition)
return groupitems[groupposition].length;
public object getgroup(int groupposition)
return groupsstrings[groupposition];
public object getchild(int groupposition, int childposition)
return groupitems[groupposition][childposition];
public long getgroupid(int groupposition)
return groupposition;
public long getchildid(int groupposition, int childposition)
return childposition;
public boolean hasstableids()
return true;
public view getgroupview(int groupposition, boolean isexpanded, view convertview, viewgroup parent)
view view = layoutinflater.from(context).inflate(r.layout.list_item_layout, null);
textview tv = (textview) view.findviewbyid(r.id.name_tv);
tv.settext(groupsstrings[groupposition]);
return view;
public view getchildview(int groupposition, int childposition, boolean islastchild, view convertview, viewgroup parent)
tv.settext(groupitems[groupposition][childposition]);
public boolean ischildselectable(int groupposition, int childposition)
在mainactivity中判斷contentview是listview的話給listview添加了footerview實作點選加載更多的功能。這隻是一個示範pulltorefreshlayout使用的demo,可以參照一下修改。我已經在裡面寫了listview,gridview和expandablelistview的初始化方法,根據自己使用的是哪個來調用吧。那麼這是listview的下拉重新整理和加載更多。如果我要gridview也有下拉重新整理功能呢?那就把mainactivity的布局換成這樣:
<gridview
android:columnwidth="90dp"
android:gravity="center"
android:horizontalspacing="10dp"
android:numcolumns="auto_fit"
android:stretchmode="columnwidth"
android:verticalspacing="15dp" >
</gridview>
如果是expandablelistview則把布局改成這樣:
<expandablelistview
android:background="@color/white" >
</expandablelistview>
怎麼樣?很簡單吧?簡單易用,不用再去繼承修改了~
點選下載下傳源碼