我们之前讨论了ListView的基本使用方法和ListView的优化
今天我们再来讨论一个关于ListView的一个新的东西~就是分页加载。那么什么是分页加载呢?简单点说,就是“下拉刷新”。
我们来简单的构思一下我们都需要些什么东西。
首先,我们需要主界面布局,并且在布局文件中写一个ListView。
然后,我们需要ListView中每个item的布局
然后,我们还需要一个当页面加载的时候出现的那个旋转的小圆圈(ProgressBar)的布局,我们可以将其称之为“底部布局”(自己起的名字)。
然后,我们需要一个Activity,在其中我们需要设置数据源和一系列操作。
然后,我们需要一个类,该类代表每个item,存储的是每个item中的内容
接下来,我们来按照我们上面的简单的构思,来做一个具体点的构思吧
主界面布局文件我就不用多说了,就是里面添加一个ListView
然后,我们来考虑item中的布局,我们想显示一个头像,一个名字,一个内容。我们就定义一个ImageView、一个TextView(管名字)、一个TextView(管内容)
然后,我们需要在底部布局中,添加一个ProgressBar (就是旋转小圆圈,又叫圆形进度条)和一个TextView(用来写“正在加载~”字样)。
然后,我们需要一个类,代表item中的内容,由于我们上面已经说了,我们想显示一个头像,一个名字,一个内容。 那么,我们在该类中就需要维护一个int类型的icon(图片是int类型的),一个String类型的title(名字/标题),一个String类型的content(内容)
然后,我们需要Acitivty,在Activity中,我们需要定义数据源,和一个适配器,还有一个存储对象的list集合。并且让Activity实现OnScrollListener接口,并实现其中的两个方法 1.onScroll方法 2.onScrollStateChanged方法
我们根据上面的构思,来做具体的实现:
我们先来看我们的主布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" 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="application.smile.listview.MainActivity"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
然后,我们来看每个item中的布局:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp"> <ImageView android:id="@+id/iv_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:src="@mipmap/ic_launcher"/> <TextView android:id="@+id/tv_title" android:layout_marginTop="10dp" android:layout_toRightOf="@+id/iv_icon" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是标题" android:textSize="20sp"/> <TextView android:id="@+id/tv_content" android:layout_toRightOf="@+id/iv_icon" android:layout_below="@+id/tv_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:text="这是内容"/> </RelativeLayout>
就是这样的效果:
接下来,我们来看我们底部布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="拼命加载中"/> </LinearLayout>
看到的效果就是这样的:
这样,我们的布局文件就到这了。
接下来,我们先来看创建的item的类
public class ItemContent { int icon; String title ; String content; }
然后,我们再来看Activity中的实现:
这里是最重要的了……因为所有的实现都在Activity中,我们还是有必要再细一些的讲解
首先,我们需要实现接口,然后设置数据源(就是图片和名字),创建一个list集合用来存放刚刚创建的类的对象(就是ItemContent),再维护一个适配器
然后,我们在onCreate(...)方法中去执行我们的老套路,找到控件、然后更新数据、然后设置适配器,然后设置点击事件(这里有两个事件,一个是滚动事件,一个是点击事件),这里还要加一个,就是得到我们的底部布局,并加入到ListView中。
然后,我们在更新数据的方法中去设置每次加载多少条数据。就是通过一个循环来控制每次加载多少条数据,每次都创建一个ItemContent(创建的item类)的对象,并为其赋值,最后添加到List集合中,这里需要做一个操作,因为我们是模拟,所以没有很多的数据,那么我们就需要让我们现有的数据来循环使用,我们需要一个控制变量(相当于一个指针),当这个控制变量的大小和数据数组的长度一致的时候,就将控制变量置零,让我们从数据数组的开始继续读取,从而实现数据的循环使用。
然后我们来看我们重写的两个方法中的onScroll方法:
该方法用于监听屏幕滚动
该方法中有四个参数:
第一个参数:AbsListView类型的view 该参数是正在滚动的视图
第二个参数:int类型的firstVisibleItem 该参数是第一个可见的item的索引
第三个参数:int类型的visibleItemCount 该参数是可见的item的数量
第四个参数:int类型的totalItemCount 该参数是列表适配器中的项目数(我们这里用不到)
然后,我们来看这个方法中,我们需要做些什么:
我们需要定义一个int类型的变量,用来标记我们能看见的最后一个item的索引 ,然后我们通过<“第一个可见的item的索引” + “可见的item的数量” -1> 来获得我们想要的最后一个item的索引。
这里为什么要-1呢?我们来仔细的考虑一下。所谓索引也就是下标,我们知道下标是从0开始的,如果我们不-1 ,我们就会多得到一条数据,每次就不是10个了,而是11个。所以,我们需要-1 来控制,我们每次都拿到10条数据
这样,我们这个方法就完成了。
然后,我们来看重写的两个方法中的onScrollStateChanged方法:
该方法用于监听屏幕滚动的状态
该方法中有两个参数:
第一个参数:AbsListView类型的view 该参数是正在滚动的视图
第二个参数:int类型的scrollState 该参数是当前滚动的状态
该参数,可以有三个值与之匹配:
1.SCROLL_STATE_FLING 代表:用户已经使用触摸来滚动,手指已经离开屏幕,但是视图还在惯性的滚动
2.SCROLL_STATE_IDLE 代表:视图不滚动,并且手指松开
3.SCROLL_STATE_TOUCH_SCROLL 代表:用户正在使用触摸来滚动,并且手指还在屏幕上
然后,我们来看这个方法中,我们需要做些什么:
我们需要通过线程来模拟加载的时间,但是在此之前,我们需要先判断“滚动的状态” 是不是处于“视图不再滚动,并且手指松开的状态”并且判断能看到的最后一条item的索引是不是适配器的长度
如果这两个条件都满足,证明已经滚动到了最下面一条,需要更新数据。
于是我们开启一个线程并且睡上1秒,睡完要更新数据,并且显示到ListView上,但是在安卓中想要更新UI就必须在UI线程中就行,不可以在子线程中进行,那么我们想要个更新UI就用上了Handler 这个类。
我们要在成员位置创建一个Handler对象,并重写handleMessage方法,在方法中做更新UI的操作,那么我们如何让子线程与HandleMessage方法链接上呢?我们需要在子线程中通过Handler对象,去发送一个一个消息,并附上一个标志
然后,我们在HandleMessage中去判断这个标志,如果成功就更新UI
接下来,我们就来看我们自定义的适配器。还是之前的套路,继承BaseAdapter,并重写方法
再重写方法之前,我们需要维护一个布局填充器,并在构造方法中给他赋值
然后我们来看重写的方法:
第一个方法:getCount方法 这个需要返回总共有多少个item,这里就是list集合的size
第二个方法:getItem方法 这个就是返回每个item,这里就是通过list集合的get方法获得item并返回
第三个方法:getItemId方法 这个就是返回每个item的id,这里就是直接返回position
第四个方法:getView方法 这个方法中就是具体的情况了
首先创建一个类取名为ViewHolder并在其中声明item中的三个控件
然后,在getView方法中维护一个ViewHolder
然后,判断convetView是否为空,如果为空就通过布局填充器获得item的布局。
并实例化ViewHolder,通过convertView找到控件并赋值给ViewHolder的相应的控件。
然后设置一个标记,把ViewHolder放进去。
如果不为空,就通过标记取出ViewHolder,并给每个空间设置数据源。
最后,返回convertView
然后,我们来上代码:
public class MainActivity extends AppCompatActivity implements AbsListView.OnScrollListener { //用来判断点击的是不是“正在加载”的item,true代表是,false代表不是 private boolean flag = false; //用于Handler返回的标志 private static final int SEND_OK = 0X1; //存储文字的数组 private String[] names = {"郭嘉", "黄月英", "华佗", "刘备", "陆逊", "吕布", "吕蒙", "马超", "司马懿", "孙权", "孙尚香", "夏侯惇", "许褚", "杨修", "张飞", "赵云", "甄姬", "周瑜", "诸葛亮"}; private List<ItemContent> project = new ArrayList<>(); //存储图片的数组 private int[] images = {R.mipmap.guojia, R.mipmap.huangyueying, R.mipmap.huatuo, R.mipmap.liubei, R.mipmap.luxun, R.mipmap.lvbu, R.mipmap.lvmeng, R.mipmap.machao, R.mipmap.simayi, R.mipmap.sunquan, R.mipmap.sunshangxiang, R.mipmap.xiahoudun, R.mipmap.xuchu, R.mipmap.yangxiu, R.mipmap.zhangfei, R.mipmap.zhaoyun, R.mipmap.zhenji, R.mipmap.zhouyu, R.mipmap.zhugeliang}; private MyAdapter myAdapter; //创建一个Handler对象 private Handler handler = new Handler() { //当子线程调用sendMessage方法时会自动调用该方法 @Override public void handleMessage(Message msg) { super.handleMessage(msg); //根据标志做相应的操作 switch (msg.what) { case SEND_OK: //更新ui myAdapter.notifyDataSetChanged(); //将“正在加载”的标志,设置为没在加载 flag = false; break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //找到控件 ListView listView = (ListView) findViewById(R.id.listView); //更新数据 initData(); //找到底部布局 View bottom_layout = getLayoutInflater().inflate(R.layout.bottom_layout, null); //设置到listView上 listView.addFooterView(bottom_layout); //设置适配器 myAdapter = new MyAdapter(this); listView.setAdapter(myAdapter); listView.setOnScrollListener(this); //设置点击事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //弹出吐司 if (!flag) { //如果点击的不是“正在加载”就弹出这个吐司 Toast.makeText(MainActivity.this, "点我了?我是" + names[position % 19], Toast.LENGTH_SHORT).show(); } else { //如果点击的是“正在加载”就弹出这个吐司 Toast.makeText(MainActivity.this, "加载中....", Toast.LENGTH_SHORT).show(); } } }); } //控制数据循环的控制变量 int count = 0; public void initData() { //通过循环控制每次获取10条数据 for (int i = 0; i < 10; i++) { //实例化一个item的类的对象 ItemContent item = new ItemContent(); //判断,如果控制变量的长度大于或者等于数据数组的长度,就置零 if (count >= images.length) { count = 0; } //赋值 item.icon = images[count]; item.title = names[count]; item.content = "我是" + names[count]; //添加到list集合 project.add(item); //控制变量自增 count++; } } /** * 用于监听ListView滚动状态的变化 * * @param view 正在报告滚动状态的视图 * @param scrollState 当前的滚动状态 */ @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (!flag) { /** *SCROLL_STATE_FLING: 用户已经使用触摸来滚动,手指已经离开屏幕,但是视图还在惯性的滚动 *SCROLL_STATE_IDLE: 视图不滚动,并且手指松开 * SCROLL_STATE_TOUCH_SCROLL: 用户正在使用触摸来滚动,并且他们的手指仍在屏幕上 */ if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && visibleLastItem == myAdapter.getCount()) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } initData(); handler.sendEmptyMessage(SEND_OK); } }).start(); flag = true; } } } private int visibleLastItem = 0; //能看到的最后一个单元格的索引 /** * 用于监听ListView屏幕滚动 * * @param view 正在报告滚动状态的视图 * @param firstVisibleItem 第一个可见单元格的索引 * @param visibleItemCount 可见单元格的数量 * @param totalItemCount 列表适配器中的项目数 */ @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { //最后能看到的视图索引 = 第一个可以看见的单元格索引 + 可见单元格的数量 - 1; //因为是从0开始的,但是数量是从1开始的,所以 需要减一才可以做索引 visibleLastItem = firstVisibleItem + visibleItemCount -1; } /** * 自定义适配器,继承BaseAdapter */ class MyAdapter extends BaseAdapter { //维护一个布局填充器,为了得到每个item 的布局 private LayoutInflater layoutInflater; public MyAdapter(Context context) { //通过构造方法获取布局填充器对象 layoutInflater = LayoutInflater.from(context); } //该方法是总共有多少个item @Override public int getCount() { return project.size(); } //该方法是得到每个item的值 @Override public Object getItem(int position) { return project.get(position); } //该方法是得到每个item的id @Override public long getItemId(int position) { return position; } //该方法是获得视图,也是这些里面最重要的方法 @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if (convertView == null) { //首先,我们通过布局填充器获得item的布局 convertView = layoutInflater.inflate(R.layout.item_layout, null); //实例化ViewHolder viewHolder = new ViewHolder(); //根据item的布局找到图片 viewHolder.iv_icon = (ImageView) convertView.findViewById(iv_icon); //根据item的布局找到标题 viewHolder.tv_title = (TextView) convertView.findViewById(tv_title); //根据item的布局找到内容 viewHolder.tv_content = (TextView) convertView.findViewById(tv_content); //设置tag标记 convertView.setTag(viewHolder); } viewHolder = (ViewHolder) convertView.getTag(); //设置图片源 viewHolder.iv_icon.setImageResource(project.get(position).icon); //设置标题文字 viewHolder.tv_title.setText(project.get(position).title); //设置内容文字 viewHolder.tv_content.setText(project.get(position).content); //返回item的布局 return convertView; } class ViewHolder { ImageView iv_icon; TextView tv_title; TextView tv_content; } } }
接下来,我们来看一下运行的结果吧~
这样,我们的ListView的分页加载就完成了
让程序写入生命,将代码融入灵魂
-------smile、zj