1. 程式人生 > 實用技巧 >Android 多級聯動控制元件實現思路討論

Android 多級聯動控制元件實現思路討論

最近有一個需求是選擇多級聯動資料,資料級別不固定,可能是五級,可能是兩級,具體看使用者等級。

所以就需要一個多級聯動選擇控制元件 ,在網上一番搜尋或找到了這個控制元件,Android-PickerView

這個控制元件在三級以內的的聯動都沒有問題,但是最多隻能到三級。

我在原有的基礎上做了一些擴充套件,主要是添加了兩個 picker

  • MultiWheelPickerView 可以根據資料動態生成多個滾輪,不再侷限於兩個三個選項
  • DynamicWheelPickerView 也是動態生成,但可以一級一級的載入資料並追加滾輪。

在使用時,根據自身情況讓你的 JavaBean 實現 IWheelItem 或者 IDynamicWheelItem 就好。

這裡記錄並分享一下我的思路和實現,也希望能和大家一起討論更好的實現方案。

起初,只是想根據獲取到的資料動態的生成滾輪,有多少級就生成多少個,自動排列出來就好。

在看了原始碼後發現原來的 OptionsPickerView 裡寫死了三個 WheelView ,所以最多隻能是三個。

如果想動態生成 WheelView 就不能寫死,只能根據資料生成,所以我選擇使用程式碼建立 WheelView,不使用 layout 佈局固定數量了。

除了 WheelView 部分外,其他部分還都是使用原來的佈局。

因為要動態顯示資料,就不能使用原來的 IPickerViewData了,使用了一個新的 IWheelItem

public interface IWheelItem {

    /**
     *
     * @return 顯示在滾輪的文字
     */
    String getShowText();

    /**
     *
     * @return 下一級的資料
     */
    <T extends IWheelItem> List<T> getNextItems();

}

只有兩個方法,返回顯示資料用來顯示在滾輪上;在選擇了一級後自動獲取下一級內容顯示。

這種多級聯動的資料,明顯有著上下級關係,我就預設為這種結構了,一級套著一級。

並在 WheelView 裡做了調整

  /**
     * 獲取所顯示的資料來源
     *
     * @param item data resource
     * @return 對應顯示的字串
     */
    private String getContentText(Object item) {
        if (item == null) {
            return "";
        } else if (item instanceof IPickerViewData) {
            return ((IPickerViewData) item).getPickerViewText();
        } else if (item instanceof Integer) {
            //如果為整形則最少保留兩位數.
            return getFixNum((int) item);
        }else if (item instanceof IWheelItem){
            return ((IWheelItem)item).getShowText();
        }
        return item.toString();
    }

First of all, 確定資料的層級,根據層級決定生成 WheelView 的數量。

   /**
     * 獲取當前 list 的層級,最深有多少層
     * 需要根據層級確定多少個滾輪
     * @param list 資料
     * @return 最深層級
     */
    private int getLevel(List<T> list) {
        int level = 0;
        if (list != null && list.size() > 0) {
            level = 1;
            int childLevel = 0;
            for (T code : list) {
                List<T> children =code.getNextItems();
                int temp = getLevel(children);
                if (temp > childLevel) {
                    childLevel = temp;
                }
            }
            level += childLevel;
        }
        return level;
    }

我使用的是一個 LinearLayout 橫向排列,用來承載動態生成的 WheelView 。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <include
        layout="@layout/include_pickerview_topbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/pickerview_topbar_height" />

    <LinearLayout
        android:id="@+id/ll_multi_picker"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:gravity="center"
        android:minHeight="180dp"
        android:orientation="horizontal">
    </LinearLayout>
</LinearLayout>

注意:這裡有一個問題就是,如果生成的滾輪很多,會顯得比較擁擠。

知道了要生成多少個滾輪後,程式碼建立直接新增到 LinearLayout 裡了。

int level =getLevel(wheelItems);
if (level > 0) {
    //生成 滾輪
    for (int i = 0; i < level; i++) {
        WheelView wheelView = generateWheel();
        mLlContainer.addView(wheelView);
    }
    //為滾輪賦值 ,都取第一個賦值
    initWheel(wheelItems, 0);
}

生成 WheelView 之後,就是給控制元件賦值了,我這裡預設取第一個當做選中的值。

只要前邊一級選中了,那就獲取它的下一級資料給下一個控制元件賦值,如此遞迴到最後一個。

    protected void initWheel(List<T> list, int wheelIndex) {
        WheelView wheelView = (WheelView) mLlContainer.getChildAt(wheelIndex);
        if (null == wheelView) {
            Log.d(MultiWheelPickerView.class.getSimpleName(), "initWheel: 超出了範圍 " + wheelIndex + " > " + mLlContainer.getChildCount());
            return;
        }
        if (null != list && list.size() > 0) {
            wheelView.setAdapter(new MultiWheelAdapter(list));
            wheelView.setCurrentItem(0);
            wheelView.setOnItemSelectedListener(new MultiWheelItemSelector(list, wheelIndex));
            //預設選中第一項,新增到結果裡。
            T wheelItem = list.get(0);
            addToResult(wheelItem, wheelIndex);
            List<T> children = list.get(0).getNextItems();
            //有子集,繼續新增
            wheelIndex++;
            initWheel(children, wheelIndex);
        }else{
            for (int i=wheelIndex;i<mLlContainer.getChildCount();i++){
                wheelView = (WheelView) mLlContainer.getChildAt(i);
                wheelView.setAdapter(new MultiWheelAdapter(null));
            }
        }
    }

關於選中的資料和事件,和原來一樣,只是換了一種形式,使用 List 容器。

按照順序,把選中的資料都列在裡面了,邏輯如下

   protected void addToResult(T value, int index) {
        //  檢測是否發生了變化,需要對外釋放訊號
        int size = resultList.size();
        Log.d(MultiWheelPickerView.class.getSimpleName(), "addToResult: " + index + "-->" + value + "; size->" + size);
        //上級換了人,下級全部移除掉
        while (index < size) {
            resultList.remove(index);
            size = resultList.size();
        }
        //已經把之後的刪除了,直接新增就行了
        boolean isAddToResult =true;
        if (null!=listener){
        // 這裡可以從外部判斷是否可以選擇,有的 是不需要選擇的,例如 all, 或者 “”
            isAddToResult = listener.isAddToResult(value);
        }
        if (isAddToResult) {
            resultList.add(value);
        }
        if (null!=listener){
            listener.onChange(resultList);
        }
    }

就這樣稍微改一改,一個動態多級關聯控制元件就有了,在使用時,讓你的 JavaBean 實現 IWheelItem 就好。

簡單使用方式如下

   MultiWheelPickerView<CodeTable> fixedPickerView;

    private void fixedPicker() {
        if (null == fixedPickerView) {
            MultiWheelPickerBuilder<CodeTable> builder = new MultiWheelPickerBuilder<>(this,
                    new MultiWheelSelectListener<CodeTable>() {
                        @Override
                        public void onChange(List<CodeTable> result) {
                            //在滾輪選擇發生變化時會被呼叫
                            showChange(result);
                        }

                        @Override
                        public void onSelect(List<CodeTable> result) {
                            //在按下確定按鈕時會被呼叫
                            StringBuffer buffer = new StringBuffer();
                            int size = result.size();
                            for (int i = 0; i < size; i++) {
                                if (i != 0) {
                                    buffer.append("->");
                                }
                                buffer.append(result.get(i).getShowText());
                            }
                            mTvResult.setText(buffer.toString());
                        }

                        @Override
                        public boolean isAddToResult(CodeTable selectValue) {
                            //此方法返回值會確定這個值是否可以被選中
                            return !selectValue.getCode().equalsIgnoreCase("all");
                        }
                    });
            fixedPickerView = builder.build();
            fixedPickerView.setTitleText("行政區劃");
            fixedPickerView.setWheelItems(getPickerData());
        }
        fixedPickerView.show();
    }


雖然實現了多級聯動,但是在實際使用時又發現了不可忽視的問題:
如果資料過多,就會載入很長時間,從省級到村級,會有數萬條記錄,一次獲取過來體驗太差了,而且有崩潰的風險。

更好的辦法是一級一級的去獲取資料,選中省級再去獲取下屬的市級並追加滾輪顯示,選中市級再去獲取縣級,如此類推。

So, 接續改,因為資料也是多次獲取了,就無法確定層級了,故需要每有新的層級時新增新的 WheelView 追加到顯示容器裡(突然增加一個View會出現橫跳的情況,最好是加入一個動畫平滑一點)。

在選中一個數據時,也要判斷是否需要去載入下一級,在我的需求裡,有的是需要到村級,有的則需要到縣級。

所以具體是否要載入下一級的配置要放出來,我這裡放在了資料介面上,由資料自身判斷。

在 IWheelItem 的基礎上擴充套件了一個 IDynamicWheelItem

public interface IDynamicWheelItem extends IWheelItem {
    /**
     * @return 是否需要載入下一級
     */
    boolean isLoadNext() ;
}

然後是在生成 WheelView 這裡做了一些修改,根據傳入的資料生成。

也是預設選擇了第一項,如果能被選中,則繼續生成或者去載入子級資料。

    protected void generateWheel(List<T> data) {
        if (data != null && data.size() > 0) {
            //需要生成 wheel
            WheelView wheelView = generateWheel();
            wheelView.setAdapter(new ArrayWheelAdapter(data));
            mLlContainer.addView(wheelView);
            int level = mLlContainer.getChildCount() - 1;
            wheelView.setOnItemSelectedListener(new DynamicWheelItemSelector(data, level));
            T iWheelItem = data.get(0);
            addToResult(iWheelItem, level);
            if (canSelect(iWheelItem)) {
                List<T> nextItems = iWheelItem.getNextItems();
                if (null != nextItems && nextItems.size() > 0) {
                    generateWheel(nextItems);
                } else {
                    if (iWheelItem.isLoadNext()) {
                        loadNext(iWheelItem, ++level);
                    }
                }
            }

        }
    }

在選中一個數據後的滾輪賦值也做了修改,如果是判斷是否需要去載入下一級資料或者是否現有資料

在後續沒有資料的情況下,也沒有移除掉 WheelView 。一旦沒有資料就移除,會出現左右橫跳的情況(這裡也可以做一個動畫,會顯得沒有那麼突兀)。

    /**
     * 設定下級Wheel 的資料
     *
     * @param current 資料
     * @param nextLevel   下一層
     */
    private void setupChildWheel(T current, int nextLevel) {
        if (mLlContainer.getChildCount() == nextLevel) {
            if (current.isLoadNext()) { //最後一級了,但是下一級仍然需要顯示
                loadNext(current, nextLevel);
            }
            return;
        }
        List<T> nextItems = current.getNextItems();
        //對於下級wheel的設定上對應的資料,即使沒有那麼多級的,也不能移除view,只能將資料設定為null
        WheelView wheelView = (WheelView) mLlContainer.getChildAt(nextLevel);
        if (null != nextItems && nextItems.size() > 0) {
            //有子集
            //在 level ==count 時可能為空
            if (wheelView == null) {
                wheelView = generateWheel();
            }
            wheelView.setAdapter(new ArrayWheelAdapter(nextItems));
            wheelView.setCurrentItem(0);
            wheelView.setOnItemSelectedListener(new DynamicWheelItemSelector(nextItems, nextLevel));
            T wheelItem = nextItems.get(0);
            addToResult(wheelItem, nextLevel);
            nextLevel++;
            if (canSelect(wheelItem)) {
                setupChildWheel(wheelItem, nextLevel);
            }else{ //當前已經不能選擇了,之後的滾輪資料也必須置空
                for (int i = nextLevel; i < mLlContainer.getChildCount(); i++) {
                    wheelView = (WheelView) mLlContainer.getChildAt(i);
                    wheelView.setOnItemSelectedListener(null);
                    wheelView.setAdapter(new MultiWheelAdapter(null));
                }
            }
        } else {
            //還需要判斷是否需要再次去獲取子集。
            //沒有子集 全部置空
            for (int i = nextLevel; i < mLlContainer.getChildCount(); i++) {
                wheelView = (WheelView) mLlContainer.getChildAt(i);
                wheelView.setOnItemSelectedListener(null);
                wheelView.setAdapter(new MultiWheelAdapter(null));
            }
            //沒有資料,需要去載入
            if (canSelect(current)&&current.isLoadNext()) {
                loadNext(current, nextLevel);
            }
        }
    }

在載入資料成功後,要將資料追加到對應的滾輪上

    public void appendWheel(List<T> list, int level) {
        WheelView wheelView = null;
        if (level < mLlContainer.getChildCount()) {
            wheelView = (WheelView) mLlContainer.getChildAt(level);
        } else {
            wheelView = generateWheel();
            if (null != list && list.size() > 0)
                mLlContainer.addView(wheelView);
        }
        if (null != list && list.size() > 0) {
            wheelView.setAdapter(new MultiWheelAdapter(list));
            wheelView.setCurrentItem(0);
            T codeTable = list.get(0);
            addToResult(codeTable,level);
            wheelView.setOnItemSelectedListener(new DynamicWheelItemSelector(list, level));
            if (canSelect(codeTable)) { //合法資料,能被選擇。
                //需要載入下一級
                level++;
                setupChildWheel(codeTable,level);
            }

        }
    }

至此,改完了,比之前那個多放出來兩個方法。

在偵聽器裡擴充套件了一個載入下級的方法。

public interface DynamicWheelSelectListener<T extends IDynamicWheelItem>extends MultiWheelSelectListener<T> {
    /**
     * 載入下一級的資料
     * @param item 當前資料
     * @param nextLevel 下一級的層級
     */
    void loadNextItems(T item, int nextLevel);
}

使用辦法和上面的 MultiWheelPickerView 大同小異

   DynamicWheelPickerView<CodeTable> dynamicPickerView;
    private void dynamicPicker() {
        if (null == dynamicPickerView) {
            dynamicPickerView =new DynamicWheelPickerBuilder<CodeTable>(this,new DynamicWheelSelectListener<CodeTable>() {
                @Override
                public void loadNextItems(CodeTable item, int nextLevel) {
                    //這裡模擬的資料,在載入後將 isLoadNext 設定為 false。
                    List<CodeTable> child = getChild(random());
                    item.setChildren(child);
                    item.setLoadNext(false);
                    //將資料賦值到對應的控制元件上,nextLevel就是控制元件的位置。
                    dynamicPickerView.appendWheel(child, nextLevel);
                }

                @Override
                public void onChange(List<CodeTable> result) {
                    showChange(result);
                }

                @Override
                public void onSelect(List<CodeTable> result) {
                    StringBuffer buffer = new StringBuffer();
                    int size = result.size();
                    for (int i = 0; i < size; i++) {
                        if (i != 0) {
                            buffer.append("->");
                        }
                        buffer.append(result.get(i).getShowText());
                    }
                    mTvResult.setText(buffer.toString());
                }

                @Override
                public boolean isAddToResult(CodeTable selectValue) {
                    //是 0 的不能被選擇
                    return !selectValue.getCode().equalsIgnoreCase("0");
                }
            })
                    .build();
            dynamicPickerView.setTitleText("行政區劃");
            dynamicPickerView.setWheelItems(getChild(random()));

        }
        dynamicPickerView.show();
    }

具體用法可以看程式碼,在這裡 TestMultiWheelActivity


其他想法:

  • 目前使用 LinearLayout 包裹的,是否可以換成 RecyclerView 呢,是否能更好的控制在一行超出多少個後換行,避免擁擠。
  • 目前在動態追加滾輪時是很生硬的追加上去的,可以優化為使用動畫平滑的過渡可能體驗更好些。

目前把程式碼放在了這裡 Android-PickerView

我的實現方式就是這樣,希望能和大家討論更好的方式。