如果你覺(jué)得從頭開(kāi)始自定義一個(gè)View比較麻煩,那么組合幾個(gè)系統(tǒng)現(xiàn)有控件來(lái)實(shí)現(xiàn)需求的功能徒恋,會(huì)是你很好的一個(gè)選擇。
一欢伏、前言
最近在項(xiàng)目中入挣,需要使用Spinner來(lái)實(shí)現(xiàn)下拉選擇功能,UI方面倒是要求不多硝拧。但是難點(diǎn)在于一個(gè)界面中有多個(gè)Spinner径筏,并且有聯(lián)動(dòng)關(guān)系葛假,數(shù)據(jù)需要在點(diǎn)擊Spinner的時(shí)候請(qǐng)求服務(wù)器。而且當(dāng)前Spinner數(shù)據(jù)依賴于前面選擇的一個(gè)或多個(gè)結(jié)果滋恬,當(dāng)獲取到最新數(shù)據(jù)后聊训,才顯示下拉選項(xiàng)。
比如說(shuō)我需要先選擇一個(gè)倉(cāng)庫(kù)恢氯,再選擇項(xiàng)目(依賴前面選擇的倉(cāng)庫(kù))魔眨,再選擇一個(gè)批次(依賴前面選擇的倉(cāng)庫(kù)和項(xiàng)目),如果我在選擇完倉(cāng)庫(kù)時(shí)酿雪,就去判斷來(lái)預(yù)加載項(xiàng)目和批次的數(shù)據(jù)遏暴,會(huì)使依賴邏輯變得非常復(fù)雜。
這時(shí)候就想在每次點(diǎn)擊一個(gè)Spinner的時(shí)候指黎,去判斷依賴的選項(xiàng)是否已經(jīng)選擇朋凉,未選擇就提示需先選擇;如果已選擇醋安,則進(jìn)行網(wǎng)絡(luò)請(qǐng)求杂彭,加載數(shù)據(jù)顯示到下拉選項(xiàng)中。
(本例使用選擇語(yǔ)言來(lái)進(jìn)行演示吓揪。)
二亲怠、使用系統(tǒng)Spinner
1. 首先,我想到的便是使用系統(tǒng)的Spinner柠辞,說(shuō)干就干团秽,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. 接著我們來(lái)設(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)
...
簡(jiǎn)單來(lái)說(shuō)就是焙格,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)完蛤虐,下拉選擇就直接顯示出來(lái)了,無(wú)法滿足我們的需求摩泪。
而且Spinner還有個(gè)問(wèn)題笆焰,一進(jìn)入界面的時(shí)候,默認(rèn)就會(huì)選擇第一項(xiàng)见坑,而我們并不需要這樣的默認(rèn)值嚷掠。
二捏检、CustomSpinner = EditText + ListPopupWindow
那我們是否可以使用別的控件,來(lái)實(shí)現(xiàn)相同的功能呢不皆?答案是肯定的贯城,我們用EditText來(lái)接收點(diǎn)擊事件,而在請(qǐng)求完數(shù)據(jù)之后霹娄,使用ListPopupWindow來(lái)顯示下拉選項(xiàng)能犯,選擇EditText的原因主要有以下幾點(diǎn):
- 默認(rèn)在底部會(huì)有帶顏色的橫線,Material Design風(fēng)格的EditText看起來(lái)效果很不賴
- EditText可以方便的配置上犬耻、下踩晶、左、右四個(gè)位置的小圖標(biāo)枕磁,我們可以在右側(cè)放置一個(gè)向下的三角箭頭渡蜻,使它看起來(lái)像一個(gè)系統(tǒng)的Spinner
而選擇ListPopupWindow則是因?yàn)椋?/p>
- 可以方便的使用下拉列表
- 可以自由設(shè)置錨點(diǎn)
1. XML布局
布局文件依舊很簡(jiǎn)單,只要一個(gè)簡(jiǎn)單的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í),請(qǐng)求數(shù)據(jù)沦寂,請(qǐng)求完成后学密,顯示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í)西壮,會(huì)處理返回按鍵的事件
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事件遗增,會(huì)出現(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ù)做修,用來(lái)模擬網(wǎng)絡(luò)請(qǐng)求
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的功能霍狰,使用起來(lái)倒是簡(jiǎn)單,邏輯也挺清晰的饰及,但是如果界面上有三四個(gè)Spinner蔗坯,那不是就得把類似的代碼寫上三四遍?
其實(shí)我們并不關(guān)心內(nèi)部是用ListPopupWindow或者其它的控件來(lái)實(shí)現(xiàn)燎含,也不想處理任何關(guān)于ListPopupWindow的細(xì)節(jié)宾濒。我們關(guān)心的只有Spinner的初始化、適配數(shù)據(jù)屏箍、Item選擇事件(ItemClick事件)绘梦,如果可以橘忱,就再加上Spinner的點(diǎn)擊事件(Click或Touch事件)、自由控制Spinner的顯示時(shí)機(jī)卸奉。
那有沒(méi)有簡(jiǎn)單易用的方法钝诚,可以直接像使用系統(tǒng)的Spinner一樣,來(lái)使用EditText和ListPopupWindow的組合呢榄棵?并且可以提供Spinner的點(diǎn)擊事件凝颇?答案是肯定的,詳情且看下一節(jié)疹鳄。
三拧略、自定義組合控件
在第二部分我們可以看出,我們的控件要滿足以下兩大功能:
- 像系統(tǒng)Spinner一樣簡(jiǎn)單易用:初始化瘪弓、適配數(shù)據(jù)辑鲤、Item選擇事件(ItemClick事件)
- 支持點(diǎn)擊事件(Click或Touch事件),自由控制下拉框顯示時(shí)機(jī)
由于控件源碼稍長(zhǎng)杠茬,就不貼出來(lái)了月褥,有興趣可以點(diǎn)擊文末的Github鏈接,源碼也比較簡(jiǎn)單瓢喉,只是進(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的其它屬性,例如gravity
走贪、textSize
佛猛、textColor
...
2.2 還是像使用系統(tǒng)Spinner一樣,在代碼中設(shè)置Adapter
和ItemClickListener
:
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了继找,如果沒(méi)有別的需求,這樣也就夠用了逃沿。如果你有點(diǎn)擊請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)婴渡,再異步顯示下拉框的需求,可以看第3步的使用方法凯亮。
2.3 (可選) 如果你想在異步加載數(shù)據(jù)后边臼,再顯示出更新后的數(shù)據(jù),你可以使用它的點(diǎn)擊事件來(lái)處理
記准傧:
在獲取數(shù)據(jù)后柠并,你必須手動(dòng)調(diào)用sherlockSpinner.show()
方法來(lái)顯示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部分中,下拉框驮⒄牵靠在Spinner所在的整行布局上盯串,寬度更大。
四戒良、其它
另外体捏,一個(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博客