2BAB's Engineering Blog

ListView 两种固定标头的技巧

##第一种情况:
界面上有三个view,上面是一个要隐藏的View A,中间是一个不隐藏的View B,下面有一个ListView C。当C向上滑动的时候,如果A还没有被隐藏,就随着滑动而隐藏,当A完全隐藏之后,B就一直在最上面,C还可以继续向上滑动;当C向下滑动的到底后A逐渐显示出来。

##突发奇想的省力方法:

给 ListView C 添加一个HeadView(包含A、B),然后另外准备一个外部的B在屏幕顶部,一开始不可见。ListView当前滚动高度超过A的高度时,显示外部的B;滚动高度小于A时隐藏内部的B。

##效果:

效果图

##代码:

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package net.bingyan.hacklistview;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
public class MainActivity extends Activity {
private ListView listView;
private LinearLayout sectionB;
private int aHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sectionB = (LinearLayout) findViewById(R.id.main_section_b_outside);
aHeight = getResources().getDimensionPixelSize(R.dimen.main_a_height);
initListView();
}
private void initListView(){
listView = (ListView) findViewById(R.id.main_list_view);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1);
for (int i = 0; i<100; i++){
adapter.add("item "+String.valueOf(i));
}
listView.setAdapter(adapter);
View headerView = LayoutInflater.from(this).inflate(R.layout.main_header,null);
listView.addHeaderView(headerView);
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (getScrollY() >= aHeight) {
if (sectionB.getVisibility() == View.INVISIBLE) {
sectionB.setVisibility(View.VISIBLE);
}
} else if (getScrollY() < aHeight){
if (sectionB.getVisibility() == View.VISIBLE){
sectionB.setVisibility(View.INVISIBLE);
}
}
}
});
}
//获取滚动距离
public int getScrollY() {
View c = listView.getChildAt(0);
if (c == null) {
return 0;
}
int firstVisiblePosition = listView.getFirstVisiblePosition();
int top = c.getTop();
int headerHeight = 0;
if (firstVisiblePosition >= 1) {
headerHeight = listView.getHeight();
}
return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}
}

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<FrameLayout 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=".MainActivity">
<ListView
android:id="@+id/main_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/material_deep_teal_200" />
<LinearLayout
android:id="@+id/main_section_b_outside"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible">
<include
layout="@layout/main_section_b"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</FrameLayout>

main_header.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/main_section_a"
android:layout_width="match_parent"
android:layout_height="@dimen/main_a_height"
android:background="@color/material_blue_grey_800">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="@dimen/main_text_size"
android:text="@string/main_section_a"/>
</RelativeLayout>
<include
android:id="@+id/main_section_b_inside"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/main_section_b"/>
</LinearLayout>

main_section_b.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/main_section_b"
android:layout_width="match_parent"
android:layout_height="@dimen/main_b_height"
android:background="@android:color/holo_orange_light"
android:gravity="center"
android:textSize="@dimen/main_text_size"
android:text="@string/main_section_b"/>
</LinearLayout>

备注:

  • 如果 View B 是一个复杂的 View,上面的方案可能需要改进。因为对 内外两个 View B 的一些代码操作可能要写两遍。我现在想的是把 headerView 的 B 去掉,保留同样大小的白色区域,然后外部的 B 根据 ListView 的滚动同步网上滚。

  • 有个小 bug 是滚动条在外部的 B 刚显示时会被遮住一部分 = = 不过现在很多设计都不用滚动条了,实在没办法就自己写一个吧。


##第二种情况:
类似于联系人列表的场景,即按首字母对ListView进行分段,并且当前分段标头会停留在ListView最上方。

##从《50 Android Hacks》中学到的方法:

一方面,每个 List Item 都添加一个隐藏的分段标头,当第 n 个 Item 与第 n-1 个 Item 的首字母不相同时(或者其他分割条件下的不同),显示这个分段标头。另一方面,在ListView的上层放一个隐藏的标头,标识当前显示的组别。

##效果:

源码地址

##代码:

header.xml

1
2
3
4
5
6
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/header"
style="@android:style/TextAppearance.Small"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/material_deep_teal_200" />

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<include layout="@layout/header" />
</FrameLayout>

list_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<include layout="@layout/header" />
<TextView
android:id="@+id/label"
style="@android:style/TextAppearance.Large"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import android.app.ListActivity;
import android.os.Bundle;
import android.widget.AbsListView;
import android.widget.TextView;
public class MainActivity extends ListActivity {
private TextView topHeader;
private int topVisiblePosition = -1;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
topHeader = (TextView) findViewById(R.id.header);
setListAdapter(new SectionAdapter(this, Countries.COUNTRIES));// Countries.COUNTRIES 是一个静态String数组
getListView().setOnScrollListener(
new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view,
int scrollState) {
// Empty.
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (firstVisibleItem != topVisiblePosition) {
topVisiblePosition = firstVisibleItem;
setTopHeader(firstVisibleItem);
}
}
});
setTopHeader(0);
}
private void setTopHeader(int pos) {
final String text = Countries.COUNTRIES[pos].substring(0, 1);
topHeader.setText(text);
}
}

SectionAdapter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class SectionAdapter extends ArrayAdapter<String> {
private Activity activity;
public SectionAdapter(Activity activity, String[] objects) {
super(activity, R.layout.list_item, R.id.label, objects);
this.activity = activity;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
view = activity.getLayoutInflater().inflate(R.layout.list_item,
parent, false);
}
TextView header = (TextView) view.findViewById(R.id.header);
String label = getItem(position);
if (position == 0
|| getItem(position - 1).charAt(0) != label.charAt(0)) {
header.setVisibility(View.VISIBLE);
header.setText(label.substring(0, 1));
} else {
header.setVisibility(View.GONE);
}
return super.getView(position, view, parent);
}
}

备注:

  • 没有下一个分段标头把上一个顶出去的效果,而只能对置顶的分段标头setText。

  • listview 从下面快速滑动到顶部后,会有回弹效果,造成分段标头瞬间变高(或出现两个分段标头)。

讨论请发邮件到 xx2bab@gmail.com
自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0