自定義組合控件:SherlockSpinner

如果你覺得從頭開始自定義一個(gè)View比較麻煩,那么組合幾個(gè)系統(tǒng)現(xiàn)有控件來實(shí)現(xiàn)需求的功能,會是你很好的一個(gè)選擇。

一、前言

最近在項(xiàng)目中,需要使用Spinner來實(shí)現(xiàn)下拉選擇功能,UI方面倒是要求不多。但是難點(diǎn)在于一個(gè)界面中有多個(gè)Spinner,并且有聯(lián)動關(guān)系,數(shù)據(jù)需要在點(diǎn)擊Spinner的時(shí)候請求服務(wù)器。而且當(dāng)前Spinner數(shù)據(jù)依賴于前面選擇的一個(gè)或多個(gè)結(jié)果,當(dāng)獲取到最新數(shù)據(jù)后,才顯示下拉選項(xiàng)。

比如說我需要先選擇一個(gè)倉庫,再選擇項(xiàng)目(依賴前面選擇的倉庫),再選擇一個(gè)批次(依賴前面選擇的倉庫和項(xiàng)目),如果我在選擇完倉庫時(shí),就去判斷來預(yù)加載項(xiàng)目和批次的數(shù)據(jù),會使依賴邏輯變得非常復(fù)雜。

這時(shí)候就想在每次點(diǎn)擊一個(gè)Spinner的時(shí)候,去判斷依賴的選項(xiàng)是否已經(jīng)選擇,未選擇就提示需先選擇;如果已選擇,則進(jìn)行網(wǎng)絡(luò)請求,加載數(shù)據(jù)顯示到下拉選項(xiàng)中。

(本例使用選擇語言來進(jìn)行演示。)

二、使用系統(tǒng)Spinner

1. 首先,我想到的便是使用系統(tǒng)的Spinner,說干就干,XML先上:

<Spinner
    android:id="@+id/spn_languages"
    android:layout_width="match_parent"
    android:layout_height="36dp"/>

2. 然后代碼設(shè)置Adapter匹配數(shù)據(jù)、設(shè)置OnItemSelectedListener綁定item選擇的事件:

spnLanguages = (Spinner) findViewById(R.id.spn_languages);
ArrayAdapter<String> mAdapterSystemSpinner = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
spnLanguages.setAdapter(mAdapterSystemSpinner);
spnLanguages.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        showMessage("Select Nothing.");
    }
});

OK,現(xiàn)在Spinner可以使用了:

3. 接著我們來設(shè)置點(diǎn)擊事件

spnLanguages.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        showMessage("Click Spinner");
    }
});

什么情況,運(yùn)行后直接崩了?

FATAL EXCEPTION: main
Process: com.sherlockshi.widget.sherlockspinner, PID: 11757
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sherlockshi.widget.sherlockspinner/com.sherlockshi.widget.MainActivity}: java.lang.RuntimeException: Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead
   ...
Caused by: java.lang.RuntimeException: Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead
   at android.widget.AdapterView.setOnClickListener(AdapterView.java:798)
   at com.sherlockshi.widget.MainActivity.initSystemSpinner(MainActivity.java:169)
   at com.sherlockshi.widget.MainActivity.onCreate(MainActivity.java:48)
   ...

簡單來說就是,AdapterView不能設(shè)置Click事件,看下Spinner源碼,確實(shí)是繼承自AdapterView。(至于為什么AdapterView不能設(shè)置Click事件,這個(gè)暫未深究。)

4. 那我們就設(shè)置Touch事件嘍:

spnLanguages.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                showMessage("Touch Spinner");
                break;
        }
        return false;
    }
});

這下確實(shí)是可以響應(yīng)點(diǎn)擊事件了,但是響應(yīng)完,下拉選擇就直接顯示出來了,無法滿足我們的需求。

而且Spinner還有個(gè)問題,一進(jìn)入界面的時(shí)候,默認(rèn)就會選擇第一項(xiàng),而我們并不需要這樣的默認(rèn)值。

二、CustomSpinner = EditText + ListPopupWindow

那我們是否可以使用別的控件,來實(shí)現(xiàn)相同的功能呢?答案是肯定的,我們用EditText來接收點(diǎn)擊事件,而在請求完數(shù)據(jù)之后,使用ListPopupWindow來顯示下拉選項(xiàng),選擇EditText的原因主要有以下幾點(diǎn):

  • 默認(rèn)在底部會有帶顏色的橫線,Material Design風(fēng)格的EditText看起來效果很不賴
  • EditText可以方便的配置上、下、左、右四個(gè)位置的小圖標(biāo),我們可以在右側(cè)放置一個(gè)向下的三角箭頭,使它看起來像一個(gè)系統(tǒng)的Spinner

而選擇ListPopupWindow則是因?yàn)椋?/p>

  • 可以方便的使用下拉列表
  • 可以自由設(shè)置錨點(diǎn)

1. XML布局

布局文件依舊很簡單,只要一個(gè)簡單的EditText,配上一個(gè)右側(cè)的下拉圖標(biāo):

<EditText
    android:id="@+id/et_languages"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="right"
    android:drawableRight="@drawable/ic_dropdown"
    android:hint="Please Select..."/>

2. 代碼中創(chuàng)建ListPopupWindow

此部分包含以下邏輯:

  • 初始化ListPopupWindow,并關(guān)聯(lián)到EditText上
  • 當(dāng)點(diǎn)擊EditText時(shí),請求數(shù)據(jù),請求完成后,顯示ListPopupWindow
  • 選中ListPopupWindow的某一項(xiàng)后,將此項(xiàng)內(nèi)容更新到EditText中,并隱藏ListPopupWindow
etLanguages = (EditText) findViewById(R.id.et_languages);
etLanguages.setKeyListener(null); // 設(shè)置EditText不可編輯,等同于在xml中設(shè)置editable="false"
lpwLanguages = new ListPopupWindow(this);
mAdapterLanguages = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
lpwLanguages.setAdapter(mAdapterLanguages);
lpwLanguages.setAnchorView(etLanguages); //設(shè)置ListPopupWindow的錨點(diǎn),即關(guān)聯(lián)PopupWindow的顯示位置
lpwLanguages.setModal(true); // 是否為模態(tài),當(dāng)設(shè)置為true時(shí),會處理返回按鍵的事件

lpwLanguages.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);

        etLanguages.setText(mLanguages[position]);
        lpwLanguages.dismiss();
    }
});

// 如果使用onClick事件,會出現(xiàn)第一次點(diǎn)擊只獲取焦點(diǎn),第二次點(diǎn)擊才出現(xiàn)下拉
etLanguages.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // Do what you want
            getDataFromNet();
        }
        return false;
    }
});

public void getDataFromNet() {
    // 延時(shí)2秒后,修改源數(shù)據(jù),用來模擬網(wǎng)絡(luò)請求
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            mLanguages[1] = "C+++++++++++++";
            mAdapterLanguages.notifyDataSetChanged();

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    lpwLanguages.show();
                }
            });
        }
    }).start();
}

實(shí)現(xiàn)效果如下所示:

3. 小結(jié)

EditText配合ListPopupWindow組合實(shí)現(xiàn)Spinner的功能,使用起來倒是簡單,邏輯也挺清晰的,但是如果界面上有三四個(gè)Spinner,那不是就得把類似的代碼寫上三四遍?

其實(shí)我們并不關(guān)心內(nèi)部是用ListPopupWindow或者其它的控件來實(shí)現(xiàn),也不想處理任何關(guān)于ListPopupWindow的細(xì)節(jié)。我們關(guān)心的只有Spinner的初始化、適配數(shù)據(jù)、Item選擇事件(ItemClick事件),如果可以,就再加上Spinner的點(diǎn)擊事件(Click或Touch事件)、自由控制Spinner的顯示時(shí)機(jī)。

那有沒有簡單易用的方法,可以直接像使用系統(tǒng)的Spinner一樣,來使用EditText和ListPopupWindow的組合呢?并且可以提供Spinner的點(diǎn)擊事件?答案是肯定的,詳情且看下一節(jié)。

三、自定義組合控件

在第二部分我們可以看出,我們的控件要滿足以下兩大功能:

  1. 像系統(tǒng)Spinner一樣簡單易用:初始化、適配數(shù)據(jù)、Item選擇事件(ItemClick事件)
  2. 支持點(diǎn)擊事件(Click或Touch事件),自由控制下拉框顯示時(shí)機(jī)

由于控件源碼稍長,就不貼出來了,有興趣可以點(diǎn)擊文末的Github鏈接,源碼也比較簡單,只是進(jìn)行控件的組合,并提供相應(yīng)的方法進(jìn)行調(diào)用,下面主要介紹下使用方法。

1. 引入依賴

dependencies {
    ...
    compile 'com.sherlockshi.widget:sherlockspinner:1.0.2'
}

2. 使用方法

2.1 像使用系統(tǒng)Spinner一樣,在XML文件中使用:

<com.sherlockshi.widget.SherlockSpinner
    android:id="@+id/sherlock_spinner"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:lineColor="#00FF00"
    android:hint="Please Select..."/>

SherlockSpinner有以下屬性:
lineColor: 設(shè)置底部橫線的顏色
同時(shí)支持使用代碼進(jìn)行配置:mSherlockSpinner.setLineColor(0x00FF00);

由于SherlockSpinner繼承自EditText, 所以你可以使用EditText的其它屬性,例如gravitytextSize、textColor...

2.2 還是像使用系統(tǒng)Spinner一樣,在代碼中設(shè)置AdapterItemClickListener:

mSherlockSpinner = (SherlockSpinner) findViewById(R.id.sherlock_spinner);
ArrayAdapter<String> mAdapterLanguages = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mLanguages);
mSherlockSpinner.setAdapter(mAdapterLanguages);
mSherlockSpinner.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        showMessage("Select " + mLanguages[position]);
    }
});

以上,就可以像系統(tǒng)Spinner一樣使用SherlockSpinner了,如果沒有別的需求,這樣也就夠用了。如果你有點(diǎn)擊請求網(wǎng)絡(luò)數(shù)據(jù),再異步顯示下拉框的需求,可以看第3步的使用方法。

2.3 (可選) 如果你想在異步加載數(shù)據(jù)后,再顯示出更新后的數(shù)據(jù),你可以使用它的點(diǎn)擊事件來處理

記?。?/code>在獲取數(shù)據(jù)后,你必須手動調(diào)用sherlockSpinner.show()方法來顯示SherlockSpinner的下拉選項(xiàng)

mSherlockSpinner.setOnClickListener(new SherlockSpinner.OnClickListener() {
    @Override
    public void onClick(View v) {
        getDataFromNet();
    }
});

public void getDataFromNet() {
    // after delay 2s, modify the source data, to simulate net request
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            mLanguages[4] = "Javaaaaaaaaaaa";
            mAdapterLanguages.notifyDataSetChanged();

            // then you must manually call sherlockSpinner.show()
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mSherlockSpinner.show();
                }
            });
        }
    }).start();
}

3. 更多屬性

  • 由于SherlockSpinner繼承自EditText,所以你可以使用EditText的其它屬性,例如gravity、textSize、textColor...
  • SherlockSpinner還有一個(gè)屬性,可以設(shè)置下拉框的顯示位置,即錨點(diǎn)設(shè)置:
mSherlockSpinner.setAnchorView(findViewById(R.id.llyt_anchor));

效果如下圖中4和5的區(qū)別,在第4部分中,下拉框停靠在Spinner上;而第5部分中,下拉框??吭赟pinner所在的整行布局上,寬度更大。

四、其它

另外,一個(gè)小小的Tip:
在styles.xml文件中配置以下代碼,可使下拉框帶上漂亮的分割線:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="android:dropDownListViewStyle">@style/mySpinnerStyle</item>
</style>

<style name="mySpinnerStyle" parent="android:Widget.ListView.DropDown">
    <item name="android:divider">#E0E0E0</item>
    <item name="android:dividerHeight">0.1dp</item>
</style>

項(xiàng)目代碼已共享到Github:SherlockSpinner
歡迎fork、star、issue。

PS:歡迎關(guān)注SherlockShi博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 47,144評論 22 665
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,922評論 25 709
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,317評論 0 17
  • 都說這個(gè)時(shí)候的日子是我們最美好的時(shí)候,我們?nèi)蘸蟊貢涯?,可是現(xiàn)在的我,感覺好難熬,每天自殺這個(gè)念頭無數(shù)遍從腦中浮現(xiàn)...
    花橙君閱讀 194評論 0 0
  • 安安靜靜地從內(nèi)心發(fā)出聲音 讓自己健健康康的 美美的滋養(yǎng)
    心源寶貝閱讀 292評論 0 0

友情鏈接更多精彩內(nèi)容