美文网首页andnroid自定义控件Android iOS开发知识库
仿房产销冠APP销控表界面-多RecyclerView同步滚动

仿房产销冠APP销控表界面-多RecyclerView同步滚动

作者: GitLqr | 来源:发表于2017-07-14 09:03 被阅读2093次

    一、简述

    最近在做一个地产项目,其实之前做出了一版,但现在要求重做(连上架的机会都没有),很服气啊~~而现在做的项目呢,比上一版功能要求更多,其中,销控表的界面效果要求跟房产销冠APP的销控表界面差不多,先来看下房产销冠APP的销控表效果吧:

    房产销冠APP的销控表效果

    说说我第一次看到这个界面效果时的感觉,就一个词:amazing~ 是的,公司就我一个人做安卓开发,感觉有点压力山大,但是,不怂,静下心来分析一下就明朗多了。先说说本文核心技术重点:两个RecyclerView同步滚动。好,下面进入正文。

    二、分析

    1、布局分析

    我认为的布局实现:将销控表分为左右两部分:左边是楼层列表,右边是单元(房间)列表。楼层列表就是一个简单的LinearLayout+TextView+RecyclerView,单元(房间)列表则有点小复杂(HorizontalScrollView、LinearLayout)+TextView+RecyclerView。为了各位看客能直观理解,我特意做了张图,请看:

    其中黄色区域就是销控表的部分。

    布局实现

    2、效果分析

    1. 当左边的楼层列表上下滑动时,右边的单元(房间)列表也跟着一起滑动,单元(房间)列表上的单元编号不动。
    2. 当右边的单元(房间)列表上下滑动时,左边的楼层列表也跟着一起滑动,单元(房间)列表上的单元编号不动。
    3. 当右边的单元(房间)列表左右滑动时,单元(房间)列表上的单元编号一起左右滑动,左边的楼层列表不动。

    那么,要实现1、2的效果,可以监听这两个列表的滚动,当其中一个列表滚动时,让另一个列表滚动相同的距离即可。要实现3的效果就简单了,因为HorizontalScrollView中嵌套RecyclerView并没有滚动冲突,HorizontalScrollView处理水平滑动事件,RecyclerView处理竖直滚动事件,所以暂时不用理(后面还是要做点简单处理的)。

    三、实现

    1、布局

    上面已经分析出了布局结构,下面直接贴布局代码:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f5f5f5"
        android:orientation="vertical">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
    
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="销控表"
                    android:textColor="#000"
                    android:textSize="16sp"/>
    
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical|right"
                    android:layout_marginRight="10dp"
                    android:text="统计"
                    android:textColor="#000"
                    android:textSize="12sp"/>
    
            </android.support.v7.widget.Toolbar>
        </android.support.design.widget.AppBarLayout>
    
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#fff"
            android:gravity="center"
            android:padding="10dp"
            android:text="CSDN_LQR的私人后宫-项目1期-1栋"
            android:textColor="#333"
            android:textSize="10sp"/>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10px"
            android:orientation="horizontal">
    
            <!--楼层-->
            <LinearLayout
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:orientation="vertical">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#fff"
                    android:gravity="center"
                    android:padding="10dp"
                    android:text="楼层&#x000A;单元"
                    android:textSize="12sp"/>
    
                <android.support.v7.widget.RecyclerView
                    android:id="@+id/rv_layer"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginTop="1dp"/>
    
            </LinearLayout>
    
            <!--单元(房间)-->
            <HorizontalScrollView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="4dp"
                android:fillViewport="true"
                android:scrollbars="none">
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">
    
                    <TextView
                        android:layout_width="match_parent"
                        android:layout_height="50dp"
                        android:background="#fff"
                        android:gravity="center"
                        android:padding="10dp"
                        android:text="3"
                        android:textSize="12sp"/>
    
                    <android.support.v7.widget.RecyclerView
                        android:id="@+id/rv_room"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_marginTop="1dp"/>
                </LinearLayout>
            </HorizontalScrollView>
        </LinearLayout>
    </LinearLayout>
    

    再通过列表的数据进行填充(这部分不是重点就不贴出来了),效果就出来了:

    初步效果

    接下来就是实现同步滚动效果了。

    2、多RecyclerView同步滚动实现

    一个大体的思路就是分别对其中一个列表设置滚动监听,当这个列表滚动时,让另一个列表也一起滚动。
    但细节上要考虑到,这种监听是双向的,A列表滚动时触发其滚动回调接口,导致B列表滚动,而此时B列表也已经设置过滚动监听,它的滚动也会触发它的滚动回调接口,导致A列表滚动,这样就形成了一个死循环。所以适当添加或移除滚动监听是本功能实现的重难点,下面直接贴出代码,请自行结合代码及注释理解。

    1)封装一个可以自行取消监听的滚动回调接口

    这样的封装使我们不用在其他地方考虑列表空闲状态时的处理,会省去很多事。

    /**
     * @创建者 CSDN_LQR
     * @描述 实现一个RecyclerView.OnScrollListener的子类,当RecyclerView空闲时取消自身的滚动监听
     */
    public class MyOnScrollListener extends RecyclerView.OnScrollListener {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == recyclerView.SCROLL_STATE_IDLE) {
                recyclerView.removeOnScrollListener(this);
            }
        }
    }
    

    2)为楼层列表控件设置滚动监听

    以下两段代码涉及两个列表滚动同步和添加或移除滚动监听的时机,具体代码及注释我已经写得很清楚了,请仔细看:

    private final RecyclerView.OnScrollListener mLayerOSL = new MyOnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            // 当楼层列表滑动时,单元(房间)列表也滑动
            mRvRoom.scrollBy(dx, dy);
        }
    };
    
    /**
     * 设置两个列表的同步滚动
     */
    private void setSyncScrollListener() {
        mRvLayer.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
    
            private int mLastY;
    
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                // 当列表是空闲状态时
                if (rv.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                    onTouchEvent(rv, e);
                }
                return false;
            }
    
            @Override
            public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                // 若是手指按下的动作,且另一个列表处于空闲状态
                if (e.getAction() == MotionEvent.ACTION_DOWN && mRvRoom.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                    // 记录当前另一个列表的y坐标并对当前列表设置滚动监听
                    mLastY = rv.getScrollY();
                    rv.addOnScrollListener(mLayerOSL);
                } else {
                    // 若当前列表原地抬起手指时,移除当前列表的滚动监听
                    if (e.getAction() == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                        rv.removeOnScrollListener(mLayerOSL);
                    }
                }
            }
    
            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
            }
        });
    
        ...
    }
    

    3)为单元(房间)列表设置滚动监听

    对于单元(房间)列表滚动监听的设置,跟前面一样,我就顺便写一下好了。

    private final RecyclerView.OnScrollListener mRoomOSL = new MyOnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            // 当单元(房间)列表滑动时,楼层列表也滑动
            mRvLayer.scrollBy(dx, dy);
        }
    };
    
    /**
     * 设置两个列表的同步滚动
     */
    private void setSyncScrollListener() {
    
        ...
    
        mRvRoom.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
    
            private int mLastY;
    
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                if (rv.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                    onTouchEvent(rv, e);
                }
                return false;
            }
    
            @Override
            public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                if (e.getAction() == MotionEvent.ACTION_DOWN && mRvLayer.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
                    mLastY = rv.getScrollY();
                    rv.addOnScrollListener(mRoomOSL);
                } else {
                    if (e.getAction() == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
                        rv.removeOnScrollListener(mRoomOSL);
                    }
                }
            }
    
            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
            }
        });
    }
    

    好了,到这里同步滚动效果就实现了,先看看效果。

    不完美的效果

    3、处理水平滚动列表事件

    在上图中,我们可以看到 ,同步滚动效果确实是实现了,但有个问题,只要一水平滚动后,再来滚动左边的楼层列表时程序就会崩溃,若是滚动右边的单元(房间)列表则会滚动不同步,会造成这种情况是因为,当水平滚动是时,事件被HorizontalScrollView处理了,导致右边的单元(房间)列表的滚动监听没有被移除。

    代码执行解析

    当我们去滚动左边的楼层列表时,会为其设置滚动监听,这时这两个列表都存在滚动监听,所以就造成了监听的递归调用(死循环),于是内存就妥妥的溢出了。下面是错误提示:

    内存溢出

    所以,解决的方法就是,当HorizontalScrollView处理水平滚动事件时,取消列表的滚动监听,而ScrollView本身不支持滚动监听,所以需要重新HorizontalScrollView,向外提供滚动监听功能。自定义HorizontalScrollView代码如下:

    /**
     * @创建者 CSDN_LQR
     * @描述 自定义HorizontalScrollView,向外提供滑动监听功能
     */
    public class ObservableHorizontalScrollView extends HorizontalScrollView {
    
        private ScrollViewListener scrollViewListener = null;
    
        public ObservableHorizontalScrollView(Context context) {
            super(context);
        }
    
        public ObservableHorizontalScrollView(Context context, AttributeSet attrs,
                                              int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public ObservableHorizontalScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public void setScrollViewListener(ScrollViewListener scrollViewListener) {
            this.scrollViewListener = scrollViewListener;
        }
    
        @Override
        protected void onScrollChanged(int x, int y, int oldx, int oldy) {
            super.onScrollChanged(x, y, oldx, oldy);
            if (scrollViewListener != null) {
                scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
            }
        }
    
        public interface ScrollViewListener {
            void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldx, int oldy);
        }
    
    }  
    

    接着就是替换代码中的HorizontalScrollView控件

    ...
    <!--单元(房间)-->
    <com.lqr.topsales.ObservableHorizontalScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="4dp"
        android:fillViewport="true"
        android:scrollbars="none">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#fff"
                android:gravity="center"
                android:padding="10dp"
                android:text="3"
                android:textSize="12sp"/>
    
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv_room"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginTop="1dp"/>
        </LinearLayout>
    </com.lqr.topsales.ObservableHorizontalScrollView>
    ...
    

    在代码中监听HorizontalScrollView滚动,当其滚动时,移除列表控件的移动监听事件:

    mSvRoom.setScrollViewListener(new ObservableHorizontalScrollView.ScrollViewListener() {
        @Override
        public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldx, int oldy) {
            mRvLayer.removeOnScrollListener(mLayerOSL);
            mRvRoom.removeOnScrollListener(mRoomOSL);
        }
    });
    

    再来试试效果:

    最终效果

    四、最后附上DEMO连接

    TopsalesSellControlTableDemo

    相关文章

      网友评论

      • 317e040e3aaf:你好 请教 一下怎么 根据x y 左边 精准定位一个格子呢?
      • 桐乃丶黑猫酱:我就想知道如何更换其中的23值、我不会弄
      • Xzhi:楼主,发现一个bug,当我快速滑动一个RecyclerView(还处在滚动状态,我已经停止触摸了),同一时刻我去滑动另一个RecyclerView,这时候就造成了,两个RecyclerView不对称了
      • Vander丶:首先,看到博主博客,然后手痒了,然后自己写按照你的思路写了一个.之后看成型的效果,确实很棒.

        但是期间还是有一些问题,也就当是建议给博主了:

        1.某个方法貌似根本不触发,例如这个底下的else里面的内容几乎没用,mLastY一直是0....

        if (e.getAction() == MotionEvent.ACTION_DOWN && mRvLayer.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
        mLastY = rv.getScrollY();
        rv.addOnScrollListener(mRoomOSL);
        } else {
        if (e.getAction() == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
        rv.removeOnScrollListener(mRoomOSL);
        }
        }

        2.之前看艺术探索的时候的还是感觉从父容器分发事件,比子View控制父View实现的方式好一点,具体是啥,我也有点忘了,但是这只是建议,这条不是特别重要.

        3.这个Demo写完之后有一个很严肃的问题,当多点触控的时候,这个Demo就会Bug频出,

        具体测试方式:

        左右两侧双手同向,反向

        右侧横划 然后控制左侧都会出现问题.

        最后看完其实收获也不少,上面都是一些看完后的建议,当然我真的是认真看的.

        希望博主加油,以后出更好的作品.
        Vander丶:不管怎么样读完了,在我尝试的时候,思考了很多,也学到了很多,尤其是滑动这块.

        额,该下班了 .
      • 木子而东:之前看你还在忙毕设,你现在在哪工作呀:stuck_out_tongue_closed_eyes:
        GitLqr:@小五_李陈 哇,居然还有人记得俺,我现在在深圳工作呀,一直在努力学习
      • 梦华芳秋:好像知道,题主是如何一步步实现这个功能的。中间有懵逼的瞬间吗?还是自己拿着需求一步步分析出来的,这一步步分析的过程,是内功心法啊:heart_eyes:
        GitLqr:@梦华芳秋 百分之99的百度solo大法 + 百分之1的内功心法,哈哈

      本文标题:仿房产销冠APP销控表界面-多RecyclerView同步滚动

      本文链接:https://www.haomeiwen.com/subject/aoknhxtx.html