前言
转载请标明出处:http://blog.csdn.net/wlwlwlwl015/article/details/40145073
从本篇blog开始将陆续记录Android UI相关的内容,ListView算是最常用的UI控件之一了,所以我选择从ListView开始学习,本篇主要记录了ListView的常用API、上拉加载更多以及模拟分页的Demo。
一、ListView简介
首先看一下ListView的父类及其继承关系:
不难发现,ListView的父类是AbsListView,AbsListView是一个抽象类,它正是AdapterView派生的子类,AdapterView是一组重要的组件,它有以下特征:
1.AdapterView继承自ViewGroup,所以它的本质也是容器。
2.AdapterView可以包括多个列表项,并将多个列表项以合适的形式展示出来。
3.AdapterView显示的列表项由Adapter(适配器)提供。
也就是说,一个ListView需要一个Adapter为其提供数据,Adapter也是一个接口,下面看看Adapter及其实现类:
上图中标出了常用的Adapter类,几乎大多数Adapter都继承了BaseAdapter,而BaseAdapter同时实现了ListAdapter和SpinnerAdapter两个接口,所以BaseAdapter及其子类可以同时为AbsListvView和AbsSpinner提供列表项。下面对这四个Adapter的实现做一个简介:
1.ArrayAdapter:简单、易用的Adapter,通常用于将数据或List集合的多个值包装成列表项。
2.SimpleAdapter:功能强大的Adapter,可以在列表项中显示图片等复杂View,通常同上。
3.SimpleCursorAdapter:同上类似,只是用于包装Cursor提供的数据。
4.BaseAdapter:通常用于被扩展。扩展BaseAdapter可以对各列表项进行最大限度的定制。
基本的概念已经介绍完毕,下面通过ListView+ArrayAdapter的简单示例来看看ListView的用法。
二、一个简单的ListView实例
功能很简单,即通过ListView显示一组数据。
Layout代码(activity_main.xml):
<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" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.listviewtest.MainActivity" > <ListView android:id="@+id/listView1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:divider="#f00" //设置List列表项的分隔条,即可用颜色,也可用Drawable android:dividerHeight="2px" //设置分隔条的高度 > </ListView> </RelativeLayout>
Activity代码:
package com.example.listviewtest; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; public class SecondActivity extends Activity { private ListView listView; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView1); //构造数据 List<String> datas = new ArrayList<String>(); for (int i = 0; i < 20; i++) { datas.add("tom" + i); } //构造Adapter ArrayAdapter<String> adapter = new ArrayAdapter<String>( SecondActivity.this, android.R.layout.simple_list_item_1, datas); listView.setAdapter(adapter); } }
运行之后可以看到一个简单的列表效果:
ListView的应用非常广泛,百度贴吧的列表、新浪微博的列表等等都是基于ListView实现的,像这种列表的数据肯定都是从服务端获取的,而且一次必定是获取固定的条数,用户通过上拉滑动浏览数据,当浏览完取到的数据时则会再次给服务端发送请求继续获取数据,那么这里必定要用到分页的技术了,下面就具体谈谈分页本身的实现机制以及结合ListView的实现方式。
三、模拟分页
分页是每个项目都必定会用到的技术,不管是web项目,或是app项目,只要有展示数据的需求,那么一般都需要通过分页去获取并展示数据,对于新手来说这通常也是一个难点,下面通过一个最简单的分页Demo来谈谈分页的思想和原理。
归根结底,分页无非就是:传入“页码”和“每页需要显示的记录数”作为参数,返回指定页码的数据集。
比如,我规定一页显示5条数据,我要第一页,那就是1~5条,第二页就应该是5~10条,依次类推。鉴于这种需求大多数关系型数据库都提供了分页查询的支持,MySQL是通过LIMIT关键字分页,SQLServer是通过TOP关键字分页,Oracle则是通过ROWNUMBER去实现分页的,像分页这种较为通用的功能一般我们都会进行封装,针对部分细节做一些适当的容错处理,这样才更加易于使用,下面获取List集合中的数据来模拟一个分页功能。
Pager.java:
package com.xw.util; /** * 分页的工具类,需要以下参数: * parameter 1 --> pageNum 需要访问的页码(第几页) * parameter 2 --> pageSize每页的大小,即每页最大记录数 * parameter 3 --> totalSize 总记录数 * * @author 王亮 * */ public class PagerUtil { private int pageNum; // 当前页码 private int pageSize; // 每页最大记录数 private int totalCount; // 总记录数 private int pageCount; // 总页数 public PagerUtil(int pageNum, int pageSize, int totalCount) { this.pageSize = pageSize; this.totalCount = totalCount; setPageNum(pageNum); } /** * 设置当前页码 * * @param pageNum */ public void setPageNum(int pageNum) { int activePage = pageNum <= 0 ? 1 : pageNum; activePage = activePage > getPageCount() ? getPageCount() : activePage; this.pageNum = activePage; } /** * 获得总页数 * * @return */ public int getPageCount() { pageCount = totalCount / pageSize; int mod = totalCount % pageSize; if (mod != 0) { pageCount++; } return pageCount == 0 ? 1 : pageCount; } /** * 得到第pageNum页的第一条数据的索引号 * * @return */ public int getFromIndex() { return (pageNum - 1) * pageSize; } /** * 得到第pageNum页的最后一条数据的索引号 * * @return */ public int getToIndex() { return pageNum * pageSize; } public int getPageNum() { return pageNum; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalCount() { return totalCount; } public void setTotalCount(int totalCount) { this.totalCount = totalCount; } }
Test.java:
package com.xw.test; import java.util.ArrayList; import java.util.List; import com.xw.util.PagerUtil; public class Test { public static void main(String[] args) { Test.tesePager(1, 5); } public static void tesePager(int pageNum, int pageSize) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 200; i++) { list.add("jack" + i); } PagerUtil pager = new PagerUtil(pageNum, pageSize, list.size()); int start = pager.getFromIndex(); // 得到指定页码第一条数据的索引 int end = pager.getToIndex(); // 得到指定页码最后一条数据的索引 List<String> subList = list.subList(start, end); // 截取List for (int i = 0; i < subList.size(); i++) { System.out.print(subList.get(i) + "---"); } } }
PagerUtil类中封装了两个方法,getFromIndex()和getToIndex(),这两个方法正是通过页码和每页记录数来计算出总数据源中的第start条到第end条数据,刚好通过List的subList方法来截取从而实现分页的功能。运行多次之后可以看到:
总共运行了5次,每一页显示5条数据,可以看出正常的显示了第1~5页的数据。现在对基本的分页原理和思想应该比较清楚了,最后我们结合ListView的监听器来实现上拉加载更多分页数据。
四、结合ListView实现上拉加载更多
上面我们已经了解了ListView的基本用法和分页技术,那么下面我们就来具体分析一下上拉加载如何实现。
首先,我们需要明白是什么时候加载?那一定是数据显示完了才需要加载更多。那怎么判断数据显示完了?那必定会用过一种监听器来监听这种情况。我们在ListView的父类AbsListView中可以看到有这样一个回调接口:
根据红色方框的描述我们不难发现,正是通过这个监听器来监听ListView的滚动行为。可以看到这个接口定义了两个抽象方法,我们给ListView绑定监听必定要重写这两个抽象方法,我们可以先打印这些参数看看它们都代表什么,我们在上面的SecondActivity中继续添加以下代码:
listView.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //System.out.println("scrollState--->" + scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { System.out.println("firstVisibleItem--->" + firstVisibleItem); System.out.println("visibleItemCount--->" + visibleItemCount); System.out.println("totalItemCount--->" + totalItemCount); } });
运行之后观察LogCat控制台可以看到:
仔细观察一下,当点击鼠标的时候(屏幕发生细微滚动)就会调用onScroll方法,打印出来的参数分别是:
firstVisibleItem--->0
visibleItemCount--->9
totalItemCount--->20
再对照一下官方文档中这三个参数的解释:
简单翻译一下,firstVisibleItem表示第一个可见行的索引,visibleItemCount表示可见行的数量,totalItemCount表示adapter中的数据个数。上面的0,9,20也就是说:
firstVisibleItem--->0 表示当前屏幕中第一个可见行的索引是0,即tom0
visibleItemCount--->9 表示当前屏幕中可见的行数是9
totalItemCount--->20 表示构建这个ListView的Adapter的数据量是20条
随着屏幕滑动我们可以发现这三个参数在不断的变化,但不难发现totalItemCount永远不会变,因为这个数字是ListView的Adapter决定的,而visibleItemCount在9和10之间徘徊,其实这个参数也是不变的,之所以变化是因为滑动的过程中有时屏幕上下各显示半行,加起来也就当成一行了。变化的始终是firstVisibleItem,可以发现随着向下滑动,firstVisibleItem逐渐增加,滑到最底端时firstVisibleItem是11,而visibleItemCount是9,totalItemCount始终不变是20。细心的话应该可以发现这个规律,当屏幕滑动到最底端时,firstVisibleItem+visibleItemCount=totalItemCount 。这也就是我们上拉加载的条件之一,当数据显示完时,需要加载,这个等式成立,也就可以说明ListView的数据加载完了。
上面说了这是条件之一,比如这种情况,当用户手指没有离开屏幕滑下来再滑回去,有时用户不一定希望加载更多数据,所以还需要有一个状态判断滑动已经停止,这样才能确切表示用户需要加载数据了。(其实这一点也不是必须的,可以根据APP的需求来定,新浪微博的动态List就没有管这个状态),我们在上面的截图也可以清楚的看到在onScrollStateChanged这个方法中有一个参数scrollState,这个参数也就是OnScrollListener定义的三种状态之一,下面通过实验看看这三种状态分别表示怎样的滑动状态。代码不变,注释onScroll中的打印语句,放开onScrollStateChanged中的打印语句,下面是运行情况:
请仔细观察我的触摸滑动的动作,程序运行之后,我开始模拟触摸滑动,鼠标按下并开始拖动,打印scrollState--->1,当我停止滑动并松开鼠标时,打印scrollState--->0,也就是说,这两个动作表示了“开始滑动”和“停止滑动”这两种状态。当滑动到屏幕最底端时,我模拟了“滑动并一扔”的动作,可以看到屏幕底端闪出一个蓝色阴影并消失,也就是执行这个动作的时候,我们可以看到打印出了scrollState--->2。下面看一下安卓源代码中定义的这三种滚动状态:
本人英语水平有限,粗略的翻译一下:
SCROLL_STATE_IDLE = 0 视图组件不再滚动。
SCROLL_STATE_TOUCH_SCROLL = 1 用户正在进行触摸滑动,并且手指始终在屏幕上没有离开。
SCROLL_STATE_FLING = 2 用户之前一直在触摸滑动并进行了“一抛”的动作,动画正在运行至停止。
前两种状态比较简单,0表示停止滑动,1表示正在滑动。而FLING这个状态通过翻译想必应该也已经清楚了,那个动画就是我们上面动态效果图的“蓝色阴影”的动画效果,这个FLING状态也正是位于“运行中”和“停止运行”之间的一个瞬时状态,也和我们测试的效果相吻合,打印的2始终位于1和0之间,呈1、2、0循环。
说了这么多,那该总结一下“上拉加载更多”的条件,除了上面的等式之外,应该再判断一下滑动状态是否停止,所以这个逻辑条件应当是这样:
(firstVisibleItem+visibleItemCount=totalItemCount) && scrollState == SCROLL_STATE_IDLE
解决了核心的问题,下面我们看看“上拉加载更多”完整的代码。数据是通过上面的分页Demo从服务端获取的,这里会用到一个简单的网络工具类(HttpUtils)和一个Json解析的工具类(JsonUtils),还有部分需要注意的关键细节会在后面解释,下面上代码,是在上面的Activity基础之上修改的:
package com.example.listviewtest; import java.util.ArrayList; import java.util.List; import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.ArrayAdapter; import android.widget.ListView; import com.xw.util.HttpUtils; import com.xw.util.JsonUtils; public class MainActivity extends ActionBarActivity { private ListView listView; private ProgressDialog dialog; private ArrayAdapter<String> adapter; private final String URI_STR = "http://192.168.1.126:8080/mec_1/testCityDatas4Pager.gxz?pageNum="; private boolean isPageDiv; private static int pageNo = 1; List<String> total = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView) findViewById(R.id.listView1); dialog = new ProgressDialog(this); dialog.setTitle("提示信息"); dialog.setMessage("Loading......"); adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1); // 取第一页数据 new MyTask().execute(URI_STR + 1); listView.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { System.out.println("scrollState-->" + scrollState); if (isPageDiv && scrollState == OnScrollListener.SCROLL_STATE_IDLE) { // 开启异步任务获取下一页数据 new MyTask().execute(URI_STR + pageNo); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { System.out.println("firstVisibleItem--->" + firstVisibleItem); System.out.println("visibleItemCount--->" + visibleItemCount); System.out.println("totalItemCount--->" + totalItemCount); // 满足本条件即可说明已经滑到最后一行,需要加载更多数据 isPageDiv = (firstVisibleItem + visibleItemCount == totalItemCount); } }); } class MyTask extends AsyncTask<String, Void, List<String>> { @Override protected void onPreExecute() { // TODO Auto-generated method stub super.onPreExecute(); dialog.show(); } @Override protected List<String> doInBackground(String... params) { // 获取服务端数据 String result = HttpUtils.sendPostMethod(params[0], "utf-8"); // 解析服务端数据 List<String> list = JsonUtils.parseJsonDatas(result); return list; } @Override protected void onPostExecute(List<String> result) { // TODO Auto-generated method stub super.onPostExecute(result); adapter.addAll(result); if (pageNo == 1) { listView.setAdapter(adapter); } adapter.notifyDataSetChanged(); pageNo++; dialog.dismiss(); } } }
上面的例子有几点需要注意的地方:
1.声明一个全局的Adapter并且只能初始化一次,数据是不断的Add进去的,就像我们上拉加载新数据,而旧的数据也要保留。
2.通过网络访问服务端的时候,最好设置一个ProgressDialog进行等待提示,这样会给用户带来不错的体验。还有访问网络的时候需要INTERNET权限,在AndroidManifest.xml中添加配置:
<uses-permission android:name="android.permission.INTERNET" />
3.ListView只能设置一次Adapter,也就是加载第一页数据的时候[if(pageo==1)],后续加载更多数据则会通过adapter.notifyDataSetChanged()去动态更新adapter,如果多次setAdapter会影响性能,而且每次ListView都会回滚到列表顶端。
最后看一下运行效果:
五、总结
关于ListView第一篇的介绍到这里就结束了,主要记录了ListView的基本使用方法、分页的设计原理以及ListView上拉加载更多的模拟实现,代码写的不够完善的地方欢迎各位批评指正,作为一个Android菜鸟真心希望能尽快提高自己的开发水平。后续的Blog会继续记录Android UI方面的内容,虽然写Blog很费时间,尤其是处女座的人,经常会为了一个小效果或者字体样式折腾很久,不过我还是会坚持下去的,为了自己能学到更多,也为了她而努力。