Android仿eleme點餐頁面二級聯(lián)動列表
本周末外賣點得多,就仿一仿“餓了么”好了。先上圖吧,這樣的訂單頁面是不是很眼熟:
右邊的listview分好組以后,在左邊的Tab頁建立索引??梢灾苯訉?dǎo)航,是不是很方便。關(guān)鍵在于右邊滑動,左邊也會跟著滑;而點擊左邊呢,也能定位右邊的項。它們存在這樣一種特殊的交互。像這種聯(lián)動的效果,還有些常見的例子呢,比如知乎采用了常見的toolbar+viewPager的聯(lián)動,只不過是上下布局:
再看看點評,它的城市選擇頁面也有這種聯(lián)動的影子,只是稍微弱一點。側(cè)邊欄可以對listview進(jìn)行索引,這最早是在微信好友列表里出現(xiàn)的把:
趁著周末,我也擼一個。就拓展性而言,應(yīng)該可以適配以上所有情況吧。我稱其為LinkedLayout,看下效果圖:
我把右邊按5個一組,可以看到,左邊的索引 = 右邊/5
特點
右邊滑動,左邊跟著動
左邊滑動到邊界,右邊跟著動
點擊左邊tab項,右邊滑動定位到相應(yīng)的group
源碼
github 傳送門: https://github.com/fashare2015/LinkedScrollDemo
知識點
做之前先羅列一下知識點,或者說我們能從這個demo里收獲到什么。
面向抽象/接口編程
自定義 view
代理模式
UML類圖
復(fù)習(xí) listview && recyclerview 的細(xì)節(jié)
感覺做完以后收獲最大的還是第一點,面向接口編程。事實上,完成功能的時間只占了一半,后邊的時間一直在抽象和重構(gòu);哎,一步到位太難了,還是老老實實寫具體類,再抽取基類把。
構(gòu)思
UI部分
LinkedLayout
要做的呢是兩個相互關(guān)聯(lián)的列表,在左邊的作為tab頁,右邊的作為content頁。先不考慮交互,我們來打個界面:搞一個叫做LinkedLayout的類,用來盛放tab和content:
public class LinkedLayout extends LinearLayout { private Context mContext; private BaseScrollableContainer mTabContainer; private BaseScrollableContainer mContentContainer; private SectionIndexer mSectionIndexer; // 代理 ... }
我們讓它繼承了LinearLayout,同時持有兩個Container的東東,還有一個上帝對象mContext,以及一個分組用的SectionIndexer。
BaseScrollableContainer
先別管這些,主要看兩個Container,從名字上看一個是tab頁,一個是content頁,嘿嘿。因為它們都能scroll嘛,干脆搞一個BaseScrollableContainer把。取名為Container呢,當(dāng)然是致敬Fragment啦。我們來定義一下這個類:
初步一想,無非有一個 mContext, 一個 viewGroup, 還有一些 Listener 嘛:
public abstract class BaseScrollableContainer<VG extends ViewGroup> { protected Context mContext; public VG mViewGroup; protected RealOnScrollListener mRealOnScrollListener; private EventDispatcher mEventDispatcher; ... }
和我們預(yù)想的差不多嘛,mContext上下文,mViewGroup基本就是指代我們的兩個listview了吧。當(dāng)然,我之后可是要做toolbar+viewpager的,肯定得依賴抽象,不能直接寫listview啦。余下兩個是Listener,等我們界面搭好,寫交互的時候在看把。
看來UML圖還是有好處的,繼承和依賴關(guān)系一目了然。
自定義View && 動態(tài)布局
好了到了自定義view地環(huán)節(jié)了。我們已經(jīng)有了一個LinkedLayout,這是我們的activity_main.xml布局代碼:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.fashare.linkedscrolldemo.ui.LinkedLayout android:id="@+id/linked_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"/> </RelativeLayout>
擦,就沒了嘛?剩下的得靠Java代碼來搞啦?;氐絃inkedLayout咱們來布局UI~:
public class LinkedLayout extends LinearLayout { ... private static final int MEASURE_BY_WEIGHT = 0; private static final float WEIGHT_TAB = 1; private static final float WEIGHT_CONTENT = 3; public void setContainers(BaseScrollableContainer tabContainer, BaseScrollableContainer contentContainer) { mTabContainer = tabContainer; mContentContainer = contentContainer; mTabContainer.setEventDispatcher(this); mContentContainer.setEventDispatcher(this); // 設(shè)置 LayoutParams mTabContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams( MEASURE_BY_WEIGHT, ViewGroup.LayoutParams.WRAP_CONTENT, WEIGHT_TAB )); mContentContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams( MEASURE_BY_WEIGHT, ViewGroup.LayoutParams.MATCH_PARENT, WEIGHT_CONTENT )); this.addView(mTabContainer.mViewGroup); this.addView(mContentContainer.mViewGroup); this.setOrientation(HORIZONTAL); } }
搞了個setContainers用來注入我們的Container,里邊有一些像layout_height,layout_width,layout_weight,orientation之類的,很眼熟吧,和xml沒差。順便一提的是,我們用了weight屬性來控制這個比例1:3,一直感覺這個屬性比較神奇。。。
注入ViewGroup, 使用自定義的LinkedLayout
到這里為止,LinkedLayout已經(jīng)布局好了,我們分別注入ViewGroup就可以用了。我這里分別用listview作tab,recyclerview作content。想像力有限,用來用去好像也就這么幾個控件。。。這部分代碼很簡單,在MainActivity里,就不貼了。
子類化 BaseScrollableContainer
按照常理,下邊應(yīng)該實現(xiàn)基類了吧。前面的MainActivity中,我們是這樣實例化的:
mTabContainer = new ListViewTabContainer(this, mListView); mContentContainer = new RecyclerViewContentContainer(this, mRecyclerView);
看名字一個是listview填充的tab,一個是recyclerview填充的content。就先實現(xiàn)這兩個類吧,從圖中可以看到,它們分別繼承于BaseScrollableContainer,并被LinkedLayout所持有:
交互部分
與用戶的交互:OnScrollListener 與 代理模式
終于到了交互部分,既然是滑動,那少不了定義監(jiān)聽器啦。然而,麻煩在于listview和recyclerview各自的OnScrollListener還不一樣,這個時候如果各自實現(xiàn)的話,既麻煩,又有冗余。像這樣子:
// RecyclerView public class RecyclerViewContentContainer extends BaseScrollableContainer<RecyclerView> { ... @Override protected void setOnScrollListener() { mViewGroup.addOnScrollListener(new ProxyOnScrollListener()); } private class ProxyOnScrollListener extends RecyclerView.OnScrollListener { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if(newState == RecyclerView.SCROLL_STATE_IDLE) { // 停止滑動 1.停止時的邏輯... }else if(newState == RecyclerView.SCROLL_STATE_DRAGGING){ // 按下拖動 2.剛剛拖動時的邏輯... } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // 滑動 3.滑動時的邏輯... } } } // ListView public class ListViewTabContainer extends BaseScrollableContainer<ListView> { ... @Override protected void setOnScrollListener() { mViewGroup.setOnScrollListener(new ProxyOnScrollListener()); ... } public class ProxyOnScrollListener implements AbsListView.OnScrollListener{ @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if(scrollState == SCROLL_STATE_IDLE) { // 停止滑動 1.停止時的邏輯... }else if(scrollState == SCROLL_STATE_TOUCH_SCROLL) // 按下拖動 2.剛剛拖動時的邏輯... } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 3.滑動時的邏輯... // 滑動 } } }
那該怎么辦呢,雖然各自的OnScrollListener差異挺大,但是仔細(xì)觀察可以發(fā)現(xiàn)其實很多邏輯都是類似的,可以共用的。這時恰恰可以用代理模式來做重構(gòu)。我抽取了1、2、3處的邏輯,由于在抽象意義上是一致的,可以整理成接口:
public interface OnScrollListener { // tab 點擊事件 void onClick(int position); // 1.滑動開始 void onScrollStart(); // 2.滑動結(jié)束 void onScrollStop(); // 3.觸發(fā) onScrolled() void onScrolled(); // 用戶手動滑, 觸發(fā)的 onScrolled() void onScrolledByUser(); // 程序調(diào)用 scrollTo(), 觸發(fā)的 onScrolled() void onScrolledByInvoked(); }
與此同時,RecyclerView和ListView各自的監(jiān)聽器便分別作為代理類,把1、2、3的邏輯都委托給某個接盤俠,不必自己去實現(xiàn),倒也落的輕松自在。如圖所示:這里寫圖片描述
然后,讓我們來看看這個接盤俠:RealOnScrollListener。。。
不愧是一個老實類,它老實地接盤了OnScrollListener的所有接口,并被兩個代理類Proxy…所持有(圖中并未畫出。。)。
具體實現(xiàn)就不貼了,大家可以下源碼來看。這里大致分析一下,它有三個成員:
public class RealOnScrollListener implements OnScrollListener { public boolean isTouching = false; // 處于觸摸狀態(tài) private int mCurPosition = 0; // 當(dāng)前選中項 private BaseViewGroupUtil<VG> mViewUtil; // ViewGroup 工具類 ... }
isTouching:
為啥要維護(hù)這個觸摸狀態(tài)呢?這是由于我們的效果是聯(lián)動的。這就比較討厭了,當(dāng)onScrolled()被調(diào)用,我們分不清是用戶的滑動,還是來自另一個列表滑動時的聯(lián)動效果。那我們記錄一下isTouching狀態(tài)呢,就能區(qū)分開這兩種情況了。
更改isTouching的邏輯在onScrollStart()和onScrollStop()里邊。
mCurPosition:
這個很好解釋,我們每次滑動需要記錄當(dāng)前位置,然后通知另一個列表進(jìn)行聯(lián)動。
這段邏輯在onScrolled()里邊。
mViewUtil:
一個工具庫,用于簡化邏輯。大概有scrollTo(),setViewSelected(),UpdatePosOnScrolled()等方法,如圖:
兩個Container之間的交互
之前都是對用戶的交互,終于到聯(lián)動部分了。不急著實現(xiàn),先回答我一個問題:假設(shè)我一個Activity里持有兩個Fragment,問它們之間如何通信?
A同學(xué)大聲道:用廣播
B同學(xué):EventBus !!!
C同學(xué):看我 RxBus 。。。
別鬧好嗎。。。給我老老實實用Listener。顯然,我們這里面臨的是同樣的場景。LinkedLayout=Activity,Container=Fragment。
動手前先定義Listener吧,要取個中二點的名字:
/* * 事件分發(fā)者 */ public interface EventDispatcher { /** * 分發(fā)事件: fromView 中的 pos 被選中 * @param pos * @param fromView */ void dispatchItemSelectedEvent(int pos, View fromView); } /* * 事件接受者 */ public interface EventReceiver { /** * 收到事件: 立即選中 newPos * @param newPos */ void selectItem(int newPos); }
然后LinkedLayout作為父級元素,肯定是分發(fā)者的角色,應(yīng)當(dāng)實現(xiàn)EventDispatcher;而BaseScrollableContainer作為子元素,接受該事件,應(yīng)當(dāng)實現(xiàn)EventReceiver??聪骂悎D:
看下相應(yīng)的實現(xiàn)(EventReceiver):
public abstract class BaseScrollableContainer<VG extends ViewGroup> implements EventReceiver { protected RealOnScrollListener mRealOnScrollListener; private EventDispatcher mEventDispatcher; // 持有分發(fā)者 ... public void setEventDispatcher(EventDispatcher eventDispatcher) { mEventDispatcher = eventDispatcher; } // 掉用 mEventDispatcher,也就是 LinkedLayout protected void dispatchItemSelectedEvent(int curPosition){ if(mEventDispatcher != null) mEventDispatcher.dispatchItemSelectedEvent(curPosition, mViewGroup); } @Override public void selectItem(int newPos) { mRealOnScrollListener.selectItem(newPos); } // OnScrollListener: 代理模式 public class RealOnScrollListener implements OnScrollListener { ... public void selectItem(int position){ mCurPosition = position; Log.d("setitem", position + ""); // 來自另一邊的聯(lián)動事件 mViewUtil.smoothScrollTo(position); // if(mViewUtil.isVisiblePos(position)) // curSection 可見時, 不滾動 mViewUtil.setViewSelected(position); } @Override public void onClick(int position) { isTouching = true; mViewUtil.setViewSelected(mCurPosition = position); dispatchItemSelectedEvent(position); // 點擊tab,分發(fā)事件 isTouching = false; } ... @Override public void onScrolled() { mCurPosition = mViewUtil.updatePosOnScrolled(mCurPosition); if(isTouching) // 來自用戶, 通知 對方 聯(lián)動 onScrolledByUser(); else // 來自對方, 被動滑動不響應(yīng) onScrolledByInvoked(); } @Override public void onScrolledByUser() { dispatchItemSelectedEvent(mCurPosition); // 來自用戶, 通知 對方 聯(lián)動 } } }
再看(EventDispatcher):
public class LinkedLayout extends LinearLayout implements EventDispatcher { private BaseScrollableContainer mTabContainer; private BaseScrollableContainer mContentContainer; private SectionIndexer mSectionIndexer; // 分組接口 ... @Override public void dispatchItemSelectedEvent(int pos, View fromView) { if (fromView == mContentContainer.mViewGroup) { // 來自 content, 轉(zhuǎn)發(fā)給 tab int convertPos = mSectionIndexer.getSectionForPosition(pos); mTabContainer.selectItem(convertPos); } else { // 來自 tab, 轉(zhuǎn)發(fā)給 content int convertPos = mSectionIndexer.getPositionForSection(pos); mContentContainer.selectItem(convertPos); } } }
總結(jié)
到此為止,有沒有一種酣暢淋漓的感覺?不管怎么說,面向?qū)ο笫切叛觯x好接口以后,實現(xiàn)起來怎么寫怎么舒服。
// TODO: 之前說了,這個聯(lián)動是通用的。之后有時間會繼續(xù)實現(xiàn)一個toolbar+viewPager的聯(lián)動…
彩蛋
高清無碼類圖:(完整)
相關(guān)文章
Android Notification實現(xiàn)動態(tài)顯示通話時間
這篇文章主要為大家詳細(xì)介紹了Android Notification實現(xiàn)動態(tài)顯示通話時間,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09Kotlin Service服務(wù)組件開發(fā)詳解
這幾天分析了一下的啟動過程,于是乎,今天寫一下Service使用; 給我的感覺是它并不復(fù)雜,千萬不要被一坨一坨的代碼嚇住了,雖然彎彎繞繞不少,重載函數(shù)一個接著一個,就向走迷宮一樣,但只要抓住主線閱讀,很快就能找到出口2022-12-12Android如何讓W(xué)ebView中的HTML5頁面實現(xiàn)視頻全屏播放
最近在工作遇到一個需求,需要讓W(xué)ebView中的HTML5頁面實現(xiàn)視頻全屏播放的效果,通過查找相關(guān)的資料終于找到了解決的方法,所以想著分享給大家,所以本文介紹了關(guān)于Android如何讓W(xué)ebView中的HTML5頁面實現(xiàn)視頻全屏播放的相關(guān)資料,需要的朋友可以參考學(xué)習(xí)。2017-04-04Android性能優(yōu)化之RecyclerView分頁加載組件功能詳解
這篇文章主要為大家介紹了Android性能優(yōu)化之RecyclerView分頁加載組件功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09Android入門之Activity四種啟動模式(standard、singleTop、singleTask、singl
當(dāng)應(yīng)用運行起來后就會開啟一條線程,線程中會運行一個任務(wù)棧,當(dāng)Activity實例創(chuàng)建后就會放入任務(wù)棧中。Activity啟動模式的設(shè)置在AndroidManifest.xml文件中,通過配置Activity的屬性android:launchMode=""設(shè)置2015-12-12