查看原文
其他

教你手撸一个APP销控表滑动自定义View—多RecyclerView同步滚动

2017-07-18 CSDN_LQR 终端研发部

前言介绍

关于Activity完全分析及其应用场景的介绍。

CSDN_LQR的博客地址:

http://blog.csdn.net/CSDN_LQR/article/details/75095517

正文 

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

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

二、分析

1、布局分析

    • 我认为的布局实现:将销控表分为左右两部分:左边是楼层列表,右边是单元(房间)列表。

  • 楼层列表就是一个简单的LinearLayout+TextView+RecyclerView,单元(房间)列表则有点小复杂(HorizontalScrollView、LinearLayout)+TextView+RecyclerView

  • 为了各位看客能直观理解,我特意做了张图,请看: 其中黄色区域就是销控表的部分。

2、效果分析

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

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

  • 当右边的单元(房间)列表左右滑动时,单元(房间)列表上的单元编号一起左右滑动,左边的楼层列表不动。

  • 那么,要实现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连接

github项目链接地址:

https://github.com/GitLqr/TopsalesSellControlTableDemo 

终端研发部提倡 没有做不到的,只有想不到的。

在这里获得的不仅仅是技术! 


让心,在阳光下学会舞蹈

让灵魂,在痛苦中学会微笑

—终端研发部—



如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :232203809   

微信公众号:终端研发部


            

这里学到的不仅仅是技术



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存