Android AbsListView坐标体系解析
Android的AbsListView与Android ListView不同,AbsListView代表了一个抽象的列表View。在实际的开发中直接使用Android ListView几乎可以完全完成所有与List这类View相关的开发任务,但在极个别情况下, 需要深入到Android的AbsListView中进行仔细的坐标定位。
为了探究Android的AbsListView,先写一个简单的ListView这样的代码:
package zhangphil.listview;
import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
public class MainActivity extends ListActivity {
private ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
// 测试数据源
String[] data = new String[50];
for (int i = 0; i < data.length; i++) {
data[i] = "child view:" + i;
}
ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item, R.id.textView, data);
this.setListAdapter(adapter);
listView = this.getListView();
// 设置ListView灰色分割线的高度,单位是pix像素
// this.getListView().setDividerHeight(20);
listView.setOnScrollListener(new OnScrollListener() {
private int firstVisibleItem;
private int totalItemCount;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
this.firstVisibleItem = firstVisibleItem;
this.totalItemCount = totalItemCount;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
Log.d("ListView",
"ListView: getTop():" + listView.getTop() + " , getBottom():" + listView.getBottom()
+ " , getY()" + listView.getY() + " , Height:" + listView.getHeight());
int cnt = view.getChildCount();
for (int i = 0; i < cnt; i++) {
View v = view.getChildAt(i);
// 为了便于分析结果,把child
// view的position和初始化的那些数据源一一对应起来:i+firstVisibleItem
Log.d("child view:" + (i + firstVisibleItem),
"getTop():" + v.getTop() + " , getBottom():" + v.getBottom() + " , getY():" + v.getY());
}
if (firstVisibleItem == 0 && isTop(view)) {
Toast.makeText(getApplicationContext(), "完全见顶!", Toast.LENGTH_SHORT).show();
}
if (listView.getLastVisiblePosition() == (totalItemCount - 1) && isBottom(view)) {
Toast.makeText(getApplicationContext(), "完全见底!", Toast.LENGTH_SHORT).show();
}
}
}
});
}
private boolean isTop(AbsListView view) {
View v = view.getChildAt(0);
return v.getTop() == 0;
}
private boolean isBottom(AbsListView view) {
int cnt = view.getChildCount();
View v = view.getChildAt(cnt - 1);
return v.getBottom() == listView.getBottom();
}
}
特别的,把Android ListView需要加载到adapter中的item设置成高度为100pix的子view:
<?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="100px"
android:orientation="vertical" >
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
</LinearLayout>
给这个ListView设置setOnScrollListener(new OnScrollListener(){})监听事件,制造实验结果,代码跑起来后滚动然后让ListView见顶后的图(1):
这是代码跑起来后,任意滑动该Listview但最终下滑见顶时候的logcat输出结果(1):
AbsListView与ListView不同,AbsListView代表当前屏幕视野可见范围内从上向下的“一组”view集合。一个ListView理论上讲可以拥有成千上万个子item,但是AbsListView所拥有,仅仅是当前屏幕可见视野范围内从上往下的一组子view集合,从某种角度上讲,AbsListView可以认为是ListView的某一段View子集。
假设Android的listview有n个子item。ListView从position=0开始,直到最后一个子item元素position=n-1结束,但是AbsListView则始终保持当前可见视野范围内的11个子item元素(注意:这在不同的设备结果不同,因为不同的设备屏幕高度不同,所以计算并加载相应个子item。数量到底是多少可以从AbsListView的getChildCount()获得,事实上也完全可以根据屏幕宽度自己手动计算出来)。
图(1)中的11个元素是AbsListView拥有的全部子元素集合,可以从AbsListView的getChildAt(int index)遍历出来每一个子集合。
在前面,故意给ListView的子item设置成100pix高度,Logcat输出结果(1)可以看到,ListView的高度是1038(pix),Listview的getTop()返回的数值和getY()坐标值相同。由于在本例中我故意把子Listview的item设置成100pix,那么第0个子item,在y坐标轴上的占据的高度是0到100pix,position=2的第二个子item是从102pix开始,为何是从102pix开始?因为ListView的默认的灰色分割线要用去1pix的高度。
为了让Logcat输出的结果和listView的position一一对应起来便于分析输出结果,每次在遍历AbsListView的子view时候,在Logcat的tag字段位置以firstVisibleItem为基数,这样就完全和ListView的adapter中的position对应起来。事实上,从一定意义上讲,在OnScrollListener里面onScroll回调得到的firstVisibleItem虽然是ListView中适配器中position,但它就是AbsListView的第一个子元素,visibleItemCount就是AbsListView所拥有的子元素总数,visibleItemCount和AbsListView的getChildCount()相等。
Logcat输出的结果(1)最后一个结果很有趣:
child view 10的getTop返回1020意为从屏幕的1020pix开始,但为何getBottom得到的是1120?要知道,整个listView才不过1038pix的像素高度!为何child view 10竟然超出整个ListView的高度!?
这正是AbsListView特殊的地方,AbsListView是抽象的,在AbsListView看来,child view 10虽然没有完全显示在屏幕上(因为屏幕高度总是有限,不可能无限高容纳所有的子元素),但它依然会被归属到AbsListView中,child view 10从1020pix开始,到AbsListView虚拟抽象出来的1120坐标位置结束。child view 10整体没有显示出来,但child view 10只要有一丁点儿显示在屏幕上,AbsListView就会把它作为子view,此时的child view 10底部被抽象、虚拟的认为跨出ListView的坐标系而存在。(1120-1020=100刚好就是我在布局文件写死的item高度的100pix)
再看一个实验,如图(2):
故意把child view 1不完全显示、遮掩住一部分。
此时Logcat输出结果(2):
注意看第一个输出结果:
child view 1的getTop()也即Y坐标轴上的值竟然是负值!这是AbsListView的坐标体系模型。在AbsListView看来,此时的child view 1被滚出了ListView,但child view 1仍然有一部分显示在屏幕中(71pix高度的部分),而另外一部分(29pix)被滚出ListView而不可见,但AbsListView仍然抽象的认为child view 1依然存在在自己的集合中,要凑足该子item view的高度( 刚好就是我在item布局文件中写死的71-(-29)=100pix ),只是一部分不可见了,不可见的部分由于是头部处于ListView的顶部不可见,那么给其坐标Y赋予负值(-29)以示区别。
由上可知AbsListView会自始至终加载一定数量(假设m)的子item,这些段m个子元素,是ListView全部n个子item顺序中的某一小段。m <= n。
AbsListView将最顶部滚出ListView可见区域的部分子item的Y坐标值赋予负值(虚拟的、抽象的),而在最底部不可见的子item那部分顺次迭加坐标值(虚拟的、抽象的)。这样最顶和最低都能凑成完整的item高度。
意义:明白了AbsListView的虚拟、抽象坐标体系后,其中一个意义就是利用这一点,判断一个ListView是否彻底的由于向下滚动而见顶,以及是否彻底向上滚动测底见底。这在一些常见的下拉、上拉刷新ListView中非常有用。
在扩展ListView功能添加下拉上拉刷新事件时,如何判断一个ListView是否彻底已经见顶或者见底,依靠一些常规的手段比较难解决。如果引入了AbsListView,就把问题的解决变得容易了。
具体结合本文例子加以说明。本文例子中有50个子item,初始化后即可任意滚动。如果换作其他情况更复杂,情况将变化(比如初始化状态无数据或者只有一两个子item根本没铺满ListView),但基本原理相同一致。
(1) 判断ListView滚动到最顶部。
首先在ListView的OnScrollListener里面取出firstVisibleItem是否等于0,如果等于0,那么表示此时的ListView的顶部可能见顶了(为什么说可能呢?因为只要ListView的第0条item只要出现在ListView的最顶部,OnScrollListener就将firstVisibleItem赋值0,无法判断firstVisibleItem到底是全部还是部分出现在ListView最顶部)。在本例中,ListView第0条子item在滚动状态中进入最顶部只有一种情况:从超出ListView顶部的部分渐渐滚入,也即getTop()的值逐渐从负值变成0。ListView的第一个item在完全贴合ListView最顶部的时候其getTop()也就是Y坐标值是0。
接着,此时判断firstVisibleItem的getTop()是否等于0,如果等于0,那么就可以认为此时的ListView最顶部的firstVisibleItem与屏幕的最顶部无缝贴合在一起了,此时可以启动下拉见顶加载更多这样的事件处理业务逻辑。
(2) 判断ListView滚动到最底部。
当ListView最后最末尾一个item完全贴合ListView时候,此时,该item的getBottom()也就是Y坐标轴刚好就是ListView的Y坐标值或者高度值(ListView的getBottom()或者ListView的getHeight(),注意:getHeight()在此处作为判断条件要小心使用,假设一个ListView只有几个item而没有铺满整个布局,但ListView的高度是match_parent,那么此时就要出问题)。ListView本身的设计使得不管如何ListView最后一个item总能在贴合ListView的底部紧密咬合在一起。
附录一些我写的相关文章,均在我的csdn博客中:
1、《Android判断ListView滚动到最顶部第0条item完全完整可见及最底部最后一条item完全完整可见》链接地址:http://blog.csdn.net/zhangphil/article/details/50329601
2、《Android ListView下拉/上拉刷新:设计原理与实现》链接地址:http://blog.csdn.net/zhangphil/article/details/47036177
3、《Android View滚动、拉伸到顶/底部弹性回弹复位》链接地址:http://blog.csdn.net/zhangphil/article/details/47333845
4,《Android ListView拉到顶/底部,像橡皮筋一样弹性回弹复位》链接地址:http://blog.csdn.net/zhangphil/article/details/47311155