Android中ListView结合HorizontalScrollView实现水平和竖直方向上的滚动效果

    一言不合先上图:

    本文实现上图效果采用了两种方案:

    方案一:采用头布局+左右两个ListView实现,头布局的右半部分以及右边的ListView完全包含在自定义HorizontalScrollView中。

    首先自定义一个CusHorizontalScrollView,在其中声明另一个HorizontalScrollView mView,添加set方法,复写onScrollChanged()方法,当本身滚动时,调用mView的滚动方法,以实现头布局和列表布局的水平同步滚动。代码如下:

    private HorizontalScrollView mView;

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if(mView != null){
            mView.scrollTo(l, t);
        }
    }

    public void setSynHorizontalScrollView(HorizontalScrollView horizontalScrollView){
        mView = horizontalScrollView;
    }

    本方案除了考虑头布局右半部分和列表item右半部分水平方向上同步滚动外,还需要考虑两个ListView在竖直方向上同步滚动的问题,解决方案如下:

    /**
     * 两个listView同步滑动
     * @param listView1
     * @param listView2
     */
    public static void setListViewOnTouchAndScrollListener(final ListView listView1, final ListView listView2){
        //设置listview2列表的scroll监听,用于滑动过程中左右不同步时校正
        listView2.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                //如果停止滑动
                if(scrollState == 0 || scrollState == 1){
                    //获取第一个子view
                    View subView = view.getChildAt(0);
                    if(subView != null){
                        int top = subView.getTop();  //第一个可见item的顶部距离ListView顶部的距离,通常为负值
                        int top1 = listView1.getChildAt(0).getTop();
                        int position = view.getFirstVisiblePosition();
                        //Log.i("sty", "content top: " + top + " | title top:: " + top1 + " | position: " + position );

                        //如果两个首个显示的子view高度不等
                        if(top != top1){
                            listView1.setSelectionFromTop(position, top);
                        }
                    }
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                                 int totalItemCount) {
                View subView = view.getChildAt(0);
                View otherView = listView1.getChildAt(0);
                if(subView != null && otherView != null){
                    int top = subView.getTop();
                    int top1 = otherView.getTop();
                    //Log.i("sty", "-----content top: " + top + " | title top:: " + top1 + " | firstVisibleItem: " + firstVisibleItem );

                    //如果两个首个显示的子view高度不等
                    if(!(top1 - 7 < top && top < top1 + 7)){
                        listView1.setSelectionFromTop(firstVisibleItem, top);
                        listView2.setSelectionFromTop(firstVisibleItem, top);
                    }
                    //使用以下代码可以解决两个listView同步不流畅的问题,但是应该会更耗性能
//                    if(top != top1){
//                        listView1.setSelectionFromTop(firstVisibleItem, top);
//                        listView2.setSelectionFromTop(firstVisibleItem, top);
//                    }
                }
            }
        });

        //设置listview1列表的scroll监听,用于滑动过程中左右不同步时校正
        listView1.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                //如果停止滑动
                if(scrollState == 0 || scrollState == 1){
                    //获取第一个子view
                    View subView = view.getChildAt(0);
                    View otherView = listView2.getChildAt(0);
                    if(subView != null){
                        int top = subView.getTop();
                        int top1 = otherView.getTop();
                        int position = view.getFirstVisiblePosition();

                        //如果两个首个显示的子view高度不等
                        if(top != top1){
                            listView1.setSelectionFromTop(position, top);
                            listView2.setSelectionFromTop(position, top);
                        }
                    }
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                                 int totalItemCount) {
                View subView = view.getChildAt(0);
                if(subView != null){
                    int top = subView.getTop();
                    listView1.setSelectionFromTop(firstVisibleItem, top);
                    listView2.setSelectionFromTop(firstVisibleItem, top);
                }
            }
        });
    }

    使用代码如下:


    private void initView(){
        lvName = findViewById(R.id.lv_name);
        lvContent = findViewById(R.id.lv_content);
        hslHeader = findViewById(R.id.hsl_header);
        hslContent = findViewById(R.id.hsl_content);

        lvName.setTag(lvContent);
        lvContent.setTag(lvName);
        hslHeader.setSynHorizontalScrollView(hslContent);
        hslContent.setSynHorizontalScrollView(hslHeader);

        ViewUtils.setListViewOnTouchAndScrollListener(lvName, lvContent);
    }

    private void setListView(){
        dataList = createDataList();

        setLvName();
        setLvContent();
        lvName.setSelection(0);
        lvContent.setSelection(0);
    }

    方案二:采用头布局+一个ListView实现,头布局的右半部分以及ListView中的Item的右半部分包含在自定义的HorizontalScrollView中。

    首先自定义一个CustomHScrollView,在其中定义一个滚动监听器OnScrollChangedListener和滚动观察者ScrollViewObserver,该滚动观察者持有一个滚动监听器的list集合,在滚动观察者的notifyOnScrollChanged()方法中遍历其持有的滚动监听器,并调用监听器的回调方法。然后复写HorizontalScrollView中的onScrollChanged()方法,在其中调用滚动观察者的notifyOnScrollChanged()方法,以实现对ListView中所有item的自定义CustomHScrollView实现同步滚动。代码如下:

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        //滚动时通知观察者
        if(mScrollViewObserver != null){
            mScrollViewObserver.notifyOnScrollChanged(l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    /**
     * 自定义的滚动监听接口
     * 当发生滚动事件时的接口,供外部访问
     */
    public interface OnScrollChangedListener{
        void onScrollChanged(int l, int t, int oldl, int oldt);
    }

    //添加滚动事件监听
    public void addOnScrollChangedListener(OnScrollChangedListener listener){
        mScrollViewObserver.addOnScrollChangedListener(listener);
    }

    //移除滚动事件监听
    public void removeOnScrollChangedListener(OnScrollChangedListener listener){
        mScrollViewObserver.removeOnScrollChangedListener(listener);
    }

    /**
     * 滚动观察者
     */
    public static class ScrollViewObserver{
        List<OnScrollChangedListener> mChangedListeners;

        public ScrollViewObserver(){
            super();
            mChangedListeners = new ArrayList<>();
        }

        //添加滚动事件监听
        public void addOnScrollChangedListener(OnScrollChangedListener listener){
            mChangedListeners.add(listener);
        }

        //移除滚动事件监听
        public void removeOnScrollChangedListener(OnScrollChangedListener listener){
            mChangedListeners.remove(listener);
        }

        //通知
        public void notifyOnScrollChanged(int l, int t, int oldl, int oldt){
            if(mChangedListeners == null || mChangedListeners.size() == 0){
                return;
            }
            for(int i = 0; i < mChangedListeners.size(); i++){
                if(mChangedListeners.get(i) != null){
                    mChangedListeners.get(i).onScrollChanged(l, t, oldl, oldt);
                }
            }
        }
    }

    为了解决滑动冲突问题,需要在自定义的CustomHScrollView外层包一层自定义的线性布局InterceptScrollLinearLayout,在其中根据手势来做事件拦截和分发操作,核心代码如下:

    //拦截onTouch事件  上下滑动时拦截事件返回true  左右滑动时不拦截返回false
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if (mTouchState == TOUCH_STATE_HORIZONTAL_SCROLLING) {
                    intercept = false;  //左右滑动时不拦截事件
                } else if (mTouchState == TOUCH_STATE_VERTICAL_SCROLLING) {
                    intercept = true;  //上下滑动时拦截
                } else {
                    float x = ev.getX();
                    int xDiff = (int) Math.abs(x - mLastMotionX);
                    boolean xMoved = xDiff > mTouchSlop;

                    float y = ev.getY();
                    int yDiff = (int) Math.abs(y - mLastMotionY);
                    boolean yMoved = yDiff > mTouchSlop;

                    if(xMoved){
                        if(xDiff >= yDiff && xDiff >= mTouchSlop){ //Scroll if the user moved far enough along the X axis
                            mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
                            mLastMotionX = x;
                        }
                    }

                    if(yMoved){
                        if(yDiff > xDiff && yDiff >= mTouchSlop){ //Scroll if the user moved far enough along the Y axis
                            mTouchState = TOUCH_STATE_VERTICAL_SCROLLING;
                            mLastMotionY = y;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_DOWN:
                mTouchState = TOUCH_STATE_REST;
                mLastMotionX = ev.getX();
                mLastMotionY = ev.getY();
                intercept = false;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //release the drag
                mTouchState = TOUCH_STATE_REST;
                intercept = false;
                break;
            default:
                break;

        }
        return intercept;
    }

    列表适配器初始化时需要传入头布局,在getView()方法中需要找到头布局中的自定义CustomHScrollView headerScrollView,以该headerScrollView为基准,为其添加每个item得到的CustomHScrollView滚动回调监听器,当item的CustomHScrollView滚动时只需要调用headerScrollView的滚动事件,就可以实现头布局和所有item水平同步滚动了。核心代码如下:

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if(convertView == null){
            convertView = inflater.inflate(R.layout.list_item, parent, false);
            holder = new ViewHolder();
            CustomHScrollView scrollView = convertView.findViewById(R.id.hsl_scrollview);
            holder.scrollView = scrollView;
            //省略部分获取控件的代码

            convertView.setTag(holder);
        }else{
            holder = (ViewHolder) convertView.getTag();
        }

        //省略部分给控件设置值的代码
        final CustomHScrollView headerScrollview = listHeader.findViewById(R.id.hsl_scrollview);
        headerScrollview.addOnScrollChangedListener(new OnScrollChangedListenerImpl(holder.scrollView));
        holder.scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View view, int i, int i1, int i2, int i3) {
                headerScrollview.smoothScrollTo(i, i1);
            }
        });
        holder.tvName.setOnClickListener(new MyOnClickListener(position));
        holder.llItemList.setOnClickListener(new MyOnClickListener(position));

        return convertView;
    }

    class OnScrollChangedListenerImpl implements CustomHScrollView.OnScrollChangedListener{
        CustomHScrollView mScrollViewArg;

        public OnScrollChangedListenerImpl(CustomHScrollView scrollViewArg){
            mScrollViewArg = scrollViewArg;
        }

        @Override
        public void onScrollChanged(int l, int t, int oldl, int oldt) {
            mScrollViewArg.smoothScrollTo(l, t);
        }
    }

    使用代码如下:

    private void initView() {
        mListView = findViewById(R.id.lv_list_view);
        listViewHeader = findViewById(R.id.head_layout);
        listViewHeader.setFocusable(true);
        listViewHeader.setClickable(true);
        setListView();
    }

    private void setListView(){
        dataList = createDataList();
        adapter = new ListViewAdapter(this, dataList, listViewHeader);
        mListView.setAdapter(adapter);
    }

    源码传送门:https://github.com/tianyalu/RelationListView        

    如果觉得本文对您有帮助的话欢迎给个star。

    本文参考:Android -- 自定义实现横竖双向滚动的列表(ListView)布局

         Android:30分钟弄明白Touch事件分发机制

         Android开发ScrollView上下左右滑动事件冲突整理一(根据事件)

         在此对各位大神表示感谢。

 

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值