Android 固定列頭列表的listview demo

公司的這個(gè)項(xiàng)目做了一年寄疏,感覺(jué)自己有了很大的提升甘苍。決定把這一年來(lái)做的比較好比較有用的一些東西抽出來(lái)記錄下來(lái)痴荐。既能整理自己的知識(shí)樹(shù),又能給其他朋友一些參考恰画。這篇講的是如何做一個(gè)可固定列頭列表滑動(dòng)的listview宾茂。

剛開(kāi)始做這個(gè)的時(shí)候,在網(wǎng)上查閱了大量資料拴还,也下載了很多其他人提供的demo跨晴,參考了他們的思路。但是總是要不就是不符合我的需求片林,要不就是有些bug端盆。最后,自己嘗試著編寫费封,經(jīng)過(guò)不斷的更改和修復(fù)焕妙,終于完成了這個(gè)功能。
首先也是比較重要的一點(diǎn)是孝偎,listview的item布局访敌,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">

<TextView
    android:id="@+id/tv_line"
    android:layout_width="80dp"
    android:layout_height="50dp"
    android:gravity="center"
    android:text="表頭"
    android:textColor="@android:color/black" />

<View
    android:layout_width="0.1dp"
    android:layout_height="50dp"
    android:background="@android:color/black" />
<!--攔截子控件的響應(yīng)事件-->
<com.example.lanyee.demofixheadlist.InterceptRelayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:focusable="false">

    <com.example.lanyee.demofixheadlist.ChartHScrollView
        android:id="@+id/scroll_item"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:overScrollMode="never"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:focusable="false"
            android:gravity="center"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_1"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="列1" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_2"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列2" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_3"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列3" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_4"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列4" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_5"
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列5" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_6"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列6" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_7"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列7" />

            <View
                android:layout_width="0.1dp"
                android:layout_height="match_parent"
                android:background="@android:color/black" />

            <TextView
                android:id="@+id/tv_8"
                android:layout_width="80dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:singleLine="true"
                android:text="列8" />
        </LinearLayout>
    </com.example.lanyee.demofixheadlist.ChartHScrollView>
</com.example.lanyee.demofixheadlist.InterceptRelayout>

</LinearLayout>

    其中,InterceptRelayout是起攔截子控件的響應(yīng)事件作用的衣盾。listview的item中的ChartHScrollView子控件是不響應(yīng)觸摸事件的寺旺,觸摸事件統(tǒng)一交給列頭的ChartHScrollView來(lái)處理,然后遍歷通知item中的ChartHScrollView進(jìn)行滑動(dòng)势决。ChartHScrollView是自定義的view阻塑,繼承自HorizonScrollView,用觀察者模式果复。注意 android:descendantFocusability="blocksDescendants"

是覆蓋子類控件而直接獲得焦點(diǎn)陈莽,如果需要有item點(diǎn)擊響應(yīng),必須加這句代碼。這兩個(gè)類的代碼如下:
public class InterceptRelayout extends RelativeLayout{
public InterceptRelayout(Context context) {
super(context);
}

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

public InterceptRelayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
}

}

public class ChartHScrollView extends HorizontalScrollView {
//滑動(dòng)事件的觀察者們走搁,即listview的item中的ChartHScrollView
private ChartScrollViewObservable observable;
//滑動(dòng)距離監(jiān)聽(tīng)
private ScrollViewMoveDistanceListener scrollViewMoveDistanceListener;

public ChartHScrollView(Context context) {
    super(context);
    observable = new ChartScrollViewObservable();
}

public ChartHScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    observable = new ChartScrollViewObservable();
}

public ChartHScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    observable = new ChartScrollViewObservable();
}

public void addObserver(ChartHScrollView observer) {
    observable.addObserver(observer);
}

public void removeObserver(ChartHScrollView observer) {
    observable.addObserver(observer);
}


@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    //通知觀察者們独柑,當(dāng)前滑動(dòng)了多遠(yuǎn)
    observable.notifyObservers(l, t);
    super.onScrollChanged(l, t, oldl, oldt);
    if (scrollViewMoveDistanceListener != null)
        scrollViewMoveDistanceListener.scrollviewMoveDistance(l);
}

public void setScrollViewMoveDistanceListener(ScrollViewMoveDistanceListener scrollViewMoveDistanceListener) {
    this.scrollViewMoveDistanceListener = scrollViewMoveDistanceListener;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    //當(dāng)scrollview的布局發(fā)生改變時(shí),使其與列頭view滑動(dòng)的距離保持一致
    if (scrollViewMoveDistanceListener != null)
        scrollTo(scrollViewMoveDistanceListener.getHeadScrollViewMoveDistance(), 0);
}

}

接下來(lái)就是Adapter了私植,Adapter要做的事情很簡(jiǎn)單忌栅。在getview的回調(diào)中,當(dāng)contentView為null的時(shí)候曲稼,用列頭的ChartHScrollView 對(duì)象索绪,調(diào)用addObserver()方法,傳入item中的ChartHScrollView 對(duì)象參數(shù)贫悄。注意瑞驱!只需要在當(dāng)contentView為null的時(shí)候,添加觀察者就行了窄坦,因?yàn)楫?dāng)contentView唤反!=null時(shí),是復(fù)用的之前的item嫡丙,所以觀察者對(duì)象集已經(jīng)有此對(duì)象了拴袭。代碼如下:

public class Adapter extends BaseAdapter implements ScrollViewMoveDistanceListener, AdapterView.OnItemClickListener {
//列頭的scrollview
private ChartHScrollView hScrollView;
//當(dāng)前滑動(dòng)的距離,當(dāng)item中的ChartHScrollView發(fā)生布局改變時(shí),需要此參數(shù)使其滑動(dòng)scrollDistance距離曙博,與列頭保持一致拥刻。
private volatile int scrollDistance = 0;
private ArrayList<Integer> datas;

public Adapter(ChartHScrollView hScrollView, ArrayList<Integer> datas) {
    this.hScrollView = hScrollView;
    this.datas = datas;

    hScrollView.setScrollViewMoveDistanceListener(this);
}

@Override
public int getCount() {
    return datas.size();
}

@Override
public Object getItem(int position) {
    return datas.get(position);
}

@Override
public long getItemId(int position) {
    return 0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;
    if (convertView == null) {
        convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, null);
        viewHolder = new ViewHolder(convertView);
        //將觀察者對(duì)象添加進(jìn)對(duì)象集
        hScrollView.addObserver(viewHolder.itemScroll);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder) convertView.getTag();
    }

    viewHolder.tvLine.setText("行" + datas.get(position));
    viewHolder.tv1.setText(String.valueOf(datas.get(position)));
    viewHolder.tv2.setText(String.valueOf(datas.get(position) + 1));
    viewHolder.tv3.setText(String.valueOf(datas.get(position) + 2));
    viewHolder.tv4.setText(String.valueOf(datas.get(position) + 3));
    viewHolder.tv5.setText(String.valueOf(datas.get(position) + 4));
    viewHolder.tv6.setText(String.valueOf(datas.get(position) + 5));
    viewHolder.tv7.setText(String.valueOf(datas.get(position) + 6));
    viewHolder.tv8.setText(String.valueOf(datas.get(position) + 7));

    return convertView;
}

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Toast.makeText(parent.getContext(), "點(diǎn)擊位置" + position, Toast.LENGTH_SHORT).show();
}

class ViewHolder {
    private TextView tvLine;
    private ChartHScrollView itemScroll;
    private TextView tv1;
    private TextView tv2;
    private TextView tv3;
    private TextView tv4;
    private TextView tv5;
    private TextView tv6;
    private TextView tv7;
    private TextView tv8;

    public ViewHolder(View view) {
        tvLine = (TextView) view.findViewById(R.id.tv_line);
        tv1 = (TextView) view.findViewById(R.id.tv_1);
        tv2 = (TextView) view.findViewById(R.id.tv_2);
        tv3 = (TextView) view.findViewById(R.id.tv_3);
        tv4 = (TextView) view.findViewById(R.id.tv_4);
        tv5 = (TextView) view.findViewById(R.id.tv_5);
        tv6 = (TextView) view.findViewById(R.id.tv_6);
        tv7 = (TextView) view.findViewById(R.id.tv_7);
        tv8 = (TextView) view.findViewById(R.id.tv_8);

        itemScroll = (ChartHScrollView) view.findViewById(R.id.scroll_item);
        itemScroll.setScrollViewMoveDistanceListener(Adapter.this);
    }

}

/**
 *  列頭的ChartHScrollView移動(dòng)的距離
 * @param distance
 */
@Override
public void scrollviewMoveDistance(int distance) {
    scrollDistance = distance;
}

/**
 * 當(dāng)item中的ChartHScrollView發(fā)生布局改變時(shí),滑動(dòng)scrollDistance使其保持與列頭一致
 * @return 列頭的ChartHScrollView移動(dòng)的距離
 */
@Override
public int getHeadScrollViewMoveDistance() {
    return scrollDistance;
}

}

    接下來(lái)這個(gè)很重要父泳,就是listview上的touch和列頭上的touch事件處理般哼。代碼如下:

public class ListViewAndHeadViewTouchHandle implements View.OnTouchListener {
//列頭的scrollView
private ChartHScrollView scrollView;
private ListView listView;
//列頭
private LinearLayout headLine;

public ListViewAndHeadViewTouchHandle(LinearLayout headLine, ListView listView) {
    scrollView = (ChartHScrollView) headLine.findViewById(R.id.scroll_item);
    this.headLine = headLine;
    this.listView = listView;
    listView.setOnTouchListener(this);
    headLine.setOnTouchListener(this);
}

float x1 = 0, y1 = 0, x2 = 0, y2 = 0;
//區(qū)分當(dāng)前的滑動(dòng)狀態(tài)
boolean isClick = false;
boolean isHorizonMove = true;
boolean isVerticalMove = false;

@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
    switch (arg1.getAction()) {
        case MotionEvent.ACTION_DOWN:
            x1 = arg1.getX();
            y1 = arg1.getY();

            //當(dāng)在列頭 和 listView控件上touch時(shí),將這個(gè)touch的事件分發(fā)給 ScrollView和listView處理惠窄。
            //一個(gè)view只有在接收到了down事件蒸眠,才能繼續(xù)接收之后的觸摸事件。對(duì)這一塊不太熟悉的建議先去看看touch事件的分發(fā)機(jī)制杆融。
            scrollView.onTouchEvent(arg1);
            listView.onTouchEvent(arg1);

            isClick = false;
            isHorizonMove = false;
            isVerticalMove = false;
            break;
        case MotionEvent.ACTION_MOVE:
            x2 = arg1.getX();
            y2 = arg1.getY();

            if (Math.abs(x2 - x1) < 10 && Math.abs(y2 - y1) < 10) {
                //判定當(dāng)前動(dòng)作是點(diǎn)擊
                isClick = true;
                isHorizonMove = false;
                isVerticalMove = false;
            } else {
                isClick = false;
                if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
                    //水平
                    //如果之前有過(guò)垂直操作,則不再更改方向
                    if (!isVerticalMove) {
                        isHorizonMove = true;
                        isVerticalMove = false;
                    }
                } else {
                    //垂直
                    //如果之前有過(guò)水平操作,則不再更改方向
                    if (!isHorizonMove) {
                        isVerticalMove = true;
                        isHorizonMove = false;
                    }
                }
            }

            //垂直動(dòng)作或點(diǎn)擊動(dòng)作交給listView來(lái)處理
            if (isVerticalMove || isClick) {
                listView.onTouchEvent(arg1);
            } else {
                //水平動(dòng)作交給列頭的scrollView來(lái)處理,列頭的scrollView接收后楞卡,會(huì)回調(diào)onScrollChanged(),重寫onScrollChanged()通知觀察者們滑動(dòng)
                scrollView.onTouchEvent(arg1);
            }
            break;

        case MotionEvent.ACTION_UP:
            if (Math.abs(arg1.getX() - x1) < 10 && Math.abs(arg1.getY() - y1) < 10) {
                isClick = true;
            }

            //isClick && arg0 != headLine這個(gè)判斷是防止在列頭點(diǎn)擊時(shí),listview會(huì)響應(yīng)點(diǎn)擊事件
            if ((isClick && arg0 != headLine) || isVerticalMove) {
                listView.onTouchEvent(arg1);
            } else {
                scrollView.onTouchEvent(arg1);
            }

            isClick = false;
            isHorizonMove = false;
            isVerticalMove = false;
            break;
    }

    return true;
}

}

接下來(lái)就是mainActivity的代碼內(nèi)容和布局內(nèi)容了脾歇。

<?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="match_parent"
android:orientation="vertical"
tools:context="com.example.lanyee.demofixheadlist.MainActivity">

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/darker_gray">

    <include
        android:id="@+id/headLine"
        layout="@layout/item_layout"/>
</RelativeLayout>

<ListView
    android:id="@+id/listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

</LinearLayout>

public class MainActivity extends AppCompatActivity {
private ListView listView;
private LinearLayout headLine;

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

    listView = (ListView) findViewById(R.id.listview);
    headLine = (LinearLayout) findViewById(R.id.headLine);

    ArrayList<Integer> datas = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        datas.add(i);
    }

    Adapter adapter = new Adapter((ChartHScrollView) headLine.findViewById(R.id.scroll_item), datas);
    listView.setAdapter(adapter);

    //統(tǒng)一處理列頭和listview的touch事件
    new ListViewAndHeadViewTouchHandle(headLine, listView);

    listView.setOnItemClickListener(adapter);
}

}

監(jiān)聽(tīng)文件代碼:

public interface ScrollViewMoveDistanceListener {
    void scrollviewMoveDistance(int distance);

    int getHeadScrollViewMoveDistance();
}
   所有的代碼都已經(jīng)貼出來(lái)啦蒋腮,代碼中也加了比較詳細(xì)的注釋。平時(shí)很少編輯文章藕各,所以表述可能不是很清楚池摧。另外這個(gè)文本編輯器貼代碼塊好像不是很好用。如果有疑問(wèn)或更好的建議激况,歡迎留言評(píng)論作彤,我們共同探討膘魄。
                                                               最后謝謝你的觀看。
                                                               轉(zhuǎn)載請(qǐng)注明出處竭讳。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末创葡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子代咸,更是在濱河造成了極大的恐慌蹈丸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呐芥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡奋岁,警方通過(guò)查閱死者的電腦和手機(jī)思瘟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闻伶,“玉大人滨攻,你說(shuō)我怎么就攤上這事±逗玻” “怎么了光绕?”我有些...
    開(kāi)封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)畜份。 經(jīng)常有香客問(wèn)我诞帐,道長(zhǎng),這世上最難降的妖魔是什么爆雹? 我笑而不...
    開(kāi)封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任停蕉,我火速辦了婚禮,結(jié)果婚禮上钙态,老公的妹妹穿的比我還像新娘慧起。我一直安慰自己,他們只是感情好册倒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布蚓挤。 她就那樣靜靜地躺著,像睡著了一般驻子。 火紅的嫁衣襯著肌膚如雪灿意。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天拴孤,我揣著相機(jī)與錄音脾歧,去河邊找鬼。 笑死演熟,一個(gè)胖子當(dāng)著我的面吹牛鞭执,可吹牛的內(nèi)容都是我干的司顿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼兄纺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼大溜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起估脆,我...
    開(kāi)封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钦奋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后疙赠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體付材,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年圃阳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了厌衔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捍岳,死狀恐怖富寿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锣夹,我是刑警寧澤页徐,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站银萍,受9級(jí)特大地震影響变勇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砖顷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一贰锁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滤蝠,春花似錦豌熄、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至览闰,卻和暖如春芯肤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背压鉴。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工崖咨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人油吭。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓击蹲,卻偏偏與公主長(zhǎng)得像署拟,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歌豺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容