之前分析了 事件分布 和 ListView复用机制 ,不能只分析不使用把,这次就用之前分析的知识来完成一个自定义列表控件。
首先看一下效果,结合了ListView的复用机制以及触摸事件的使用。
首先我们需要实现一个静态的页面效果。
他分为四部分,左上角是怎么滑动都不会动的,上和左各有一个首行只可以单向滑动,而蓝色部分是可以上下左右,甚至斜着都可以,而且在实现静态页面的同是我们利用学过的ListView源码里的逻辑可以实现只加载屏幕内显示的View,所以不论有多少数据,我们都不用担心内存问题。
首先我们看一下需要用到的变量都是干什么的
private BaseTableAdapter adapter;
private int downX;//滑动时手指落下的X Y
private int downY;
private int scrollX;//滑动的距离
private int scrollY;
private int firstRow;//当前第一行postiton
private int firstColumn; //当前第一列position
private int[] widths;//存放每个View的宽高
private int[] heights;
@SuppressWarnings("unused")
private View headView;//头View 为使用
private List rowViewList;//保存一行数据 因为在滑动是可能一行数据直接就滑上去了
private List columnViewList;
private List> bodyViewTable;//表格数据
private int rowCount;//行数
private int columnCount;//列数
private int width;//控件宽高
private int height;
private final ImageView[] shadows;//分割的黑线
private final int shadowSize;//黑线宽度
private int minimumVelocity;//惯性滑动时最小和最大速率
private int maximumVelocity;
private final Flinger flinger;//惯性滑动
private VelocityTracker velocityTracker;//惯性滑动
private boolean needRelayout; //需要重绘标志位
private int touchSlop; //滑动最小距离
private Recycler recycler;//复用相关类
接下来按一下onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int w;
final int h;
if (adapter != null) {
this.rowCount = adapter.getRowCount();//获取数据个数
this.columnCount = adapter.getColumnCount();
//
widths = new int[columnCount + 1];//初始化保存的数组 这里+1 是包括了有一个单向滑动的头部。
for (int i = -1; i < columnCount; i++) {//这里从-1开始是为了可以添加columnCount + 1条数据
widths[i + 1] += adapter.getWidth(i);
}
heights = new int[rowCount + 1];
for (int i = -1; i < rowCount; i++) {
heights[i + 1] += adapter.getHeight(i);
}
if (widthMode == MeasureSpec.AT_MOST) {//AT_MOST wrap_content
//sumArray方法是计算出数组的总和
w = Math.min(widthSize, sumArray(widths));//判读屏幕宽度和数据宽度,取最小的
} else if (widthMode == MeasureSpec.UNSPECIFIED) {
w = sumArray(widths);
} else {//具体指或match_parent
w = widthSize;
int sumArray = sumArray(widths);
if (sumArray < widthSize) {//如果 现有view的宽度小于 屏幕宽度 将会把屏幕宽度平分
final float factor = widthSize / (float) sumArray;
for (int i = 1; i < widths.length; i++) {
widths[i] = Math.round(widths[i] * factor);
}
widths[0] = widthSize - sumArray(widths, 1, widths.length - 1);
}
}
if (heightMode == MeasureSpec.AT_MOST) {
h = Math.min(heightSize, sumArray(heights));
} else if (heightMode == MeasureSpec.UNSPECIFIED) {
h = sumArray(heights);
} else {
h = heightSize;
}
} else {
if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
w = 0;
h = 0;
} else {
w = widthSize;
h = heightSize;
}
}
//必须调用
setMeasuredDimension(w, h);
}
通过onMeasure我们不仅适配了屏幕,而且还获取了每个View的宽高,这样任由我们摆放了,所以接下来看一下onLayout方法。
@SuppressLint("DrawAllocation")
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (needRelayout || changed) {
needRelayout = false;
resetTable();//情况所有的集合和View
if (adapter != null) {
width = r - l;//屏幕当前的宽度和高度
height = b - t;
//画那四条黑线,在实现静态页面是还没什么用
int left, top, right, bottom;
right = Math.min(width, sumArray(widths));
bottom = Math.min(height, sumArray(heights));
addShadow(shadows[0], widths[0], 0, widths[0] + shadowSize, bottom);
addShadow(shadows[1], 0, heights[0], right, heights[0] + shadowSize);
addShadow(shadows[2], right - shadowSize, 0, right, bottom);
addShadow(shadows[3], 0, bottom - shadowSize, right, bottom);
//画左上角那个固定的ViewItem(红色部分)
headView = makeAndSetup(-1, -1, 0, 0, widths[0], heights[0]);
//画除左上角以外的第一行数据(橘黄色部分)
left = widths[0] ;
//这里用到了源码里的机制,只加载屏幕以内的View
//当left(当前View的左边<屏幕的宽度才去加载)
for (int i = firstColumn; i < columnCount && left < width; i++) {
//不停的去找下个View的左右边的值
right = left + widths[i + 1];
final View view = makeAndSetup(-1, i, left, 0, right, heights[0]);
rowViewList.add(view);//保存第一行数据
left = right;
}
//画除左上角以外的第一列数据(棕色部分)
top = heights[0] ;
for (int i = firstRow; i < rowCount && top < height; i++) {
bottom = top + heights[i + 1];
final View view = makeAndSetup(i, -1, 0, top, widths[0], bottom);
columnViewList.add(view);
top = bottom;
}
//画Body部分(蓝色部分)
top = heights[0];
for (int i = firstRow; i < rowCount && top < height; i++) {
bottom = top + heights[i + 1];
left = widths[0] - scrollX;
List list = new ArrayList();
for (int j = firstColumn; j < columnCount && left < width; j++) {
right = left + widths[j + 1];
final View view = makeAndSetup(i, j, left, top, right, bottom);
list.add(view);//当前行 一个一个添加 最后相当于一行数据
left = right;
}
bodyViewTable.add(list);//添加一行数据 最后相当于 表格内所有数据
top = bottom;
}
shadowsVisibility();//分割的黑线
}
}
}
这里突出了静态时的一个关键点,就是只加载当前页面内的数据,优化效率非常明显。之后只要设置数据就可以正常显示了,具体看 源码,这里我们看一下优化的效率。首先我们改变一下代码
//画Body部分(蓝色部分)
for (int i = firstRow; i < rowCount ; i++) {
...
for (int j = firstColumn; j < columnCount ; j++) {
...
}
...
}
添加蓝色区域View的时候 我们把屏幕限制条件删除,并且我们隔八秒后添加1亿跳数据,我们看一下内存状况。
宝宝表示震精了~我们在看一下添加上限制条件后是什么情况。
//画Body部分(蓝色部分)
for (int i = firstRow; i < rowCount && top < height ; i++) {
...
for (int j = firstColumn; j < columnCount && left < width ; j++) {
...
}
...
}
效果很明显,在第8秒时内存只是增加了一点,将屏幕填满了,之后就再也没有变化,静态的效果我们已经达到了,之后就是滑动时对View的分离以及复用的操作。首先我们需要了解一下复用类。
public class Recycler {
private Stack[] views;
public Recycler(int type) {
views=new Stack[type];
for (int i = 0; i < type; i++) {
views[i]=new Stack();
}
}
public void addRecycledView(View view,int type){//滑动时就会调用
views[type].push(view);//根据ItemType添加View
}
public View getRecyclerView(int type){//添加View的时候调用(静态页面第一次添加View也会调用)
try {//一定要try catch 因为type第一次出现时可能还没有添加过
return views[type].pop();//根据ItemType拿到View
} catch (Exception e) {
return null;
}
}
}
这个类其实就是对View的一个Item滑出屏幕时需要添加到这个数组里,item出现是需要判断之前是否有缓存过。在添加View的时候就会起到非常大的优化作用。
然后就开始实现滑动效果,这里主要会将触摸事件拦截,以及滑动时临界值的计算。首先看一下拦截事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) ev.getRawX();
downY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//拦截move事件 防止子view中有Button一类的控件
int x2 = Math.abs(downX - (int)ev.getRawX());
int y2 = Math.abs(downY - (int)ev.getRawY());
//touchSlop是用来判断是否是一个合理的滑动
//因为一般情况只要我们手指按下去,不发生Move事件的情况很少很少,总要动一点点的,可能我们都没察觉自己动了。
//这里给一个滑动最小距离,大于这个最小距离才算是滑动。
if (x2 > touchSlop || y2 > touchSlop) {
intercept = true;
}
break;
}
return intercept;
}
拦截事件很简单,防止子View中有Button一类的控件,如果没有就可以不用拦截。重点还是在onTouchEvent()的Move事件。
@Override
public void scrollBy(int x, int y) {
scrollX += x;
scrollY += y;
if (needRelayout) {
return;
}
scrollBounds();
if (scrollX == 0) {
// no op
} else if (scrollX > 0) {//向左滑动
//当scrollX大于body(蓝色区域)内第一个可见View的宽度的时候
//这里用while是有可能快速移动,直接处理多个View的情况
while (widths[firstColumn + 1] < scrollX) {
if (!rowViewList.isEmpty()) {
removeLeft();
}
scrollX -= widths[firstColumn + 1];
firstColumn++;
}
//如果不快速滑动 这里的rowViewList可以理解为body中可见的View
//这里的getFilledWidth()其实就是计算出第一列(单向滑动的那列)的宽度+body(蓝色区域)内rowViewList的中保存的所有View的宽度(有可能首尾的View超出去一部分或超出多个View,那部分也算)-scrollX
//所以这里计算的就是body(蓝色区域)左边到rowViewList的中保存的最后一个View的宽度(最后一个View超出屏幕部分也算)。因为scrollX就是向左滑了的部分,也就是左边超出的部分
//这里用while是有可能快速移动,直接处理多个View的情况
while (getFilledWidth() < width) {//这里的判断就是当把最后一个View超出屏幕的部分全部移回来了,就是添加下个view的时候
addRight();
}
} else {//向右滑动第一个View全部出现时调用一次
//往右滑的时候scrollX是负的。所以getFilledWidth()里的-scrollX 成了 +|scrollX|
//和上边的一样getFilledWidth()计算的是第一列(单向滑动的那列)的宽度+body(蓝色区域)的第一个view到rowViewList的中保存的最后一个View的宽度(最后一个View超出屏幕部分也算)
//因为只有在右滑时第一个View全部出现的时候调用一次,所以这里body(蓝色区域)的第一个view就相当于从body的左边开始
//这里判断就相当于:可见的第一个View到rowViewList的中保存的最后一个
while (!rowViewList.isEmpty() && getFilledWidth() - widths[firstColumn + rowViewList.size()] >= width) {
removeRight();
}
//当scrollX小于0的时候证明已经 将之前左滑的部分又向右滑回来了
while (0 > scrollX) {
addLeft();
firstColumn--;
scrollX += widths[firstColumn + 1];
}
}
...
repositionViews();//没有这个体现不出滑动的效果
shadowsVisibility();//分割线
}
我在这里只分析了左右滑动的临界值的计算,说实话有点烧脑,不过自己多尝试两遍还是可以理解的。注释中基本把所有的理解都写了。提示一点注释用到rowViewList的地方如果不快速滑动,可以理解为当前行可见View的一个集合。
最后我感觉可以用 源码 来理解会更方便一些。
这篇文章是在我学习的基础上进行了总结,可想而知我还是个很小的菜鸟,如果其中有错误还请指出,我会尽快修改文章,并改正自己的理解,谢谢。