仿房產銷冠APP銷控表介面-多RecyclerView同步滾動

GitLqr發表於2017-07-14

一、簡述

最近在做一個地產專案,其實之前做出了一版,但現在要求重做(連上架的機會都沒有),很服氣啊~~而現在做的專案呢,比上一版功能要求更多,其中,銷控表的介面效果要求跟房產銷冠APP的銷控表介面差不多,先來看下房產銷冠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

相關文章