Android打造全方位滚动的ListView

来源:互联网 发布:python生成密码 编辑:程序博客网 时间:2024/05/17 05:55

       在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实现的。

四. 源码

       源码链接

五. 写在结尾的话

       整个实现过程介绍的很简单,如果有相同需求的话可以扒源码下来瞧一瞧,也可以在这基础上做相应的修改,当然里面肯定也有很多不合理的地方,很多需要完善的地方。欢迎指出。

原创粉丝点击