轉載自:http://blog.csdn.net/zhaokaiqiang1992/article/details/42392731
xlistview是一個非常受歡迎的下拉重新整理控件,但是已經停止維護了。之前寫過一篇xlistview的使用介紹,用起來非常簡單,這兩天放假無聊,研究了下xlistview的實作原理,學到了很多,今天分享給大家。
提前聲明,為了讓代碼更好的了解,我對代碼進行了部分删減和重構,如果大家想看原版代碼,請去github自行下載下傳。
xlistview項目主要是三部分:xlistview,xlistviewheader,xlistviewfooter,分别是xlistview主體、header、footer的實作。下面我們分開來介紹。
下面是修改之後的xlistviewheader代碼
[java] view
plaincopy
public class xlistviewheader extends linearlayout {
private static final string hint_normal = "下拉重新整理";
private static final string hint_ready = "松開重新整理資料";
private static final string hint_loading = "正在加載...";
// 正常狀态
public final static int state_normal = 0;
// 準備重新整理狀态,也就是箭頭方向發生改變之後的狀态
public final static int state_ready = 1;
// 重新整理狀态,箭頭變成了progressbar
public final static int state_refreshing = 2;
// 布局容器,也就是根布局
private linearlayout container;
// 箭頭圖檔
private imageview marrowimageview;
// 重新整理狀态顯示
private progressbar mprogressbar;
// 說明文本
private textview mhinttextview;
// 記錄目前的狀态
private int mstate;
// 用于改變箭頭的方向的動畫
private animation mrotateupanim;
private animation mrotatedownanim;
// 動畫持續時間
private final int rotate_anim_duration = 180;
public xlistviewheader(context context) {
super(context);
initview(context);
}
public xlistviewheader(context context, attributeset attrs) {
super(context, attrs);
private void initview(context context) {
mstate = state_normal;
// 初始情況下,設定下拉重新整理view高度為0
linearlayout.layoutparams lp = new linearlayout.layoutparams(
layoutparams.match_parent, 0);
container = (linearlayout) layoutinflater.from(context).inflate(
r.layout.xlistview_header, null);
addview(container, lp);
// 初始化控件
marrowimageview = (imageview) findviewbyid(r.id.xlistview_header_arrow);
mhinttextview = (textview) findviewbyid(r.id.xlistview_header_hint_textview);
mprogressbar = (progressbar) findviewbyid(r.id.xlistview_header_progressbar);
// 初始化動畫
mrotateupanim = new rotateanimation(0.0f, -180.0f,
animation.relative_to_self, 0.5f, animation.relative_to_self,
0.5f);
mrotateupanim.setduration(rotate_anim_duration);
mrotateupanim.setfillafter(true);
mrotatedownanim = new rotateanimation(-180.0f, 0.0f,
mrotatedownanim.setduration(rotate_anim_duration);
mrotatedownanim.setfillafter(true);
// 設定header的狀态
public void setstate(int state) {
if (state == mstate)
return;
// 顯示進度
if (state == state_refreshing) {
marrowimageview.clearanimation();
marrowimageview.setvisibility(view.invisible);
mprogressbar.setvisibility(view.visible);
} else {
// 顯示箭頭
marrowimageview.setvisibility(view.visible);
mprogressbar.setvisibility(view.invisible);
}
switch (state) {
case state_normal:
if (mstate == state_ready) {
marrowimageview.startanimation(mrotatedownanim);
}
if (mstate == state_refreshing) {
marrowimageview.clearanimation();
mhinttextview.settext(hint_normal);
break;
case state_ready:
if (mstate != state_ready) {
marrowimageview.startanimation(mrotateupanim);
mhinttextview.settext(hint_ready);
case state_refreshing:
mhinttextview.settext(hint_loading);
mstate = state;
public void setvisiableheight(int height) {
if (height < 0)
height = 0;
linearlayout.layoutparams lp = (linearlayout.layoutparams) container
.getlayoutparams();
lp.height = height;
container.setlayoutparams(lp);
public int getvisiableheight() {
return container.getheight();
public void show() {
container.setvisibility(view.visible);
public void hide() {
container.setvisibility(view.invisible);
}
xlistviewheader繼承自linearlayout,用來實作下拉重新整理時的界面展示,可以分為三種狀态:正常、準備重新整理、正在加載。
在linearlayout布局裡面,主要有訓示箭頭、說明文本、圓形加載條三個控件。在構造函數中,調用了initview()進行控件的初始化操作。在添加布局檔案的時候,指定高度為0,這是為了隐藏header,然後初始化動畫,是為了完成箭頭的旋轉動作。
setstate()是設定header的狀态,因為header需要根據不同的狀态,完成控件隐藏、顯示、改變文字等操作,這個方法主要是在xlistview裡面調用。除此之外,還有setvisiableheight()和getvisiableheight(),這兩個方法是為了設定和擷取header中根布局檔案的高度屬性,進而完成拉伸和收縮的效果,而show()和hide()則顯然就是完成顯示和隐藏的效果。
下面是header的布局檔案
[html] view
<?xml version="1.0" encoding="utf-8"?>
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom" >
<relativelayout
android:id="@+id/xlistview_header_content"
android:layout_width="match_parent"
android:layout_height="60dp"
tools:ignore="uselessparent" >
<textview
android:id="@+id/xlistview_header_hint_textview"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerinparent="true"
android:gravity="center"
android:text="正在加載"
android:textcolor="@android:color/black"
android:textsize="14sp" />
<imageview
android:id="@+id/xlistview_header_arrow"
android:layout_width="30dp"
android:layout_centervertical="true"
android:layout_toleftof="@id/xlistview_header_hint_textview"
android:src="@drawable/xlistview_arrow" />
<progressbar
android:id="@+id/xlistview_header_progressbar"
style="@style/progressbar_style"
android:layout_height="30dp"
android:visibility="invisible" />
</relativelayout>
</linearlayout>
說完了header,我們再看看footer。footer是為了完成加載更多功能時候的界面展示,基本思路和header是一樣的,下面是footer的代碼
public class xlistviewfooter extends linearlayout {
// 準備狀态
// 加載狀态
public final static int state_loading = 2;
private view mcontentview;
private view mprogressbar;
private textview mhintview;
public xlistviewfooter(context context) {
public xlistviewfooter(context context, attributeset attrs) {
linearlayout moreview = (linearlayout) layoutinflater.from(context)
.inflate(r.layout.xlistview_footer, null);
addview(moreview);
moreview.setlayoutparams(new linearlayout.layoutparams(
layoutparams.match_parent, layoutparams.wrap_content));
mcontentview = moreview.findviewbyid(r.id.xlistview_footer_content);
mprogressbar = moreview.findviewbyid(r.id.xlistview_footer_progressbar);
mhintview = (textview) moreview
.findviewbyid(r.id.xlistview_footer_hint_textview);
/**
* 設定目前的狀态
*
* @param state
*/
mprogressbar.setvisibility(view.invisible);
mhintview.setvisibility(view.invisible);
mhintview.setvisibility(view.visible);
mhintview.settext(r.string.xlistview_footer_hint_ready);
mhintview.settext(r.string.xlistview_footer_hint_normal);
case state_loading:
public void setbottommargin(int height) {
if (height > 0) {
linearlayout.layoutparams lp = (linearlayout.layoutparams) mcontentview
.getlayoutparams();
lp.bottommargin = height;
mcontentview.setlayoutparams(lp);
public int getbottommargin() {
linearlayout.layoutparams lp = (linearlayout.layoutparams) mcontentview
return lp.bottommargin;
lp.height = 0;
mcontentview.setlayoutparams(lp);
lp.height = layoutparams.wrap_content;
從上面的代碼裡面,我們可以看出,footer和header的思路是一樣的,隻不過,footer的拉伸和顯示效果不是通過高度來模拟的,而是通過設定bottommargin來完成的。
下面是footer的布局檔案
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
android:id="@+id/xlistview_footer_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:id="@+id/xlistview_footer_progressbar"
android:id="@+id/xlistview_footer_hint_textview"
android:layout_width="wrap_content"
android:text="@string/xlistview_footer_hint_normal"
在了解了header和footer之後,我們就要介紹最核心的xlistview的代碼實作了。
在介紹代碼實作之前,我先介紹一下xlistview的實作原理。
首先,一旦使用xlistview,footer和header就已經添加到我們的listview上面了,xlistview就是通過繼承listview,然後處理了螢幕點選事件和控制滑動實作效果的。是以,如果我們的adapter中getcount()傳回的值是20,那麼其實xlistview裡面是有20+2個item的,這個數量即使我們關閉了xlistview的重新整理和加載功能,也是不會變化的。header和footer通過addheaderview和addfooterview添加上去之後,如果想實作下拉重新整理和上拉加載功能,那麼就必須有拉伸效果,是以就像上面的那樣,header是通過設定height,footer是通過設定bottommargin來模拟拉伸效果。那麼回彈效果呢?僅僅通過設定高度或者是間隔是達不到模拟回彈效果的,是以,就需要用scroller來實作模拟回彈效果。在說明原理之後,我們開始介紹xlistview的核心實作原理。
再次提示,下面的代碼經過我重構了,隻是為了看起來更好的了解。
public class xlistview extends listview {
private final static int scrollback_header = 0;
private final static int scrollback_footer = 1;
// 滑動時長
private final static int scroll_duration = 400;
// 加載更多的距離
private final static int pull_load_more_delta = 100;
// 滑動比例
private final static float offset_radio = 2f;
// 記錄按下點的y坐标
private float lasty;
// 用來復原
private scroller scroller;
private ixlistviewlistener mlistviewlistener;
private xlistviewheader headerview;
private relativelayout headerviewcontent;
// header的高度
private int headerheight;
// 是否能夠重新整理
private boolean enablerefresh = true;
// 是否正在重新整理
private boolean isrefreashing = false;
// footer
private xlistviewfooter footerview;
// 是否可以加載更多
private boolean enableloadmore;
// 是否正在加載
private boolean isloadingmore;
// 是否footer準備狀态
private boolean isfooteradd = false;
// total list items, used to detect is at the bottom of listview.
private int totalitemcount;
// 記錄是從header還是footer傳回
private int mscrollback;
private static final string tag = "xlistview";
public xlistview(context context) {
public xlistview(context context, attributeset attrs) {
public xlistview(context context, attributeset attrs, int defstyle) {
super(context, attrs, defstyle);
scroller = new scroller(context, new decelerateinterpolator());
headerview = new xlistviewheader(context);
footerview = new xlistviewfooter(context);
headerviewcontent = (relativelayout) headerview
.findviewbyid(r.id.xlistview_header_content);
headerview.getviewtreeobserver().addongloballayoutlistener(
new ongloballayoutlistener() {
@suppresswarnings("deprecation")
@override
public void ongloballayout() {
headerheight = headerviewcontent.getheight();
getviewtreeobserver()
.removeglobalonlayoutlistener(this);
}
});
addheaderview(headerview);
@override
public void setadapter(listadapter adapter) {
// 確定footer最後添加并且隻添加一次
if (isfooteradd == false) {
isfooteradd = true;
addfooterview(footerview);
super.setadapter(adapter);
public boolean ontouchevent(motionevent ev) {
totalitemcount = getadapter().getcount();
switch (ev.getaction()) {
case motionevent.action_down:
// 記錄按下的坐标
lasty = ev.getrawy();
case motionevent.action_move:
// 計算移動距離
float deltay = ev.getrawy() - lasty;
// 是第一項并且标題已經顯示或者是在下拉
if (getfirstvisibleposition() == 0
&& (headerview.getvisiableheight() > 0 || deltay > 0)) {
updateheaderheight(deltay / offset_radio);
} else if (getlastvisibleposition() == totalitemcount - 1
&& (footerview.getbottommargin() > 0 || deltay < 0)) {
updatefooterheight(-deltay / offset_radio);
case motionevent.action_up:
if (getfirstvisibleposition() == 0) {
if (enablerefresh
&& headerview.getvisiableheight() > headerheight) {
isrefreashing = true;
headerview.setstate(xlistviewheader.state_refreshing);
if (mlistviewlistener != null) {
mlistviewlistener.onrefresh();
}
resetheaderheight();
} else if (getlastvisibleposition() == totalitemcount - 1) {
if (enableloadmore
&& footerview.getbottommargin() > pull_load_more_delta) {
startloadmore();
resetfooterheight();
return super.ontouchevent(ev);
public void computescroll() {
// 松手之後調用
if (scroller.computescrolloffset()) {
if (mscrollback == scrollback_header) {
headerview.setvisiableheight(scroller.getcurry());
} else {
footerview.setbottommargin(scroller.getcurry());
postinvalidate();
super.computescroll();
public void setpullrefreshenable(boolean enable) {
enablerefresh = enable;
if (!enablerefresh) {
headerview.hide();
headerview.show();
public void setpullloadenable(boolean enable) {
enableloadmore = enable;
if (!enableloadmore) {
footerview.hide();
footerview.setonclicklistener(null);
isloadingmore = false;
footerview.show();
footerview.setstate(xlistviewfooter.state_normal);
footerview.setonclicklistener(new onclicklistener() {
@override
public void onclick(view v) {
});
public void stoprefresh() {
if (isrefreashing == true) {
isrefreashing = false;
resetheaderheight();
public void stoploadmore() {
if (isloadingmore == true) {
private void updateheaderheight(float delta) {
headerview.setvisiableheight((int) delta
+ headerview.getvisiableheight());
// 未處于重新整理狀态,更新箭頭
if (enablerefresh && !isrefreashing) {
if (headerview.getvisiableheight() > headerheight) {
headerview.setstate(xlistviewheader.state_ready);
headerview.setstate(xlistviewheader.state_normal);
private void resetheaderheight() {
// 目前的可見高度
int height = headerview.getvisiableheight();
// 如果正在重新整理并且高度沒有完全展示
if ((isrefreashing && height <= headerheight) || (height == 0)) {
// 預設會復原到header的位置
int finalheight = 0;
// 如果是正在重新整理狀态,則復原到header的高度
if (isrefreashing && height > headerheight) {
finalheight = headerheight;
mscrollback = scrollback_header;
// 復原到指定位置
scroller.startscroll(0, height, 0, finalheight - height,
scroll_duration);
// 觸發computescroll
invalidate();
private void updatefooterheight(float delta) {
int height = footerview.getbottommargin() + (int) delta;
if (enableloadmore && !isloadingmore) {
if (height > pull_load_more_delta) {
footerview.setstate(xlistviewfooter.state_ready);
footerview.setstate(xlistviewfooter.state_normal);
footerview.setbottommargin(height);
private void resetfooterheight() {
int bottommargin = footerview.getbottommargin();
if (bottommargin > 0) {
mscrollback = scrollback_footer;
scroller.startscroll(0, bottommargin, 0, -bottommargin,
scroll_duration);
invalidate();
private void startloadmore() {
isloadingmore = true;
footerview.setstate(xlistviewfooter.state_loading);
if (mlistviewlistener != null) {
mlistviewlistener.onloadmore();
public void setxlistviewlistener(ixlistviewlistener l) {
mlistviewlistener = l;
public interface ixlistviewlistener {
public void onrefresh();
public void onloadmore();
在三個構造函數中,都調用initview進行了header和footer的初始化,并且定義了一個scroller,并傳入了一個減速的插值器,為了模仿回彈效果。在initview方法裡面,因為header可能還沒初始化完畢,是以通過globallayoutlistener來擷取了header的高度,然後addheaderview添加到了listview上面。
通過重寫setadapter方法,保證footer最後天假,并且隻添加一次。
最重要的,要屬ontouchevent了。在方法開始之前,通過getadapter().getcount()擷取到了item的總數,便于計算位置。這個操作在源代碼中是通過scrollerlistener完成的,因為scrollerlistener在這裡沒大有用,是以我直接去掉了,然後把位置改到了這裡。如果在setadapter裡面擷取的話,隻能擷取到沒有header和footer的item數量。
在action_down裡面,進行了lasty的初始化,lasty是為了判斷移動方向的,因為在action_move裡面,通過ev.getrawy()-lasty可以計算出手指的移動趨勢,如果>0,那麼就是向下滑動,反之向上。getrowy()是擷取元y坐标,意思就是和window和view坐标沒有關系的坐标,代表在螢幕上的絕對位置。然後在下面的代碼裡面,如果第一項可見并且header的可見高度>0或者是向下滑動,就說明使用者在向下拉動或者是向上拉動header,也就是訓示箭頭顯示的時候的狀态,這時候調用了updateheaderheight,來更新header的高度,實作header可以跟随手指動作上下移動。這裡有個offset_radio,這個值是一個移動比例,就是說,你手指在y方向上移動400px,如果比例是2,那麼螢幕上的控件移動就是400px/2=200px,可以通過這個值來控制使用者的滑動體驗。下面的關于footer的判斷與此類似,不再贅述。
當使用者移開手指之後,action_up方法就會被調用。在這裡面,隻對可見位置是0和item總數-1的位置進行了處理,其實正好對應header和footer。如果位置是0,并且可以重新整理,然後目前的header可見高度>原始高度的話,就說明使用者确實是要進行重新整理操作,是以通過setstate改變header的狀态,如果有監聽器的話,就調用onrefresh方法,然後調用resetheaderheight初始化header的狀态,因為footer的操作如出一轍,是以不再贅述。但是在footer中有一個pull_load_more_delta,這個值是加載更多觸發條件的臨界值,隻有footer的間隔超過這個值之後,才能夠觸發加載更多的功能,是以我們可以修改這個值來改變使用者體驗。
說到現在,大家應該明白基本的原理了,其實xlistview就是通過對使用者手勢的方向和距離的判斷,來動态的改變header和footer實作的功能,是以如果我們也有類似的需求,就可以參照這種思路進行自定義。
下面再說幾個比較重要的方法。
前面我們說道,在action_move裡面,會不斷的調用下面的updatexxxx方法,來動态的改變header和fooer的狀态,
private void updateheaderheight(float delta) {
private void updatefooterheight(float delta) {
在移開手指之後,會調用下面的resetxxx來初始化header和footer的狀态
private void resetheaderheight() {
private void resetfooterheight() {
我們可以看到,滾動操作不是通過直接的設定高度來實作的,而是通過scroller.startscroll()來實作的,通過調用此方法,computescroll()就會被調用,然後在這個裡面,根據mscrollback區分是哪一個滾動,然後再通過設定高度和間隔,就可以完成收縮的效果了。
至此,整個xlistview的實作原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。