支持原作者 | CSDN_LQR
地址 | http://www.reibang.com/p/ed7fce51e5b5
最近在做一個地產(chǎn)項目,其實之前做出了一版忿薇,但現(xiàn)在要求重做(連上架的機(jī)會都沒有),很服氣啊~~而現(xiàn)在做的項目呢峡碉,比上一版功能要求更多,其中窄刘,銷控表的界面效果要求跟房產(chǎn)銷冠 APP 的銷控表界面差不多阱驾,先來看下房產(chǎn)銷冠 APP 的銷控表效果吧:
說說我第一次看到這個界面效果時的感覺就谜,就一個詞:amazing~ 是的,公司就我一個人做安卓開發(fā)里覆,感覺有點壓力山大丧荐,但是,不慫喧枷,靜下心來分析一下就明朗多了篮奄。先說說本文核心技術(shù)重點:兩個 RecyclerView 同步滾動。好割去,下面進(jìn)入正文窟却。
分析
1、布局分析
我認(rèn)為的布局實現(xiàn):將銷控表分為左右兩部分:左邊是樓層列表呻逆,右邊是單元(房間)列表夸赫。樓層列表就是一個簡單的LinearLayout+TextView+RecyclerView,單元(房間)列表則有點小復(fù)雜(HorizontalScrollView咖城、LinearLayout)+TextView+RecyclerView茬腿。為了各位看客能直觀理解,我特意做了張圖宜雀,請看:
其中黃色區(qū)域就是銷控表的部分切平。
2、效果分析
當(dāng)左邊的樓層列表上下滑動時辐董,右邊的單元(房間)列表也跟著一起滑動悴品,單元(房間)列表上的單元編號不動。
當(dāng)右邊的單元(房間)列表上下滑動時简烘,左邊的樓層列表也跟著一起滑動苔严,單元(房間)列表上的單元編號不動。
當(dāng)右邊的單元(房間)列表左右滑動時孤澎,單元(房間)列表上的單元編號一起左右滑動届氢,左邊的樓層列表不動。
那么覆旭,要實現(xiàn)1退子、2的效果岖妄,可以監(jiān)聽這兩個列表的滾動,當(dāng)其中一個列表滾動時寂祥,讓另一個列表滾動相同的距離即可衣吠。要實現(xiàn)3的效果就簡單了,因為HorizontalScrollView 中嵌套 RecyclerView 并沒有滾動沖突壤靶,HorizontalScrollView 處理水平滑動事件缚俏,RecyclerView 處理豎直滾動事件,所以暫時不用理(后面還是要做點簡單處理的)贮乳。
實現(xiàn)
1忧换、布局
上面已經(jīng)分析出了布局結(jié)構(gòu),下面直接貼布局代碼:
<?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="統(tǒng)計" 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="樓層
單元" 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>
再通過列表的數(shù)據(jù)進(jìn)行填充(這部分不是重點就不貼出來了)向拆,效果就出來了:
接下來就是實現(xiàn)同步滾動效果了亚茬。
2、多RecyclerView同步滾動實現(xiàn)
一個大體的思路就是分別對其中一個列表設(shè)置滾動監(jiān)聽浓恳,當(dāng)這個列表滾動時刹缝,讓另一個列表也一起滾動。
但細(xì)節(jié)上要考慮到颈将,這種監(jiān)聽是雙向的梢夯,A 列表滾動時觸發(fā)其滾動回調(diào)接口,導(dǎo)致B列表滾動晴圾,而此時 B 列表也已經(jīng)設(shè)置過滾動監(jiān)聽颂砸,它的滾動也會觸發(fā)它的滾動回調(diào)接口,導(dǎo)致A列表滾動死姚,這樣就形成了一個死循環(huán)人乓。所以適當(dāng)添加或移除滾動監(jiān)聽是本功能實現(xiàn)的重難點,下面直接貼出代碼都毒,請自行結(jié)合代碼及注釋理解色罚。
1)封裝一個可以自行取消監(jiān)聽的滾動回調(diào)接口
這樣的封裝使我們不用在其他地方考慮列表空閑狀態(tài)時的處理,會省去很多事账劲。
/*** @創(chuàng)建者 CSDN_LQR* @描述 實現(xiàn)一個RecyclerView.OnScrollListener的子類戳护,當(dāng)RecyclerView空閑時取消自身的滾動監(jiān)聽*/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)為樓層列表控件設(shè)置滾動監(jiān)聽
以下兩段代碼涉及兩個列表滾動同步和添加或移除滾動監(jiān)聽的時機(jī),具體代碼及注釋我已經(jīng)寫得很清楚了涤垫,請仔細(xì)看:
private final RecyclerView.OnScrollListener mLayerOSL = new MyOnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // 當(dāng)樓層列表滑動時姑尺,單元(房間)列表也滑動 mRvRoom.scrollBy(dx, dy); }};/*** 設(shè)置兩個列表的同步滾動*/private void setSyncScrollListener() { mRvLayer.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private int mLastY; @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { // 當(dāng)列表是空閑狀態(tài)時 if (rv.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { onTouchEvent(rv, e); } return false; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { // 若是手指按下的動作竟终,且另一個列表處于空閑狀態(tài) if (e.getAction() == MotionEvent.ACTION_DOWN && mRvRoom.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { // 記錄當(dāng)前另一個列表的y坐標(biāo)并對當(dāng)前列表設(shè)置滾動監(jiān)聽 mLastY = rv.getScrollY(); rv.addOnScrollListener(mLayerOSL); } else { // 若當(dāng)前列表原地抬起手指時蝠猬,移除當(dāng)前列表的滾動監(jiān)聽 if (e.getAction() == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) { rv.removeOnScrollListener(mLayerOSL); } } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } }); ...}
3)為單元(房間)列表設(shè)置滾動監(jiān)聽
對于單元(房間)列表滾動監(jiān)聽的設(shè)置,跟前面一樣统捶,我就順便寫一下好了榆芦。
private final RecyclerView.OnScrollListener mRoomOSL = new MyOnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // 當(dāng)單元(房間)列表滑動時柄粹,樓層列表也滑動 mRvLayer.scrollBy(dx, dy); }};/*** 設(shè)置兩個列表的同步滾動*/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) { } });}
好了,到這里同步滾動效果就實現(xiàn)了匆绣,先看看效果驻右。
3、處理水平滾動列表事件
在上圖中崎淳,我們可以看到 堪夭,同步滾動效果確實是實現(xiàn)了,但有個問題拣凹,只要一水平滾動后森爽,再來滾動左邊的樓層列表時程序就會崩潰,若是滾動右邊的單元(房間)列表則會滾動不同步嚣镜,會造成這種情況是因為爬迟,當(dāng)水平滾動是時,事件被 HorizontalScrollView 處理了菊匿,導(dǎo)致右邊的單元(房間)列表的滾動監(jiān)聽沒有被移除付呕。
當(dāng)我們?nèi)L動左邊的樓層列表時,會為其設(shè)置滾動監(jiān)聽跌捆,這時這兩個列表都存在滾動監(jiān)聽徽职,所以就造成了監(jiān)聽的遞歸調(diào)用(死循環(huán)),于是內(nèi)存就妥妥的溢出了佩厚。下面是錯誤提示:
所以活箕,解決的方法就是,當(dāng) HorizontalScrollView 處理水平滾動事件時可款,取消列表的滾動監(jiān)聽育韩,而 ScrollView 本身不支持滾動監(jiān)聽,所以需要重新HorizontalScrollView闺鲸,向外提供滾動監(jiān)聽功能筋讨。自定義HorizontalScrollView 代碼如下:
/*** @創(chuàng)建者 CSDN_LQR* @描述 自定義HorizontalScrollView,向外提供滑動監(jiān)聽功能*/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>...
在代碼中監(jiān)聽 HorizontalScrollView 滾動摸恍,當(dāng)其滾動時悉罕,移除列表控件的移動監(jiān)聽事件:
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
https://github.com/GitLqr/TopsalesSellControlTableDemo