ListView添加動態懸浮header的另類方式

今天看了一篇文章是搞ListView動態懸浮header的, 我又結合了WheelView的item的繪製方式,最終終於有了這篇博客,在講解實現方式之前,我們先來看看要實現的效果。

要實現這種效果有很多方式,普通的佈局, 給ListView添加header都ok,而且也有很簡單,不過現在我們不打算這麼做。記得在看WheelView的時候,他的View裏竟然有一個ViewGroup,當時感覺好神奇,這玩意怎麼繪製出來呢? 哦, 原來他是在onMeasure、onLayout、onDraw中分別調用了那個ViewGroup的measure、layout、draw方法,這種方式我還是第一次見到! 雖然過去很長時間了,不過今天在看到這篇給ListView添加動態懸浮header的時候我突然想起了這種方式,於是…開幹!

如何幹? 現在我們重寫了一份ListView,雖然這種方式絕壁不是好的, 但是爲了使用上面提到的方式,忍了!

public class StickyListView extends ListView implements AbsListView.OnScrollListener {

    /**隱藏的延遲時間*/
    public static final int DURATION = 1000;

    /**header view*/
    private View mStickyHeaderView;

    /**可以設置header的adapter*/
    private StickyListAdapter mAdapter;

    /** 是否正在滑動*/
    private boolean scrolled;
    /** 用戶是不是touch屏幕*/
    private boolean touched;

    /**保存ListView原始的padding*/
    private int[] mPaddings = new int[4];

    /** 設置scroll監聽*/
    private AbsListView.OnScrollListener mScrollListener;

    public StickyListView(Context context) {
        super(context);
        init();
    }

    public StickyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public StickyListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        // 保存原始的padding
        mPaddings[0] = getPaddingLeft();
        mPaddings[1] = getPaddingTop();
        mPaddings[2] = getPaddingRight();
        mPaddings[3] = getPaddingBottom();
        // 調用super.setOnScrollListener
        // 因爲下面我們重寫了setOnScrollListener
        super.setOnScrollListener(this);
    }

  ...
}

上面的代碼,除了定義變量外, 我們還注意到了init方法,在這個方法中我們除了保存一個這個ListView的padding以外,還調用了super.setOnScrollListener(this),這裏是有學問的,爲什麼是super呢?因爲下面我們將要重寫setOnScrollListener方法並且覆蓋默認的邏輯,這樣做的目的是:別讓使用者在setOnScrollListener後覆蓋了我們設置的默認監聽。在變量中你可能還注意到了一個StickyListAdapter這是我們繼承了BaseAdapter並且做了擴展的adapter,擴展的內容僅僅是爲了獲取header,

public void setAdapter(StickyListAdapter adapter) {
    super.setAdapter(adapter);
    mAdapter = adapter;
    mStickyHeaderView = adapter.getHeaderView(null, this, 0);
    hideHeader();
}

public static abstract class StickyListAdapter extends BaseAdapter {
    public abstract View getHeaderView(View convertView, ViewGroup container, int position);
}

setAdapter中我們獲取了第一個headerView,這裏主要是爲了測量,佈局使用, 那下面我們就來看看如何爲這個headerView測量和佈局的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if(mStickyHeaderView != null) {
        mStickyHeaderView.measure(widthMeasureSpec,
                MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST));
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    if(mStickyHeaderView != null) {
        mStickyHeaderView.layout(getPaddingLeft(), getPaddingTop(),
                mStickyHeaderView.getMeasuredWidth() + getPaddingLeft(),
                mStickyHeaderView.getMeasuredHeight() + getPaddingTop());
    }
}

在ListView的onMeasureonLayout中我們直接調用了headerView的measurelayout,而且在全局代碼中沒有任何代碼是將這個headerView添加到任何view中的。那他怎麼顯示出來呢? 不要着急,我們在看繪製的時候就明白了。
再來看看測量和佈局,這裏很簡單,就是將這個headerView佈局到ListView的padding以內的位置。so,easy。 那趕緊來看看如何繪製到屏幕的吧!

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if(mStickyHeaderView.getVisibility() == View.VISIBLE) mStickyHeaderView.draw(canvas);
}

我們直接調用了這個view的draw方法,但是在這之前我們需要自己去判斷可見了,因爲繪製並不是一層層的分發下來的,而是我們自己調用的。

現在,我們的header就可以顯示出來了,並且可以顯示到正確的位置了,那如何改變header的內容呢?爲了靈活性,設置內容我們交給用戶去做,我們僅僅是調用mAdapter.getHeaderView(mStickyHeaderView, this, firstVisibleItem)就ok啦,在哪調用?當然是滑動的時候了。

private Runnable mHideTask = new Runnable() {
    public void run() {
        hideHeader();
        smoothScrollBy(mStickyHeaderView.getMeasuredHeight(), 0);
    }
};

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if(mScrollListener != null) mScrollListener.onScrollStateChanged(view, scrollState);
    removeCallbacks(mHideTask);
    if(touched && scrolled && scrollState == SCROLL_STATE_IDLE) {
        scrolled = false;
        touched = false;
        postDelayed(mHideTask, DURATION);
    }
}

public void onScroll(AbsListView view, int firstVisibleItem,
        int visibleItemCount, int totalItemCount) {
    if(mScrollListener != null) mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
    if(touched && mAdapter != null) {
        mStickyHeaderView = mAdapter.getHeaderView(mStickyHeaderView, this, firstVisibleItem);
        if(!scrolled) {
            scrolled = true;
            showHeader();
        }
    }
}

重點來看看touched,這裏爲什麼要這麼一個boolean變量呢? 主要是在mHideTask中,我們調用了smoothScrollBy這個方法會導致監聽事件的回調,如果沒有這個boolean變量,那我們的滑動就可能一直進行下去了,這個touched變量正是表示了滑動是由用戶發起的,而不是自動滑動的。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_MOVE:
        touched = true;
        break;
    }
    return super.dispatchTouchEvent(ev);
}

到現在我們的代碼就基本完成了,下面我們來寫一個試試吧。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.loader.stickyheaderlistview.MainActivity" >

    <org.loader.stickyheaderlistview.StickyListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final StickyListView listView = (StickyListView) findViewById(R.id.list);
        listView.setAdapter(new MyAdapter());
    }

    private class MyAdapter extends StickyListAdapter {

        public int getCount() {
            return STRS.length;
        }

        public Object getItem(int position) {
            return STRS[position];
        }

        public long getItemId(int position) {
            return position;
        }

        public View getView(int position, View convertView, ViewGroup parent) {
            final Holder holder;
            if(convertView == null) {
                convertView = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.item, parent, false);
                holder = new Holder(convertView);
                convertView.setTag(holder);
            }else {
                holder = (Holder) convertView.getTag();
            }

            holder.textView.setText(STRS[position]);
            return convertView;
        }

        @Override
        public View getHeaderView(View convertView, ViewGroup container,
                int position) {
            if(convertView == null) {
                convertView = LayoutInflater.from(container.getContext())
                        .inflate(R.layout.header, container, false);
            }

            TextView header = (TextView) convertView.findViewById(R.id.header);
            char headerText = STRS[position].charAt(0);
            header.setText(String.valueOf(headerText));
            return convertView;
        }

        class Holder {
            TextView textView;
            public Holder(View itemView) {
                textView = (TextView) itemView.findViewById(R.id.item);
            }
        }
    }

    public static final String[] STRS = {
            "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
            "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
            "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
            "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
            "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
            "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
            "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
            "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
            "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
            "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
            "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
            "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
            "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)",
            "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves",
            "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur",
            "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon",
            "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin",
            "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)",
            "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine",
            "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza",
            "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)",
            "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta",
            "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie",
            "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat",
            "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano",
            "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain",
            "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou",
            "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar",
            "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno",
            "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack",
            "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper",
            "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)",
            "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese",
            "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza",
            "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley",
            "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino",
            "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina",
            "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby",
            "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin",
            "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester",
            "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue",
            "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz",
            "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich",
            "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue",
            "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle",
            "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia",
            "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis",
            "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus",
            "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison",
            "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois",
            "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse",
            "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese",
            "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise",
            "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra",
            "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola",
            "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost",
            "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel",
            "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve",
            "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi",
            "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti",
            "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve",
            "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster",
            "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg",
            "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa",
            "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine",
            "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese",
            "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere",
            "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire",
            "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou",
            "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger",
            "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings",
            "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse",
            "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam",
            "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego",
            "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin",
            "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)",
            "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse",
            "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda",
            "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte",
            "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio",
            "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne",
            "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)",
            "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster",
            "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel",
            "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca",
            "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre",
            "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty",
            "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela",
            "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano",
            "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage",
            "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry",
            "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid",
            "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn",
            "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse",
            "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin",
            "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin",
            "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre",
            "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone",
            "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark",
            "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit",
            "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia",
            "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)",
            "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna",
            "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera",
            "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou",
            "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder",
            "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort",
            "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr",
            "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin",
            "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre",
            "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss",
            "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela",
            "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda",
            "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain",
            "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese",
            "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale",
            "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie",
            "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri",
            "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar",
            "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance",
            "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes",
            "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet",
            "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe",
            "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa",
            "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois",
            "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue",
            "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington",
            "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou",
            "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
            "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano"
    };
}

哈哈, 數據借用的別人的,感謝一下那位哥們的辛勤勞動。重點在getHeaderView中,我們將第一個可見項的首字母作爲header的內容,不過這裏很靈活,你可以添加任意內容。

demo下載

發佈了82 篇原創文章 · 獲贊 782 · 訪問量 85萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章