Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android 二級聯動列表,仿eleme點餐頁面

android 二級聯動列表,仿eleme點餐頁面

編輯:關於Android編程

寫在前面


最近手感不錯,老想寫點輪子。正好周末外賣點得多,就仿一仿“餓了麼”好了。先上圖吧,這樣的訂單頁面是不是很眼熟:

這裡寫圖片描述

右邊的listview分好組以後,在左邊的Tab頁建立索引。可以直接導航,是不是很方便。關鍵在於右邊滑動,左邊也會跟著滑;而點擊左邊呢,也能定位右邊的項。它們存在這樣一種特殊的交互。像這種聯動的效果,還有些常見的例子呢,比如知乎采用了常見的toolbar+viewPager的聯動,只不過是上下布局:

這裡寫圖片描述

再看看點評,它的城市選擇頁面也有這種聯動的影子,只是稍微弱一點。側邊欄可以對listview進行索引,這最早是在微信好友列表裡出現的把:

這裡寫圖片描述

趁著周末,我也撸一個。就拓展性而言,應該可以適配以上所有情況吧。我稱其為LinkedLayout,看下效果圖:

這裡寫圖片描述

我把右邊按5個一組,可以看到,左邊的索引 = 右邊/5
 

特點


右邊滑動,左邊跟著動 左邊滑動到邊界,右邊跟著動 點擊左邊tab項,右邊滑動定位到相應的group

源碼


github 傳送門: https://github.com/fashare2015/LinkedScrollDemo
 

知識點


做之前先羅列一下知識點,或者說我們能從這個demo裡收獲到什麼。

面向抽象/接口編程 自定義 view 代理模式 UML類圖 復習 listview && recyclerview 的細節

感覺做完以後收獲最大的還是第一點,面向接口編程。事實上,完成功能的時間只占了一半,後邊的時間一直在抽象和重構;哎,一步到位太難了,還是老老實實寫具體類,再抽取基類把。
 

構思


UI部分

LinkedLayout

要做的呢是兩個相互關聯的列表,在左邊的作為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呢,當然是致敬Fragment啦。我們來定義一下這個類:
初步一想,無非有一個 mContext, 一個 viewGroup, 還有一些 Listener 嘛:

這裡寫圖片描述

public abstract class BaseScrollableContainer {
    protected Context mContext;
    public VG mViewGroup;

    protected RealOnScrollListener mRealOnScrollListener;
    private EventDispatcher mEventDispatcher;
    ...
}

和我們預想的差不多嘛,mContext上下文,mViewGroup基本就是指代我們的兩個listview了吧。當然,我之後可是要做toolbar+viewpager的,肯定得依賴抽象,不能直接寫listview啦。余下兩個是Listener,等我們界面搭好,寫交互的時候在看把。
看來UML圖還是有好處的,繼承和依賴關系一目了然。

自定義View && 動態布局

好了到了自定義view地環節了。我們已經有了一個LinkedLayout,這是我們的activity_main.xml布局代碼:




    

擦,就沒了嘛?剩下的得靠java代碼來搞啦。回到LinkedLayout咱們來布局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);

        // 設置 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已經布局好了,我們分別注入ViewGroup就可以用了。我這裡分別用listview作tab,recyclerview作content。想像力有限,用來用去好像也就這麼幾個控件。。。這部分代碼很簡單,在MainActivity裡,就不貼了。

子類化 BaseScrollableContainer

按照常理,下邊應該實現基類了吧。前面的MainActivity中,我們是這樣實例化的:

mTabContainer = new ListViewTabContainer(this, mListView);
mContentContainer = new RecyclerViewContentContainer(this, mRecyclerView);

看名字一個是listview填充的tab,一個是recyclerview填充的content。就先實現這兩個類吧,從圖中可以看到,它們分別繼承於BaseScrollableContainer,並被LinkedLayout所持有:
這裡寫圖片描述
 

交互部分

與用戶的交互:OnScrollListener 與 代理模式

終於到了交互部分,既然是滑動,那少不了定義監聽器啦。然而,麻煩在於listview和recyclerview各自的OnScrollListener還不一樣,這個時候如果各自實現的話,既麻煩,又有冗余。像這樣子:

// RecyclerView
public class RecyclerViewContentContainer extends BaseScrollableContainer {
    ...
    @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 {
    ...
    @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差異挺大,但是仔細觀察可以發現其實很多邏輯都是類似的,可以共用的。這時恰恰可以用代理模式來做重構。我抽取了1、2、3處的邏輯,由於在抽象意義上是一致的,可以整理成接口:

public interface OnScrollListener {
    // tab 點擊事件
    void onClick(int position);

    // 1.滑動開始
    void onScrollStart();

    // 2.滑動結束
    void onScrollStop();

    // 3.觸發 onScrolled()
    void onScrolled();

    // 用戶手動滑, 觸發的 onScrolled()
    void onScrolledByUser();

    // 程序調用 scrollTo(), 觸發的 onScrolled()
    void onScrolledByInvoked();
}

與此同時,RecyclerView和ListView各自的監聽器便分別作為代理類,把1、2、3的邏輯都委托給某個接盤俠,不必自己去實現,倒也落的輕松自在。如圖所示:這裡寫圖片描述

然後,讓我們來看看這個接盤俠:RealOnScrollListener。。。
不愧是一個老實類,它老實地接盤了OnScrollListener的所有接口,並被兩個代理類Proxy…所持有(圖中並未畫出。。)。
具體實現就不貼了,大家可以下源碼來看。這裡大致分析一下,它有三個成員:

public class RealOnScrollListener implements OnScrollListener {
    public boolean isTouching = false;  // 處於觸摸狀態
    private int mCurPosition = 0;       // 當前選中項
    private BaseViewGroupUtil mViewUtil; // ViewGroup 工具類
    ...
}

isTouching:
為啥要維護這個觸摸狀態呢?這是由於我們的效果是聯動的。這就比較討厭了,當onScrolled()被調用,我們分不清是用戶的滑動,還是來自另一個列表滑動時的聯動效果。那我們記錄一下isTouching狀態呢,就能區分開這兩種情況了。
更改isTouching的邏輯在onScrollStart()和onScrollStop()裡邊。

mCurPosition:
這個很好解釋,我們每次滑動需要記錄當前位置,然後通知另一個列表進行聯動。
這段邏輯在onScrolled()裡邊。

mViewUtil:
一個工具庫,用於簡化邏輯。大概有scrollTo(),setViewSelected(),UpdatePosOnScrolled()等方法,如圖:
這裡寫圖片描述
 

兩個Container之間的交互

之前都是對用戶的交互,終於到聯動部分了。不急著實現,先回答我一個問題:假設我一個Activity裡持有兩個Fragment,問它們之間如何通信?

A同學大聲道:用廣播 B同學:EventBus !!! C同學:看我 RxBus 。。。

別鬧好嗎。。。給我老老實實用Listener。顯然,我們這裡面臨的是同樣的場景。LinkedLayout=Activity,Container=Fragment。
動手前先定義Listener吧,要取個中二點的名字:

/*
 * 事件分發者
 */
public interface EventDispatcher {
    /**
     * 分發事件: fromView 中的 pos 被選中
     * @param pos
     * @param fromView
     */
    void dispatchItemSelectedEvent(int pos, View fromView);
}

/*
 * 事件接受者
 */
public interface EventReceiver {
    /**
     * 收到事件: 立即選中 newPos
     * @param newPos
     */
    void selectItem(int newPos);
}

然後LinkedLayout作為父級元素,肯定是分發者的角色,應當實現EventDispatcher;而BaseScrollableContainer作為子元素,接受該事件,應當實現EventReceiver。看下類圖:
這裡寫圖片描述

看下相應的實現(EventReceiver):

public abstract class BaseScrollableContainer
        implements EventReceiver {
    protected RealOnScrollListener mRealOnScrollListener;
    private EventDispatcher mEventDispatcher; // 持有分發者
    ...
    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 + "");
            // 來自另一邊的聯動事件
            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,分發事件
            isTouching = false;
        }
        ...
        @Override
        public void onScrolled() {
            mCurPosition = mViewUtil.updatePosOnScrolled(mCurPosition);
            if(isTouching)          // 來自用戶, 通知 對方 聯動
                onScrolledByUser();
            else                    // 來自對方, 被動滑動不響應
                onScrolledByInvoked();
        }

        @Override
        public void onScrolledByUser() {
            dispatchItemSelectedEvent(mCurPosition);    // 來自用戶, 通知 對方 聯動
        }
    }
}

再看(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, 轉發給 tab
            int convertPos = mSectionIndexer.getSectionForPosition(pos);
            mTabContainer.selectItem(convertPos);
        } else {                    // 來自 tab, 轉發給 content
            int convertPos = mSectionIndexer.getPositionForSection(pos);
            mContentContainer.selectItem(convertPos);
        }
    }
}

 

總結


到此為止,有沒有一種酣暢淋漓的感覺?不管怎麼說,面向對象是信仰,定義好接口以後,實現起來怎麼寫怎麼舒服。
// TODO: 之前說了,這個聯動是通用的。之後有時間會繼續實現一個toolbar+viewPager的聯動…

彩蛋


高清無碼類圖:(完整)

這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved