一、ListView的简单介绍
1. ListView 概念:
ListView是Android中最重要的组件之一,几乎每个Android应用中都会使用ListView。它以垂直列表的方式列出所需的列表项。
2. ListView的两个职责:
(1)将数据填充到布局;
(2)处理用户的选择点击等操作。
3. 列表的显示需要三个元素
(1)ListVeiw:用来展示列表的View;
(2)适配器: 用来把数据映射到ListView上的中介;
(3)数据源: 具体的将被映射的字符串,图片,或者基本组件。
4. 什么是适配器?
适配器是一个连接数据和AdapterView的桥梁,通过它能有效地实现数据与AdapterView的分离设置,使AdapterView与数据的绑定更加简便,修改更加方便。
Android开发中的适配器一共可分为:
ArrayAdapter,
BaseAdapter,
CursorAdapter,
HeaderViewListAdapter,
ResourceCursorAdapter,
SimpleAdapter,
SimpleCursorAdapter,
WrapperListAdapter
其中,ArrayAdapter和SimpleAdapter最为常见。
ArrayAdapter最为简单,只能展示一行字;
SimpleAdapter有最好的扩充性,可以自定义各种各样的布局,除了文本外,还可以放ImageView(图片)、Button(按钮)、CheckBox(复选框)等等;
但是实际工作中,常用自定义适配器。即继承于BaseAdapter的自定义适配器类。
5. ListView的常用UI属性:
android:divider
android:dividerHeight
二、ListView的创建与使用
1. ArrayAdapter适配器
先看下面代码:
package com.danny_jiang.day08_listview_introduce;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1 初始化ListView
ListView listView = (ListView) findViewById(R.id.list_Main);
// 2 初始化数据源
String[] data = getResources().getStringArray(R.array.arr);
// 3 初始化适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, data);
// 4 将适配器设置到ListView控件中
listView.setAdapter(adapter);
// 设置ListView的单击事件
listView.setOnItemClickListener(new OnItemClickListener() {
/**
* @param parent
* ListView
* @param view
* 所点击的item视图,也就是TextView
* @param position
* 所点击item的位置
* @param id
* 所点击item的id
*/
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
String content = textView.getText().toString();
Toast.makeText(MainActivity.this, "点击了 " + content,
Toast.LENGTH_SHORT).show();
}
}
});
// 设置ListView的长按事件
listView.setOnItemLongClickListener(new OnItemLongClickListener() {
/**
* @param parent
* ListView
* @param view
* 所点击的item视图,也就是TextView
* @param position
* 所点击item的位置
* @param id
* 所点击item的id
*/
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
String content = textView.getText().toString();
Toast.makeText(MainActivity.this, "长按了 " + content,
Toast.LENGTH_SHORT).show();
}
// 返回true,表示将单击事件进行拦截
return true;
}
});
}
}
布局文件:
<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="${relativePackage}.${activityClass}" >
<!--
通过ListView标签可以声明列表视图
android:divider="#FF0000" 设置ListView间隙之间的颜色,必须指定dividerHeight后才会生效
android:dividerHeight="2dp" 设置ListView间隙之间的间隔
-->
<ListView
android:id="@+id/list_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#FF0000"
android:dividerHeight="2dp" >
</ListView>
</RelativeLayout>
strings.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Day08_ListView_Introduce</string>
<string name="hello_world">Hello world!</string>
<string-array name="arr">
<item>西游记</item>
<item>红楼梦</item>
<item>李尔王</item>
<item>麦克白</item>
<item>西游记</item>
<item>红楼梦</item>
<item>李尔王</item>
<item>麦克白</item>
<item>西游记</item>
<item>红楼梦</item>
<item>李尔王</item>
<item>麦克白</item>
<item>西游记</item>
<item>红楼梦</item>
<item>李尔王</item>
<item>麦克白</item>
</string-array>
</resources>
效果图如下:
2. SimpleAdapter适配器
2.1 使用系统自带item布局
先看下面代码:
package com.danny_jiang.day08_listview_simpleadapter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;
public class MainActivity extends Activity {
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化ListView控件
listView = (ListView) findViewById(R.id.list_Main);
// 初始化数据源
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
for (int i = 0; i < 10; i++) {
Map<String, String> map = new HashMap<String, String>();
map.put("name", "Android-" + i);
map.put("age", "age-" + i);
map.put("gender", "male");
list.add(map);
}
// 初始化item的布局文件(使用系统自带布局)
int itemLayout = android.R.layout.simple_list_item_2;
/**
* 初始化SimpleAdapter适配器
*
* @param this
* 上下文
* @param list
* 数据源 List<Map>
* @param itemLayout
* item的布局文件ID,使用此item来显示ListVirw中的每一个item
* @param new String[] { "gender", "age" }
* 是一系列String,key的数组,key是Map中put时所使用的key值
* @param new int[] { android.R.id.text1, android.R.id.text2 }
* 一些列id的int数组,id的顺序决定了数据源中item的摆放顺序
*
*/
SimpleAdapter adapter = new SimpleAdapter(this, list, itemLayout,
new String[] { "gender", "age" },
new int[] { android.R.id.text1, android.R.id.text2 });
// ListView设置适配器
listView.setAdapter(adapter);
}
}
布局文件:
<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="${relativePackage}.${activityClass}" >
<ListView
android:id="@+id/list_Main"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</RelativeLayout>
效果图如下:
2.2 自定义item布局文件
代码如下:
package com.danny_jiang.day08_listview_iconitem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;
public class MainActivity extends Activity {
private ListView listView;
// 声明需要显示到ListView上的图片资源ID
private int[] iconId = new int[] { R.drawable.tv01, R.drawable.tv02,
R.drawable.tv03, R.drawable.tv04, R.drawable.tv05,
R.drawable.tv06, R.drawable.tv07, R.drawable.tv08,
R.drawable.tv09, R.drawable.tv10, R.drawable.tv01,};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化ListView控件
listView = (ListView) findViewById(R.id.list_main);
// 初始化数据源
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (int i = 0; i < iconId.length; i++) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", "Android-" + i);
map.put("icon", iconId[i]);
list.add(map);
}
// 初始化item布局
int itemLayout = R.layout.list_item;
// 初始化适配器SimpleAdapter
SimpleAdapter adapter = new SimpleAdapter(this, list, itemLayout,
new String[] { "icon", "name" }, new int[] { R.id.image,
R.id.name });
// ListView设置适配器
listView.setAdapter(adapter);
}
}
布局文件:
<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="${relativePackage}.${activityClass}" >
<ListView
android:id="@+id/list_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
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="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal" >
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="TEXT" />
<ImageView
android:id="@+id/image"
android:layout_width="200dp"
android:layout_height="120dp"
android:layout_marginLeft="50dp"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />
</LinearLayout>
效果图如下:
3. BaseAdapter适配器
代码如下:
package com.danny_jiang.day08_listview_baseadapter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class MainActivity extends Activity {
private ListView listView;
private List<Map<String, String>> list = new ArrayList<Map<String,String>>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.list_Main);
for(int i = 0; i < 50; i++) {
Map<String, String> map = new HashMap<String, String>();
map.put("key", "value" + i);
map.put("asd", "asd" + i);
list.add(map);
}
MyBaseAdapter adapter = new MyBaseAdapter(this, list);
listView.setAdapter(adapter);
}
}
自定义适配器 MyBaseAdapter:
package com.danny_jiang.day08_listview_baseadapter;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
public class MyBaseAdapter extends BaseAdapter{
// 在自定义Adapter中声明全局的数据源对象
private List<Map<String, String>> list;
private Context context;
// 声明布局填充器,通过它可以填充xml布局文件并返回View对象
private LayoutInflater inflater;
public MyBaseAdapter(Context context, List<Map<String, String>> list) {
this.context = context;
inflater = LayoutInflater.from(context);
this.list = list;
}
/**
* 返回ListView需要显示条数
* 一般情况下需要返回数据源的长度
*/
@Override
public int getCount() {
// 如果数据源是null,则返回0,否则返回数据源的长度
return list == null ? 0 : list.size();
}
/**
* 返回某位置上的item对象
* 当调用ListView.getItemAtPosition(0/1)
* 应当返回数据源中position对应的对象
*/
@Override
public Object getItem(int position) {
return list.get(position);
}
/**
* 返回position对应item的id,一般返回position
*/
@Override
public long getItemId(int position) {
return position;
}
/**
* 返回ListView中position所对应的item需要显示的视图对象
* position从0开始
*
* @param position 需要填充视图的视图位置,从0开始
* @param convertView ListView的优化机制,Android系统对getView返回视图的一个复用机制
* @param parent 将item视图填充到的AdapterView,也就是ListView本身
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
// 通过LayoutInflater填充xml布局文件,将获取到的convertView对象返回
convertView = inflater.inflate(R.layout.list_item, null);
// 初始化holder,并为其属性赋值
holder = new ViewHolder();
holder.text1 = (TextView) convertView.findViewById(R.id.text1);
holder.text2 = (TextView) convertView.findViewById(R.id.text2);
// 给convertView添加标签holder
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 获取当前位置中数据源的元素
Map<String, String> map = list.get(position);
String value = map.get("key");
String asd = map.get("asd");
// 为每个item中的所有UI控件设置属性值
holder.text1.setText(value);
holder.text2.setText(asd);
return convertView;
}
class ViewHolder{
TextView text1;
TextView text2;
}
}
布局文件:
<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="${relativePackage}.${activityClass}" >
<ListView
android:id="@+id/list_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
自定义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="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal" >
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="TEXT" />
<ImageView
android:id="@+id/image"
android:layout_width="200dp"
android:layout_height="120dp"
android:layout_marginLeft="50dp"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />
</LinearLayout>
效果图如下:
三、convertView原理:
Adapter的作用就是ListView界面与数据之间的桥梁,当列表里的每一项显示到页面时,都会调用Adapter的getView方法返回一个View。
如果在我们的列表有上千项时会是什么样的?是不是会占用极大的系统资源?
Android中有个叫做Recycler的构件,如果你有100个item,其中只有可见的项目存在内存中,其他的在Recycler中。ListView先请求一个type1视图(getView),然后请求其他可见的item,convertView在getView中是空(null)的。
当item1滚出屏幕,并且一个新的item从屏幕底端上来时,ListView再请求一个type1视图,convertView此时不是空值了,它的值是item1。你只需设定新的数据,然后返回convertView,不必重新创建一个视图。
四、什么是listview点击的灵异事件?
项目中的ListView不仅仅是简单的文字,常常需要自己定义ListView,如果自己定义的Item中存在诸如ImageButton,Button,CheckBox等子控件,此时这些子控件会将焦点获取到,所以当点击item中的子控件时有变化,而item本身的点击没有响应。
解决方案的关键是:android:descendantFocusability
当一个view获取焦点时,定义ViewGroup及其子控件之间的关系。
属性的值有三种:
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
通常我们用到的是第三种,即在Item布局的根布局加android:descendantFocusability=”blocksDescendants”的属性(阻塞子控件抢夺焦点,让Item具有焦点。这样ListView的onItemClick就能被正确触发,同时item上的button等控件在被点击时照样可以触发自身的点击事件)就好了,至此ListView点击的灵异事件告一段落。
五、ListView优化中的细节问题:
1、android:layout_height属性:
必须将ListView的布局高度属性设置为非“wrap_content“,(可以是match_parent/fill parent/400dp等绝对数值),如果ListView的布局高度为“wrap_content”,那么getView()就会重复调用。一般来说,一个item会被调用三次左右。
2、ViewHolder:
利用ViewHolder内部类,将item布局文件中需要展示的控件定义为属性(其实ViewHolder就是一个自定义的模型类)。这样就把item中散在的多个控件合成一个整体,这样可以有效地避免图片错位。
3、convertView:
ListView的加载是一个item一个item的加载,这样就会每次都inflate一个item布局,然后findViewById一遍该布局上的所有控件。当数据量大的时候,是不可想象的。而利用Recycle回收利用就可以解决问题。所以要善于重复利用convertView,这样可以减少填充布局的过程,减少ViewHolder对象实例化的次数。减少内存开销,提高性能。
4、convertView的setTag():
利用setTag()方法将ViewHolder对象作为标签附加到convertView上,当convertView被重复利用的时候,因为上面有ViewHolder对象,所以convertView就具有了ViewHolder中的几个属性,这样就节省了findViewById()这个过程。如果一个item有三个控件,如果有100条item,那么在加载数据过程中,就就相当于节省了几百次findViewById(),节约了执行findViewById()的时间,提升了加载速度,节省了性能的开销。
5、LayoutInflater对象的inflate()方法:
inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。
inflate()方法还有个接收三个参数的方法重载
1.如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
2.如果root不为null,attachToRoot设为true,则会在加载的布局文件的最外层再嵌套一层root布局。
3.如果root不为null,attachToRoot设为false,则root参数失去作用。
4.在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。
所以在使用LayoutInflater填充布局的时候,要注意inflate()方法的参数。如果是两个参数,则第二个参数可以采用null;如果使用三个参数的方法,则要注意参数之间的搭配。
六、ListView分页的实现
1. 目的:
Android 应用开发中,采用ListView组件来展示数据是很常用的功能,当一个应用要展现很多的数据时,一般情况下都不会把所有的数据一次就展示出来,而是通过 分页的形式来展示数据,这样会有更好的用户体验。因此,很多应用都是采用分批次加载的形式来获取用户所需的数据。例如:微博客户端可能会在用户滑 动至列表底端时自动加载下一页数据,也可能在底部放置一个”查看更多”按钮,用户点击后,加载下一页数据。
2. 核心技术点:
a. 借助 ListView组件的OnScrollListener监听事件,去判断何时该加载新数据;
b.往服务器get传递表示页码的参数:page。而该page会每加载一屏数据后自动加一;
c.利用addAll()方法不断往list集合末端添加新数据,使得适配器的数据源每新加载一屏数据就发生变化;
d.利用适配器对象的notifyDataSetChanged()方法。该方法的作用是通知适配器自己及与该数据有关的view,数据已经发生变动,要刷新自己、更新数据。
案例演示:
package com.danny_jiang.day09_listview_scroll;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AbsListView.LayoutParams;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.ListView;
public class MainActivity extends Activity {
private ListView listView;
private boolean isLastShown;
private List<String> data = new ArrayList<String>();
private ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.list_Main);
for(int i = 0; i < 20; i++) {
data.add("Android-" + i);
}
adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, data);
listView.setAdapter(adapter);
/**
* 通过ListView.addHeaderView方法可以给ListView添加一个头视图View
*/
ImageView header = new ImageView(this);
/**
* 当需要动态修改UI控件的宽高,需要使用LayoutParams参数对象,
* 指定宽高,并将此LayoutParams设置到相应的UI控件上
*/
//初始化LayoutParams时,需要指定宽高
LayoutParams param = new LayoutParams(LayoutParams.MATCH_PARENT, 200);
//将LayoutParams设置到UI控件上
header.setLayoutParams(param);
//设置ImageView宽高都填充父视图
header.setScaleType(ScaleType.FIT_XY);
header.setImageResource(R.drawable.food);
listView.addHeaderView(header);
/**
* 通过ListView.addFooterView方法可以给ListView添加一个底部视图View
*/
View footer = getLayoutInflater().inflate(R.layout.footer_loading, null);
listView.addFooterView(footer);
/**
* ListView的分页实现
* 通过setOnScrollListener可以给ListView设置滑动监听
* 1 滑动状态的改变
* 2 滑动时,item的位置信息
* 通过此OnScrollListener可以判断出ListView是否已经滑动到屏幕底部,并且滑动停止
*/
listView.setOnScrollListener(new OnScrollListener() {
/**
* 当ListView的滑动状态发送改变时,此方法被调用
* @param view 指ListView本身
* @param scrollState 表示当前ListView所处于的状态
* SCROLL_STATE_FLING--2 抛掷状态--手指离开屏幕前,用力滑了一下,屏幕产生惯性滑动
* SCROLL_STATE_IDLE--0 停止状态
* SCROLL_STATE_TOUCH_SCROLL--1 手指触摸屏幕触发ListView滚动状态
*/
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
android.util.Log.e("TAG", "onScrollStateChanged");
// 说明ListView已经滑动到底部,并且停止状态
if (isLastShown && scrollState == SCROLL_STATE_IDLE) {
int dataSize = data.size();
for(int i = dataSize; i < dataSize + 20; i++) {
data.add("Andorid-" + i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 通知适配器数据源已经发生改变
adapter.notifyDataSetChanged();
}
}
/**
* 只要ListView处于滑动状态,此方法会被一直调用
* @param view 指ListView本身
* @param firstVisibleItem 屏幕当前第一个可见Item的位置,从0开始
* @param visibleItemCount 屏幕当前可见item的个数
* @param totalItemCount ListView中总的item的个数
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
android.util.Log.e("TAG", "onScroll");
// 通过方法参数可以判断出最后一条是否已经显示到屏幕上
isLastShown = firstVisibleItem + visibleItemCount == totalItemCount;
}
});
}
}
布局文件:
<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="${relativePackage}.${activityClass}" >
<ListView
android:id="@+id/list_Main"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</RelativeLayout>
底部视图布局文件:footer_loading.xml
<?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="match_parent"
android:gravity="center"
android:orientation="horizontal" >
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:text="Loading..." />
</LinearLayout>