版本2只是简单的实现了当手指按下的时候listView的Item向左移动一定的距离,并没有随着手指的左右移动而左右滚动,在这个版本3.0中将会实现随着手指的移动而滚动的目标:当手指向左移动的时候,listView向左滚动;当手指向右移动的时候,listView向右滚动;在开始进入正题之前,先说说预备的知识和涉及到的方法。
在version2.0之前添加View的时候用的都是addView最终辗转调用了addViewInner方法,经过查询viewGroup的源码发现有一个addViewInLayout方法,api的说明如下:Adds a view during layout,this is usefule if in your onLayout method.而我们这个横向listView正好是在onLayout里面添加的View,所以从现在开始添加View的时候就改为addViewInLayout了。ViewGrouup里面提供一个View的数组mChildren,在addView方法中调用了addVew(view
view,index)index默认传的是-1,表明添加到View数组mChildren最后面。而addViewInLaout的方法的第二个参数就是这个index,如果index为负数的话就把该View方法数组的最后面(实际代码处理中就是数组的插入操作,很简单的源码),当index为正数的时候就把该View插入到数组中的index的位置,相应的View数组mChildren里面的元素后移,当然对应的删除View的时候也有removeViewInLayout方法;其实getChildAt(int index)就是从数组mChildrenCount获取返回对应索引的View,所以左右滑动的核心思想就出来了:
1)当向左滚动的时候,把右边即将要滚动出来的View添加到mChildren数组的最后面,在页面上的显示为在屏幕的右边的View,同时提供一个rightIndex来表示adapter.getView的position。即addViewInlayout(view,-1,view.getLayoutParams,true),
2)当向右滚动的时候,把左边即将滚动出来的View添加的mChildren数组的最前面数组0的位置,在页面上的显示为在屏幕左边的View,同事提供一个left来表示adapter.getView 的position参数。即addViewInlayout(view,0,view.getLayoutParams,true),用代码体现如下
private void addRightChildViews(int dianceX) { // 2.让屏幕尽可能的显示Item。注意刚开始的时候是没有 View rightChildView = getChildAt(getChildCount() - 1); // 获取此childView右边框距离parentView左边框的距离 int rightEdge = rightChildView != null ? rightChildView.getRight() : 0; while (rightEdge + dianceX < getWidth() && rightIndex < listAdapter.getCount()) { View child = listAdapter.getView(rightIndex, null, null); child = measureChild(child); addViewInLayout(child, -1, child.getLayoutParams(), true); rightEdge += child.getMeasuredWidth(); rightIndex++; } } private void addLeftChildViews(int dianceX) { View leftChildView = getChildAt(0); int leftEdge = leftChildView != null ? leftChildView.getLeft() : 0; while (leftEdge + dianceX > 0 && leftIndex >= 0) { View child = listAdapter.getView(leftIndex, null, null); child = measureChild(child); addViewInLayout(child, 0, child.getLayoutParams(), true); leftEdge -= child.getMeasuredWidth(); leftIndex--; //此处省略了一个重要的代码 } }
该篇博客就是围绕着这两个核心思路进行拓展和修改进而实现左右滚动的。在版本2.0中向左移动的时候需要在合适的时机移除左边符合某些条件的View(具体见简单的横向ListView实现(version 2.0) 同样,在想右滚动的时候也需要把超出右边屏幕的View从Viewgroup里面删除出去(当child.getLeft()+移动偏移量大于parent.getWidth()的时候进行删除):简而言之就是向左滚动时,需要删除滚动过后左边的看不见View;当向右滚动时,需要删除滚动过后右边的看不见的View。用代码表示如下:
/* 删除看不见的view */ private void removeAnvisiableViews(int dianceX) { // 移除左边看不到的view View firtVisiableView = getChildAt(0); if (firtVisiableView != null && dianceX + firtVisiableView.getRight() <= 0) { removeViewInLayout(firtVisiableView); leftIndex++; //此处省略了一行重要的代码 } // 移除右边看不到的view View lastVisialbeView = getChildAt(getChildCount() - 1); if (lastVisialbeView != null && lastVisialbeView.getLeft() + dianceX >= getWidth()) { removeViewInLayout(lastVisialbeView); rightIndex--; } }
到此为止在考虑左右滚动的条件下什么时候添加左边的View、什么时候添加右边的View以及在什么时候删除左右两边看不见的View的思路以及代码都实现了。之前的版本的滚动值都是写死的,现在这个版本将用GestureDetector来处理手指的移动,至于GestureDetector的用法在这里不再赘述,网上好多资料可以查阅在.SimpleOnGestureListener()里面有一个 onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float
distanceY)方法,因为是水平滚动,所以第三个参数distanceX使我们所需要的,该参数指的是距离上次调用onScroll方法的时候x轴滚动的距离,所以在代码中我用变量distanceX来记录下了:
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //记录滚动的距离 HListView.this.distanceX= (int) distanceX; // 仍然需要重绘 requestLayout(); return true; };
这里有个需要注意的小地方,手指向左的时候distanceX>0,而手指向右的时候distanceX<0.所以在用的时候我们要取反,所以在onLayout里面这么调用(较版本2.0之前对这个方法进行了重构处理):
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (listAdapter == null) { return; } removeAnvisiableViews(-distanceX); addRightChildViews(-distanceX); addLeftChildViews(-distanceX); layoutChildViews(-distanceX); }
到此为止基本上该说的都说了,运行一把试试,我屮艸芔茻,发现动不了,怎么回事?各种抓耳挠腮。最终吃饭的时候灵光一闪知道问题出现在哪儿了!我们知道横向listView滚动的逻辑就是重复addView和removeView的操作,但是最重要的一点就是最终都要调用layoutChildViews方法进行处理,而在version2.0版本的代码中layoutChildViews代码是这么样的:
int childLeft = 0; for(int i=0;i<getChildCount();i++) { View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); child.layout(childLeft, 0, childWidth+childLeft, child.getMeasuredHeight()); //不过最好的写法是 childLeft += childWidth+child.getPaddingRight(); }
layout方法的第一个三处我们总是让它从0开始!!!!问题就出在这儿!!!简单的想象一下,当向右滚动的时候左边会出现半个Item的情况(或者不完整的Item),怎么可能是从0开始呢,肯定会出现childLeft<0的情况。所以当处理左边第一个可以看见的子View的时候,layout第一个参数是随着滚动而发生变化的,并不是固定为0的死的值!其实上面的代码中我有写到:此处省略了一行重要的代码,其实这行代码就是解决这个问题的!
我在类里面定义了个leftOffset=0的变量来进行控制表示下一个即将添加的子View的起始位置(注意要加上distanceX),(这个变量的作用想了半天不好用语言描述,郁闷).当调用addleftChildView方法的时候每添加一个View时对该变量进行leftOffset -= child.getMeasuredWidth();。相应的当删除左边的一个View的时候leftOffset += child.getMeasuredWidth();所以layoutchildView的代码修改为:
private void layoutChildViews(int distanceX) { if(getChildCount()==0) { return; } leftOffset += distanceX; int childLeft = leftOffset; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); child.layout(childLeft, 0, childWidth + childLeft, child.getMeasuredHeight()); // 不过最好的写法是 childLeft += childWidth + child.getPaddingRight(); } }
运行一把,OK,爽歪歪!version3.0到此位置完成了!不过功能仍然不够完善:比如当左边已经是第一个Item的时候仍然可以想右边滚动,同理当右边是最后一个View的时候仍然可以向左滚动、同样的当手指离开的时候没有惯性滚动,比如每次getView的时候都需要对xml文件进行解析,没有很好的重用已经解析过的xml等,这些丰富的功能将在下一个版本中实现。(此处为源代码)