在Android原有ListView控件基础之上打造一个类似于表格形式,全方位滚动(既可以上下滚动又可以左右滚动)的UDLRSlideListView控件。
一. 要实现的目标
在实现之前咱们先列出UDLRSlideListView控件要实现的目标有哪些:
- 为了扩展方便重写ListView,ListView大部分的特性我们还继续保留着,关键时候有大用处。
- ListView的行可以左右滑动,因为手机的屏幕比较小,咱们经常显示表格的时候不能保证一个屏幕全部显示完成。
- 在行可以左右滑动的基础上,咱还可以动态设置固定每一行前面多少列是不会滑动(把每一行分成左右两个部分,左边的一部分不能滑动,右边的一部可以滑动)。
- 可以设置标题,并且UDLRSlideListView上下滑动的时候标题可以一直固定在顶部(可以动态设置是否固定)。
- 下拉刷新,上拉加载功能。
二. 效果展示
三. 实现过程
为了实现UDLRSlideListView控件的所有功能,咱们把整体拆分成一个一个小的部分,依次实现,最后再拼到一起来。
3.1 固定标题栏在顶部
中心思想就是在上下滑动的过程中,把标题栏View画到ListView的顶部。这里又得分两个部分了:一个是怎么在上下滚动的过程中拿到标题栏对应的View,并且让重绘;一个怎么画到ListView的顶部。
1). 拿到标题栏,这个简单,adapter里面有getView()的方法,同时我们规定如果有标题栏的时候position=0的位置是标题栏。核心代码如下:
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mScrollListener != null) {
mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (getAdapter() != null && !(getAdapter() instanceof UDLRSlideAdapter)) {
return;
}
int headerViewCount = getHeaderViewsCount();
if (getAdapter() == null || !mPinTitle || firstVisibleItem < headerViewCount) {
/**
* 第一个section都还没出来
*/
mLayoutTitleSection = null;
for (int i = 0; i < visibleItemCount; i++) {
View itemView = getChildAt(i);
if (itemView != null) {
itemView.setVisibility(VISIBLE);
}
}
return;
}
if (getAdapter().getCount() <= 0) {
return;
}
if (mLayoutTitleSection == null) {
mLayoutTitleSection = getTitleSectionLayout(0);
ensurePinViewLayout(mLayoutTitleSection);
}
if (mLayoutTitleSection == null) {
return;
}
invalidate();
}
/**
* 获取固定在顶部的View
*
* @return View
*/
private View getTitleSectionLayout(int adapterPosition) {
if (getAdapter() == null) {
return null;
}
/**
* getView的第二个参数一定要传空,因为我们不能用复用的View
*/
return getAdapter().getView(adapterPosition, null, this);
}
2). 拿到标题栏的View之后,就要把他绘制到ListView的顶部了。核心代码如下(其中mLayoutTitleSection是标题栏View):
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getAdapter() == null || !(getAdapter() instanceof UDLRSlideAdapter) || mLayoutTitleSection == null || !mPinTitle) {
return;
}
int saveCount = canvas.save();
canvas.clipRect(0, 0, getWidth(), mLayoutTitleSection.getMeasuredHeight());
mLayoutTitleSection.draw(canvas);
canvas.restoreToCount(saveCount);
}
ps: 关于固定标题栏在顶部更加具体的细节可以瞧一瞧 Android分组悬浮列表实现
3.2 上下滚动
这个容易,上下滚动我们直接用ListView自带的就好了,super,super就好了。
3.3 左右滚动
对于每一行,前面一部分不让滚动fix,后面一部分可以让滚动slide。这样我们每一行都是一个horizontal的LinearLayout了,并且含有两个LinearLayout,我们在adapter getView()获取每一行的convertView的时候,把固定的column,addView到不可滚动的LinearLayout当中去,把可以滚动的column addView到可以滚动的LinearLayout里面去。这样每一行的convertView就出来了。对每一行的convertView我们做了一个简单的封装UDLRSlideRowLayout主要是封装左右滑动的处理。因为adapter的getView()我们要提前做好处理,那我们就得在BaseAdapter的基础上打造一个UDLRSlideAdapter,核心代码如下(这里我们只是列出了UDLRSlideAdapter里面的getView()方法的实现):
@Override
public View getView(int position, View convertView, ViewGroup parent) {
List<T> itemData = getItem(position);
UDLRSlideViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_udlr_slide, parent, false);
//设置item高度
AbsListView.LayoutParams params = (AbsListView.LayoutParams) convertView.getLayoutParams();
params.height = getItemViewHeight();
convertView.setLayoutParams(params);
onCrateConvertViewFinish(convertView, position);
holder = new UDLRSlideViewHolder(convertView, position);
UDLRSlideRowLayout rowLayout = (UDLRSlideRowLayout) convertView.findViewById(R.id.item_udls_slide_row);
//组合每一行的View,包含两部分,一个是固定的LinearLayout,一个是可滑动的LinearLayout
if (itemData != null && !itemData.isEmpty()) {
for (int index = 0; index < itemData.size(); index++) {
View columnView;
if (index < mSlideStartColumn) {
columnView = getColumnView(position, index, itemData.size(), rowLayout.getFixLayout());
columnView.setLayoutParams(getColumnViewParams(position, index, itemData.size()));
rowLayout.getFixLayout().addView(columnView);
} else {
columnView = getColumnView(position, index, itemData.size(), rowLayout.getSlideLayout());
columnView.setLayoutParams(getColumnViewParams(position, index, itemData.size()));
rowLayout.getSlideLayout().addView(columnView);
}
holder.addColumnView(index, columnView);
}
}
convertView.setTag(holder);
} else {
holder = (UDLRSlideViewHolder) convertView.getTag();
//更新下holder position的位置
holder.setPosition(position);
}
//复用的时候不能滑倒指定位置
UDLRSlideRowLayout slideLayout = (UDLRSlideRowLayout) holder.getConvertView().findViewById(R.id.item_udls_slide_row);
slideLayout.slideSet(mSlideLength);
if (itemData != null && !itemData.isEmpty()) {
for (int index = 0; index < itemData.size(); index++) {
if (holder.getColumnView(index) != null) {
convertColumnViewData(position, index, holder.getColumnView(index), convertView, itemData.get(index), itemData);
}
}
}
return convertView;
}
这里稍微做一点点解释,15~29行,生成每一个row里面所有的column的View并且分成了两部分,不可以滑动的column View add到了fix layout里面,可以滑动的column View add到了slide layout里面。
到这里每一行的converView我们就已经组合好了,接下来就是捕捉左右滑动的事件了,这里为了简单一点当左右滑动的同时我们就不让上下滑动了,事件的捕捉这个应该简单吧,那咱就得对onTouchEvent()函数动刀子了,核心代码如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handler = false;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();//跟踪触摸事件滑动的帮助类
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(x - mLastMotionDownX);
final int yDiff = (int) Math.abs(y - mLastMotionDownY);
if (!mInSlideMode) {
if (xDiff > mTouchSlop && xDiff > yDiff) {
mInSlideMode = true;
}
}
if (mInSlideMode) {
final int deltaX = (int) (mLastMotionX - x);//滑动的距离
prepareSlideMove(deltaX);
mLastMotionX = x;
handler = true;
}
break;
case MotionEvent.ACTION_UP:
if (mInSlideMode) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);//1000毫秒移动了多少像素
int velocityX = (int) velocityTracker.getXVelocity();//当前的速度
if (canSlide()) {
if (Math.abs(velocityX) < SNAP_VELOCITY) {
//TODO:
} else {
prepareFling(-velocityX);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
}
mInSlideMode = false;
break;
case MotionEvent.ACTION_CANCEL:
mInSlideMode = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return handler || super.onTouchEvent(ev);
}
UDLRSlideListView里面onTouchEvent()函数15~26行,判断是左右滑动,拿到滑动的位移,最终会调用到UDLRSlideRowLayout里面的slideMove()函数,更加详细的实现可以参考UDLRSlideRowLayout类的实现。
3.4 事件的拦截
每一行里面的某列子View要自己处理事件的时候,会和左右滑动事件冲突,那咱们就得稍微做点处理了,当左右滑动的时候,咱得把事件拦截下来,那就得对onInterceptTouchEvent函数动刀子了,核心代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(x - mLastMotionDownX);
final int yDiff = (int) Math.abs(y - mLastMotionDownY);
if (!mInSlideMode) {
if (xDiff > mTouchSlop && xDiff > yDiff) {
return true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return super.onInterceptTouchEvent(ev);
}
ps:在实现的过程中,这里有个小插曲,也算是个小问题吧,在MotionEvent.ACTION_DOWN的时候我们肯定是要做一些初始化的,刚开始的时候我是放在onTouchEvent()函数里面做的,后来我直接放到dispatchTouchEvent()函数里面去处理了。因为当子View有事件处理的时候onTouchEvent()函数里面是走不到MotionEvent.ACTION_DOWN事件的。
3.5 下拉刷新,上拉加载
因为我们的UDLRSlideListView没有破坏ListView的特性,这样网上就有很多开源的框架来给我们实现这个功能了。例子里面用的XRefreshView实现的。
四. 源码
五. 写在结尾的话
整个实现过程介绍的很简单,如果有相同需求的话可以扒源码下来瞧一瞧,也可以在这基础上做相应的修改,当然里面肯定也有很多不合理的地方,很多需要完善的地方。欢迎指出。