Anroid ListView分組和懸浮Header實(shí)現(xiàn)方法
之前在使用iOS時(shí),看到過(guò)一種分組的View,每一組都有一個(gè)Header,在上下滑動(dòng)的時(shí)候,會(huì)有一個(gè)懸浮的Header,這種體驗(yàn)覺(jué)得很不錯(cuò),請(qǐng)看下圖:

上圖中標(biāo)紅的1,2,3,4四張圖中,當(dāng)向上滑動(dòng)時(shí),仔細(xì)觀察灰色條的Header變化,當(dāng)?shù)诙M向上滑動(dòng)時(shí),會(huì)把第一組的懸浮Header擠上去。
這種效果在Android是沒(méi)有的,iOS的SDK就自帶這種效果。這篇文章就介紹如何在Android實(shí)現(xiàn)這種效果。
1、懸浮Header的實(shí)現(xiàn)
其實(shí)Android自帶的聯(lián)系人的App中就有這樣的效果,我也是把他的類(lèi)直接拿過(guò)來(lái)的,實(shí)現(xiàn)了PinnedHeaderListView這么一個(gè)類(lèi),擴(kuò)展于ListView,核心原理就是在ListView的最頂部繪制一個(gè)調(diào)用者設(shè)置的Header View,在滑動(dòng)的時(shí)候,根據(jù)一些狀態(tài)來(lái)決定是否向上或向下移動(dòng)Header View(其實(shí)就是調(diào)用其layout方法,理論上在繪制那里作一些平移也是可以的)。下面說(shuō)一下具體的實(shí)現(xiàn):
1.1、PinnedHeaderAdapter接口
這個(gè)接口需要ListView的Adapter來(lái)實(shí)現(xiàn),它定義了兩個(gè)方法,一個(gè)是讓Adapter告訴ListView當(dāng)前指定的position的數(shù)據(jù)的狀態(tài),比如指定position的數(shù)據(jù)可能是組的header;另一個(gè)方法就是設(shè)置Header View,比如設(shè)置Header View的文本,圖片等,這個(gè)方法是由調(diào)用者去實(shí)現(xiàn)的。
/**
* Adapter interface. The list adapter must implement this interface.
*/
public interface PinnedHeaderAdapter {
/**
* Pinned header state: don't show the header.
*/
public static final int PINNED_HEADER_GONE = 0;
/**
* Pinned header state: show the header at the top of the list.
*/
public static final int PINNED_HEADER_VISIBLE = 1;
/**
* Pinned header state: show the header. If the header extends beyond
* the bottom of the first shown element, push it up and clip.
*/
public static final int PINNED_HEADER_PUSHED_UP = 2;
/**
* Computes the desired state of the pinned header for the given
* position of the first visible list item. Allowed return values are
* {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
* {@link #PINNED_HEADER_PUSHED_UP}.
*/
int getPinnedHeaderState(int position);
/**
* Configures the pinned header view to match the first visible list item.
*
* @param header pinned header view.
* @param position position of the first visible list item.
* @param alpha fading of the header view, between 0 and 255.
*/
void configurePinnedHeader(View header, int position, int alpha);
}
1.2、如何繪制Header View
這是在dispatchDraw方法中繪制的:
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderViewVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
1.3、配置Header View
核心就是根據(jù)不同的狀態(tài)值來(lái)控制Header View的狀態(tài),比如PINNED_HEADER_GONE(隱藏)的情況,可能需要設(shè)置一個(gè)flag標(biāo)記,不繪制Header View,那么就達(dá)到隱藏的效果。當(dāng)PINNED_HEADER_PUSHED_UP狀態(tài)時(shí),可能需要根據(jù)不同的位移來(lái)計(jì)算Header View的移動(dòng)位移。下面是具體的實(shí)現(xiàn):
public void configureHeaderView(int position) {
if (mHeaderView == null || null == mAdapter) {
return;
}
int state = mAdapter.getPinnedHeaderState(position);
switch (state) {
case PinnedHeaderAdapter.PINNED_HEADER_GONE: {
mHeaderViewVisible = false;
break;
}
case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {
mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);
if (mHeaderView.getTop() != 0) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
}
mHeaderViewVisible = true;
break;
}
case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {
View firstView = getChildAt(0);
int bottom = firstView.getBottom();
int itemHeight = firstView.getHeight();
int headerHeight = mHeaderView.getHeight();
int y;
int alpha;
if (bottom < headerHeight) {
y = (bottom - headerHeight);
alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
} else {
y = 0;
alpha = MAX_ALPHA;
}
mAdapter.configurePinnedHeader(mHeaderView, position, alpha);
if (mHeaderView.getTop() != y) {
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
}
mHeaderViewVisible = true;
break;
}
}
}
1.4、onLayout和onMeasure
在這兩個(gè)方法中,控制Header View的位置及大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mHeaderView != null) {
measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
mHeaderViewWidth = mHeaderView.getMeasuredWidth();
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mHeaderView != null) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
configureHeaderView(getFirstVisiblePosition());
}
}
好了,到這里,懸浮Header View就完了,各位可能看不到完整的代碼,只要明白這幾個(gè)核心的方法,自己寫(xiě)出來(lái),也差不多了。
2、ListView Section實(shí)現(xiàn)
有兩種方法實(shí)現(xiàn)ListView Section效果:
方法一:
每一個(gè)ItemView中包含Header,通過(guò)數(shù)據(jù)來(lái)控制其顯示或隱藏,實(shí)現(xiàn)原理如下圖:
優(yōu)點(diǎn):
1,實(shí)現(xiàn)簡(jiǎn)單,在Adapter.getView的實(shí)現(xiàn)中,只需要根據(jù)數(shù)據(jù)來(lái)判斷是否是header,不是的話,隱藏Item view中的header部分,否則顯示。
2,Adapter.getItem(int n)始終返回的數(shù)據(jù)是在數(shù)據(jù)列表中對(duì)應(yīng)的第n個(gè)數(shù)據(jù),這樣容易理解。
3,控制header的點(diǎn)擊事件更加容易
缺點(diǎn):
1、使用更多的內(nèi)存,第一個(gè)Item view中都包含一個(gè)header view,這樣會(huì)費(fèi)更多的內(nèi)存,多數(shù)時(shí)候都可能header都是隱藏的。
方法二:
使用不同類(lèi)型的View:重寫(xiě)getItemViewType(int)和getViewTypeCount()方法。
優(yōu)點(diǎn):
1,允許多個(gè)不同類(lèi)型的item
2,理解更加簡(jiǎn)單
缺點(diǎn):
1,實(shí)現(xiàn)比較復(fù)雜
2,得到指定位置的數(shù)據(jù)變得復(fù)雜一些
到這里,我的實(shí)現(xiàn)方式是選擇第二種方案,盡管它的實(shí)現(xiàn)方式要復(fù)雜一些,但優(yōu)點(diǎn)比較明顯。
3、Adapter的實(shí)現(xiàn)
這里主要就是說(shuō)一下getPinnedHeaderState和configurePinnedHeader這兩個(gè)方法的實(shí)現(xiàn)
private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {
private ArrayList<Contact> mDatas;
private static final int TYPE_CATEGORY_ITEM = 0;
private static final int TYPE_ITEM = 1;
public ListViewAdapter(ArrayList<Contact> datas) {
mDatas = datas;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
// 異常情況處理
if (null == mDatas || position < 0|| position > getCount()) {
return true;
}
Contact item = mDatas.get(position);
if (item.isSection) {
return false;
}
return true;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public int getItemViewType(int position) {
// 異常情況處理
if (null == mDatas || position < 0|| position > getCount()) {
return TYPE_ITEM;
}
Contact item = mDatas.get(position);
if (item.isSection) {
return TYPE_CATEGORY_ITEM;
}
return TYPE_ITEM;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public Object getItem(int position) {
return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int itemViewType = getItemViewType(position);
Contact data = (Contact) getItem(position);
TextView itemView;
switch (itemViewType) {
case TYPE_ITEM:
if (null == convertView) {
itemView = new TextView(SectionListView.this);
itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
mItemHeight));
itemView.setTextSize(16);
itemView.setPadding(10, 0, 0, 0);
itemView.setGravity(Gravity.CENTER_VERTICAL);
//itemView.setBackgroundColor(Color.argb(255, 20, 20, 20));
convertView = itemView;
}
itemView = (TextView) convertView;
itemView.setText(data.toString());
break;
case TYPE_CATEGORY_ITEM:
if (null == convertView) {
convertView = getHeaderView();
}
itemView = (TextView) convertView;
itemView.setText(data.toString());
break;
}
return convertView;
}
@Override
public int getPinnedHeaderState(int position) {
if (position < 0) {
return PINNED_HEADER_GONE;
}
Contact item = (Contact) getItem(position);
Contact itemNext = (Contact) getItem(position + 1);
boolean isSection = item.isSection;
boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;
if (!isSection && isNextSection) {
return PINNED_HEADER_PUSHED_UP;
}
return PINNED_HEADER_VISIBLE;
}
@Override
public void configurePinnedHeader(View header, int position, int alpha) {
Contact item = (Contact) getItem(position);
if (null != item) {
if (header instanceof TextView) {
((TextView) header).setText(item.sectionStr);
}
}
}
}
在getPinnedHeaderState方法中,如果第一個(gè)item不是section,第二個(gè)item是section的話,就返回狀態(tài)PINNED_HEADER_PUSHED_UP,否則返回PINNED_HEADER_VISIBLE。
在configurePinnedHeader方法中,就是將item的section字符串設(shè)置到header view上面去。
【重要說(shuō)明】
Adapter中的數(shù)據(jù)里面已經(jīng)包含了section(header)的數(shù)據(jù),數(shù)據(jù)結(jié)構(gòu)中有一個(gè)方法來(lái)標(biāo)識(shí)它是否是section。那么,在點(diǎn)擊事件就要注意了,通過(guò)position可能返回的是section數(shù)據(jù)結(jié)構(gòu)。
數(shù)據(jù)結(jié)構(gòu)Contact的定義如下:
public class Contact {
int id;
String name;
String pinyin;
String sortLetter = "#";
String sectionStr;
String phoneNumber;
boolean isSection;
static CharacterParser sParser = CharacterParser.getInstance();
Contact() {
}
Contact(int id, String name) {
this.id = id;
this.name = name;
this.pinyin = sParser.getSpelling(name);
if (!TextUtils.isEmpty(pinyin)) {
String sortString = this.pinyin.substring(0, 1).toUpperCase();
if (sortString.matches("[A-Z]")) {
this.sortLetter = sortString.toUpperCase();
} else {
this.sortLetter = "#";
}
}
}
@Override
public String toString() {
if (isSection) {
return name;
} else {
//return name + " (" + sortLetter + ", " + pinyin + ")";
return name + " (" + phoneNumber + ")";
}
}
}
完整的代碼
package com.lee.sdk.test.section;
import java.util.ArrayList;
import android.graphics.Color;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.lee.sdk.test.GABaseActivity;
import com.lee.sdk.test.R;
import com.lee.sdk.widget.PinnedHeaderListView;
import com.lee.sdk.widget.PinnedHeaderListView.PinnedHeaderAdapter;
public class SectionListView extends GABaseActivity {
private int mItemHeight = 55;
private int mSecHeight = 25;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
float density = getResources().getDisplayMetrics().density;
mItemHeight = (int) (density * mItemHeight);
mSecHeight = (int) (density * mSecHeight);
PinnedHeaderListView mListView = new PinnedHeaderListView(this);
mListView.setAdapter(new ListViewAdapter(ContactLoader.getInstance().getContacts(this)));
mListView.setPinnedHeaderView(getHeaderView());
mListView.setBackgroundColor(Color.argb(255, 20, 20, 20));
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ListViewAdapter adapter = ((ListViewAdapter) parent.getAdapter());
Contact data = (Contact) adapter.getItem(position);
Toast.makeText(SectionListView.this, data.toString(), Toast.LENGTH_SHORT).show();
}
});
setContentView(mListView);
}
private View getHeaderView() {
TextView itemView = new TextView(SectionListView.this);
itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
mSecHeight));
itemView.setGravity(Gravity.CENTER_VERTICAL);
itemView.setBackgroundColor(Color.WHITE);
itemView.setTextSize(20);
itemView.setTextColor(Color.GRAY);
itemView.setBackgroundResource(R.drawable.section_listview_header_bg);
itemView.setPadding(10, 0, 0, itemView.getPaddingBottom());
return itemView;
}
private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {
private ArrayList<Contact> mDatas;
private static final int TYPE_CATEGORY_ITEM = 0;
private static final int TYPE_ITEM = 1;
public ListViewAdapter(ArrayList<Contact> datas) {
mDatas = datas;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
// 異常情況處理
if (null == mDatas || position < 0|| position > getCount()) {
return true;
}
Contact item = mDatas.get(position);
if (item.isSection) {
return false;
}
return true;
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public int getItemViewType(int position) {
// 異常情況處理
if (null == mDatas || position < 0|| position > getCount()) {
return TYPE_ITEM;
}
Contact item = mDatas.get(position);
if (item.isSection) {
return TYPE_CATEGORY_ITEM;
}
return TYPE_ITEM;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public Object getItem(int position) {
return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int itemViewType = getItemViewType(position);
Contact data = (Contact) getItem(position);
TextView itemView;
switch (itemViewType) {
case TYPE_ITEM:
if (null == convertView) {
itemView = new TextView(SectionListView.this);
itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
mItemHeight));
itemView.setTextSize(16);
itemView.setPadding(10, 0, 0, 0);
itemView.setGravity(Gravity.CENTER_VERTICAL);
//itemView.setBackgroundColor(Color.argb(255, 20, 20, 20));
convertView = itemView;
}
itemView = (TextView) convertView;
itemView.setText(data.toString());
break;
case TYPE_CATEGORY_ITEM:
if (null == convertView) {
convertView = getHeaderView();
}
itemView = (TextView) convertView;
itemView.setText(data.toString());
break;
}
return convertView;
}
@Override
public int getPinnedHeaderState(int position) {
if (position < 0) {
return PINNED_HEADER_GONE;
}
Contact item = (Contact) getItem(position);
Contact itemNext = (Contact) getItem(position + 1);
boolean isSection = item.isSection;
boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;
if (!isSection && isNextSection) {
return PINNED_HEADER_PUSHED_UP;
}
return PINNED_HEADER_VISIBLE;
}
@Override
public void configurePinnedHeader(View header, int position, int alpha) {
Contact item = (Contact) getItem(position);
if (null != item) {
if (header instanceof TextView) {
((TextView) header).setText(item.sectionStr);
}
}
}
}
}
最后來(lái)一張截圖:
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android學(xué)習(xí)之Intent中顯示意圖和隱式意圖的用法實(shí)例分析
這篇文章主要介紹了Android學(xué)習(xí)之Intent中顯示意圖和隱式意圖的用法,以實(shí)例形式分析了Intent通訊的相關(guān)技巧與注意事項(xiàng),具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android PopupWindow全屏詳細(xì)介紹及實(shí)例代碼
這篇文章主要介紹了 Android PopupWindow全屏詳細(xì)介紹及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android自定義View的使用及其原理知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是關(guān)于Android自定義View的使用及其原理知識(shí)點(diǎn)總結(jié)內(nèi)容,需要的朋友們可以學(xué)習(xí)下。2019-08-08
Android自定義控件實(shí)現(xiàn)時(shí)間軸
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)時(shí)間軸,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04
Android頁(yè)面中可編輯與不可編輯切換的實(shí)現(xiàn)
這篇文章主要給大家介紹了關(guān)于在Android頁(yè)面中可編輯與不可編輯切換的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
Android實(shí)現(xiàn)屏幕旋轉(zhuǎn)四個(gè)方向準(zhǔn)確監(jiān)聽(tīng)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)屏幕旋轉(zhuǎn)四個(gè)方向準(zhǔn)確監(jiān)聽(tīng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
Android 使用viewpager實(shí)現(xiàn)無(wú)限循環(huán)(定時(shí)+手動(dòng))
這篇文章主要介紹了Android 使用viewpager實(shí)現(xiàn)無(wú)限循環(huán)(定時(shí)+手動(dòng))的相關(guān)資料,需要的朋友可以參考下2015-11-11
Android寫(xiě)一個(gè)實(shí)時(shí)輸入框功能
這篇文章主要介紹了Android寫(xiě)一個(gè)實(shí)時(shí)輸入框功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Kotlin?LinearLayout與RelativeLayout布局使用詳解
Kotlin?的基本特性就先寫(xiě)到這里,我們這個(gè)系列的定位是基礎(chǔ),也就是能用就好,夠用就好,我們不會(huì)舉太多的例子,但是這些都是最經(jīng)常用到的特性。從這節(jié)開(kāi)始就是Kotlin和android?進(jìn)行結(jié)合,使用Kotlin進(jìn)行安卓應(yīng)用的開(kāi)發(fā)了2022-12-12

