Android打造全方位滚动的ListView

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

四. 源码

       源码链接

五. 写在结尾的话

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值