处女男学Android(六)---ListView上拉加载更多模拟获取分页数据



前言



转载请标明出处: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很费时间,尤其是处女座的人,经常会为了一个小效果或者字体样式折腾很久,不过我还是会坚持下去的,为了自己能学到更多,也为了她而努力。




  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值