天天看點

Android XListView實作原理講解及分析

轉載自: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 XListView實作原理講解及分析
Android XListView實作原理講解及分析

    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的核心實作原理。

    再次提示,下面的代碼經過我重構了,隻是為了看起來更好的了解。

Android XListView實作原理講解及分析
Android 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的狀态,

Android XListView實作原理講解及分析
Android XListView實作原理講解及分析

private void updateheaderheight(float delta) {  

private void updatefooterheight(float delta) {  

    在移開手指之後,會調用下面的resetxxx來初始化header和footer的狀态

Android XListView實作原理講解及分析
Android XListView實作原理講解及分析

private void resetheaderheight() {  

private void resetfooterheight() {  

    我們可以看到,滾動操作不是通過直接的設定高度來實作的,而是通過scroller.startscroll()來實作的,通過調用此方法,computescroll()就會被調用,然後在這個裡面,根據mscrollback區分是哪一個滾動,然後再通過設定高度和間隔,就可以完成收縮的效果了。

    至此,整個xlistview的實作原理就完全的搞明白了,以後如果做滾動類的自定義控件,應該也有思路了。

繼續閱讀