第六章 活用列表和适配器
Hack 24 处理空列表
ListView以及其它继承自AdapterView的类可以通过setEmptyView(View)方法处理空状态,当需要绘制AdapterView时,如果适配器为null或适配器的isEmpty()方法返回true,此时会显示setEmptyView(View)方法所设置的视图。
假设:需要创建一个应用程序处理TODO列表,主界面是一个显示所有TODO项的ListView,但当第一次启动该应用程序时,列表是空的。对于这种空状态,我们可以在界面上绘制一张图片。布局文件代码如下:
<FrameLayout
...>
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height"match_parent"
android:src="@drawable/empty_view"/>
</FrameLayout>
Activity中实现,代码如下:
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ListView mListView = (ListView)findViewById(R.id.list_view);
mListView.setEmptyView(findViewById(R.id.empty_view));
}
这里我们为了测试,所以没有为ListView设置适配器,运行代码就会显示imageview。
我吗也可以尝试使用ViewStub作为空状态时显示的视图。该方法可以保证在不需要显示该视图时,不必填充(inflate)该视图。
Hack 25 通过ViewHolder优化适配器
Adapter开发文档介绍:
“Adapter对象是AdapterView和底层数据间的桥梁。Adapter用于访问数据项,并且负责为数据项生成视图。“
AdapterView是一个抽象类,用于那些需要通过Adapter填充自身的视图。常见子类是ListView。显示AdapterView时,会调用Adapter的getView()方法创建并添加每个子条目的视图。Adapter的getView()方法就是用来创建这些视图的。Adapter并不会为每行数据都创建一个新视图,而是提供了回收视图的方法。
运行机制:
当getView()方法被调用时,如果convertView参数不为null,就使用convertView,不用在新建视图。我们需要通过convertView.findViewById()方法获取每个ui控件的引用然后使用与当前位置绑定的数据来填充视图。
这里我们可以使用ViewHolder模式,ViewHolder是一个静态类,可以用于保存每行视图以避免每次调用getView()时都会调用findViewById()。
Hack 26 为ListView添加分段标头
分段标头就像是有的手机中通讯录的样式,根据姓名首字母进行分组,分段标头一直显示在屏幕顶端。
实现方式:
一。
开发者实现这个需求通常是创建两种类型的列表:
1.常规列表用于显示数据
2.特殊列表用于显示分段标头
这种方式需要重写getViewTypeCount()方法,让其返回2;然后修改getView()方法,在该方法中创建并返回对应类型的列表项。
缺点:会导致代码逻辑混乱。
二。
可以在列表项中嵌入分段标头,然后根据需求显示或隐藏分段标头。我们可以创建一个特殊的TextView,让其叠加在列表的顶部,当列表滚动到一个新的分段时,就更新其内容。
优点:简化了创建列表以及选项列表项的逻辑。
26.1 创建列表布局
我们先在单独文件中为分段标头创建布局,这样就可以在随着列表滚动的分段标头和列表顶部的固定分段标头中复用这个布局文件。代码如下:
<TextView
...
android:id="@+id/header"
android:background="#0000ff"
style="@android:style/TextAppearance.Small"/>
包含固定分段标头的XML布局文件,代码如下:
<FrameLayout ...>
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<include layout="@layout/header"/>
</FrameLayout>
列表使用Android标准的列表ID,因此可以在ListActivity的子类中使用它。将分段标头包含在帧布局中,这样标头就可以与列表重叠在一起了。
创建列表的布局文件,代码如下:
<LinearLayout ...>
<include layout="@layout/header"/>
<TextView
android:id="@+id/label"
style=""@android:style/TextAppearance.Large
android:layout_width="mathc_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
26.2 创建可视分段标头
与其它创建分段列表的方式不同之处在于:开发者只需要重写getView()方法。我们不需要返回多种类型的视图,也不需要在分段列表与原始列表间转换数据项的位置(position)。代码如下:
public class SectionAdapter extends ArrayAdapter<String>{
private Activity activity;
public SectionAdapter(Activity activity,String[] objects){
//1.为自定义视图指定xml布局文件
super(activity,R.layout.list_item,R.id.label.bjects);
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);
//2.检查列表顶部起始字母是否发生改变
if(position == 0 || getItem(position - 1).charAt(0) != label.charAt(0)){
//3.显示分段标头,并更改分段标头的文本内容
header.setVisibility(View.VISIBLE);
header.setText(label.substring(0,1));
}else{
//4.隐藏分段标头
header.setVisibility(View.GONE);
}
return super.getView(position,view,parent);
}
}
用于配置屏幕顶部悬浮分段标头辅助方法,代码如下:
//1.用于访问分段标头
private TextView topHeader;
...
private void setTopHeader(int pos){
final String text = Countries.COUNTRIES[pos].substring(0,1);
//2.更新文本内容
topHeader.setText(text);
}
26.3 最后一步
我们在Activity的onCreate()方法中国年整合所有内容。代码如下:
private int topVisiblePosition;
...
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.list);
topHeader = (TextView)findViewById(R.id.top);
setListAdapter(new SectionAdapter(this,Countries.COUNTRIES));
//1.设置滚动监听器
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;
//2.调用辅助方法
setTopHeader(firstVisibleItem);
}
}
});
//3.初始化第一个列表项的分段标头
setTopHeader(0);
}
Hack 27 使用Activity和Delegate与适配器交互
委托模式(Delegate Pattern)在iOS开发中被大量使用。如:创建http请求,开发者可以设置一个委托对象,当请求处理完毕后指定一些操作。
在这里开发者可以使用关注点分离(separation哦分concerns,SoC)的设计原则。
委托模式:可以帮助开发者把所有业务逻辑从适配器中移到Activity中。
实现思路:
如:我们在适配器中实现删除按钮点击处理器,但并不在适配器中实现删除对象的方法。我们通过一个委托接口调用Activity的方法删除对象。代码如下:
public class NumbersAdapter extends ArrayAdapter<Integer>{
//1.定义委托接口
public static interface NumbersAdapterDelegate{
void removeItem(Integer value);
}
private LayoutInflater mInflator;
private NumbersAdapterDelegate mDelegate;
public NumbersAdapter(Context context,List<Integer> objects){
super(context,0,objects);
mInflator = LayoutInflater.from(context);
}
@Override
public view getView(int position,View cv,ViewGroup parent){
if(null == cv){
cv = mInflator.inflate(R.layout.number_row,parent,false);
}
final Integer value = getItem(position);
TextView tv = (TextView)cv.findViewById(R.id.number_row_text);
tv.setText(value.toString());
View button = cv.findViewById(R.id.numbers_row_button);
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
if(null != mDelegate){
//2.删除对象
mDelegate.removeItem(value);
}
}
});
return cv;
}
//3.为适配器设置委托对象
public void setDelegate(NumbersAdapterDelegate delegate){
mDelegate = delegate;
}
}
适配器准备就绪,Activity的代码如下:
//1.实现NumberAdapterDelegate接口
public class MainActivity extends Activity implements NumbersAdapterDelegate{
private ListView mListView;
private ArrayList<Integer> mNumbers;
private NumbersAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mListView = () findViewById(R.id.main_listview);
mNumbers = new ArrayList<Integer>();
mAdapter = new NumbersAdapter(this,mNumbers);
mListView.setAdapter(mAdapter);
}
@Override
protected void onResume(){
super.onResume();
//2.在onResume()方法中注册委托对象
mAdapter.setDelegate(this);
}
@Override
protected void onPause(){
super.onPause();
//3.在onPause()方法中取消注册委托对象
mAdapter.setDelegate(null);
}
@Override
public void removeItem(Integer value){
//从列表中移除指定项,然后通知适配器绑定的数据发生变化
mNumbers.remove(value);
mAdapter.notifyDataSetChanged();
}
}
我们在onCreate()方法中将当前Activity设置为适配器的委托对象,而是在onResume()方法中注册代理对象,然后在onPause()方法中取消注册。这样做的目的是为了确保只在Activity显示在屏幕上的时候才作为委托对象使用。
Hack 28 充分利用ListView的头视图
需求:在界面上提供一个显示图片的相册和一个显示数字的列表,当向下滚动界面时,相册也会随之滚动,直到图片消失。
一般想法:把GalleryListView这个两个控件置于ScrollView中。
但这样是不行的,因为ListView本身就是一种ScrollView,会出现滚动事件的冲突。
可以使用的做法:
ListView提供了可以为列表添加头视图(Header Veiw)和尾视图(Footer View)的方法。
使用如上方法把Gallery设置为ListView的代码如下:
public class MainActivity extends Activity{
private static final String[] NUMBERS = {"1","2","3","4","5"};
private Gallery mGallery;
private View mHeader;
private ListView mListView;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//1.获取ListView的引用
mListView = (ListView) findViewById(R.id.main_listview);
//2.创建需要被填充的xml文件
LayoutInflater inflator = LayoutInflater.from(this);
mHeader = inflator.inflate(R.layout.header,mListView,false);
mGallery = (Gallery) mHeader.findViewById(R.id.gallery);
mGallery.setAdapter(new ImageAdapter(this));
//3.替换视图的原始LayoutParams
ListView.LayoutParams params = new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,ListView.LayoutParams.WRAP_CONTENT);
mHeader.setLayoutParams(params);
//4.将这个头视图添加到ListView中
mListView.addHeaderView(mHeader,null,false);
//5.为ListView设置适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.list_item,NUMBERS);
mListView.setAdapter(adapter);
//6.添加一个onItemClick监视器
mListView.setOnItemClickListener(new OnItemClickListener(){
@Override
public void onItemClick(AdapterView<?> parent,View view,int position,long id){
mGallery.setSelection(position-1);
}
});
}
}
我们可以通过监听器实现点击列表中某个数字时,滚动到相册中对应图片。
Hack 29 在ViewPager中处理转屏
ViewPager可以用于创建任何需要显示分页视图的应用程序,用法与AdapterView相似。
假设需要创建一个电子杂志风格的app,要在不用分页中分别采取不同横竖屏显示。
需要的组件:
Activity:持有ViewPager的引用、控制屏幕旋转
ColorFragment类:用于显示颜色,并在屏幕中央显示文本内容
ColorAdapter类:负责创建Fragment、通知Activity对于哪个Fragment需要改变屏幕显示方向
ViewPager:使用ColorAdapter显示Fragment
代码如下:
public class MainActivity extends FragmentActivity{
private ViewPager mViewPager;
private ColorAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
//1.设置默认屏幕方向
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(R.layout.main);
//2.引用viewpager
mViewPager = (ViewPager) findViewById();
mAdapter = new ColorAdapter();
mViewPager.setAdapter();
//3.添加监听器
mViewPager.setOnPageChangeListener(new OnPageChangeListener(){
@Override
public void onPageSelected(){
if(mAdapter.usesLandscape(position)){
allowOrientationChanges();
}else{
enforcePortrait();
}
}
...
});
}
//4.实现方法
public void allowOrientationChanges(){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
public void enforcePortrait(){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
第一步是将默认屏幕方向设置为竖屏,如果视图没有指定是否需要改变屏幕方向,就竖屏显示该视图。代码中持有ViewPager的引用,我们为其设置ColorAdapter,此外还需添加一个监听器用于监听页面切换。该监听器会通过适配器判断是否需要改变屏幕方向,最后调用Activity类提供的setRequestedOrientation()方法改变屏幕方向。
ViewPager类是Android用于水平视图切换的标准实现,可以向后兼容到API level 4。我吗最好每个视图都可以支持两种不同屏幕方向,当用户使用应用程序的时候,如果可以改变屏幕方向,会提升用户体验。
Hack 30 ListView的选择模式
ListView定义了choiceMode属性,开发文档如下:
“用于为视图定义选择行为。默认情况下,列表是没有任何选择行为的。如果把choiceMode设置为singleChoice,列表允许又一个列表项处于被选择状态。如果把choiceMode设置为multipleChoice,那么列表允许有任意数量的列表项处于被选择状态。“
ListView另一个有趣的功能是:不管使用singleChoice还是multipleChoice,所选列表项的位置信息都会被自动保存。
示例代码如下:
<LinearLayout ...>
<Button
android:onClick="onPickCountryClick"
.../>
<ListView
android:choiceMode="singleChoice"
.../>
</LinearLayout>
使用按钮来执行指定方法,我们使用选择模式让ListView显示列表。
Activity代码如下:
public class MainActivity extends Activity{
private ListView mListView;
private CountryAdapter mAdapter;
private List<Country> mCountries;
private String mToastFmt;
@Override
public void onCreate(...){
super.onCreate(...);
setContentView(R.layout.activity_main);
//向列表中填充信息辅助方法
createCountriesList();
mToastFmt = getString(R.string.activity_main_toast_fmt);
mAdapter = new CountryAdapter(this,-1,mCountries);
mListView = (ListView) findViewById(R.id.activity_main_list);
mListVeiw.setAdapter(mAdapter);
}
public void onPickCountryClick(View v){
int pos = mListView.getCheckedItemPosition();
if(ListView.INVALID_POSITION != pos){
String msg = String.format(mToastFmt,mCountries.get(pos).getName());
Toast.makeText(this,msg,Toasst.LENGTH_SHORT).show();
}
}
}
布局文件代码如下:
<LinearLayout ...>
<TextView
android:id="@+id/country_view_title"
.../>
<CheckBox
android:id="@+id/country_view_checkbox"
.../>
</LinearLayout>
ListView是以某种方式通过Checkable接口来处理视图的选择状态,在ListView源码中有如下代码:
if(mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null){
if(child instanceof Checkable){
((Checkable)child).setChecked(mCheckStates.get(position));
}
}
所以如果需要ListView处理选择行为,需要令列表项对应的自定义视图实现Checkable接口。遗憾的是,这种方式必须创建自定义视图。
我们创建的CountryView类代码如下:
public class CountryView extends LinearLayout implements Checkable{
private TextView mTitle;
private CheckBox mCheckBox;
public CountryView(Context context,AttributeSet attrs){
super(context,attrs);
//填充布局
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.country_view,this,true);
mTitle = (TextView) v.findViewById(R.id.country_view_title);
mCheckBox = (CheckBox) v.findViewById(R.id.country_view_checkbox);
}
public void setTitle(String title){
mTitle.setText(title);
}
@Override
public boolean isChecked(){
//重写所有Checkable接口方法
return mCheckBox.isChecked();
}
@Override
public void setChecked(boolean checked){
mCheckBox.setChecked(checked);
}
@Override
public void toggle(){
mCheckBox.toggle();
}
}
这里每个被实现的接口方法都调用了mCheckBox的相应方法。这意味着,在ListView中选择一行时,会调用CountryView的setChecked()方法。
这时我们点击某行时CheckBox中没有打勾,但点击CheckBox时,CheckBox就会打勾,这是哪里出问题了呢?
问题出现在我们添加了一个可获取焦点的ui控件CheckBox。解决方法就是设置CheckBox不能点击。
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"