Android 开发 Tip 6 -- Spinner

男儿欲遂平生志,六经勤向窗前读。这篇文章主要讲述Android 开发 Tip 6 -- Spinner相关的知识,希望能为你提供帮助。
转载请注明出处: http://blog.csdn.net/crazy1235/article/details/70903974
设置Spinner 文字居中 默认情况下, Spinner控件的效果是这样的:

Android 开发 Tip 6 -- Spinner

文章图片

想让文字居中显示怎么办? ? ?
在布局文件中设置
android:gravity= " center"

也不起作用! !
源码走读 先来看 Spinner 的构造函数
public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode, Theme popupTheme) { super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.Spinner, defStyleAttr, defStyleRes); // 省略代码if (mode = = MODE_THEME) { mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG); }// 判断弹出模式 dialog or dropdown switch (mode) { case MODE_DIALOG: { mPopup = new DialogPopup(); // DialogPopup mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt)); break; }case MODE_DROPDOWN: { final DropdownPopup popup = new DropdownPopup( mPopupContext, attrs, defStyleAttr, defStyleRes); // DropdownPopup // 省略代码 break; } }// ... a.recycle(); // 设置adapter if (mTempAdapter != null) { setAdapter(mTempAdapter); mTempAdapter = null; } }

当mTempAdapter 不为空时, 调用了setAdapter() 设置适配器!
但是我们如果在xml中设置了entries属性, 并没有设置adapter
android:entries= " @ array/date_spinner_items"

上图的列表是怎么出来的呢? !
来看父类~~
AbsSpinner
public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initAbsSpinner(); final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes); final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); if (entries != null) { final ArrayAdapter< CharSequence> adapter = new ArrayAdapter< CharSequence> ( context, R.layout.simple_spinner_item, entries); adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); setAdapter(adapter); }a.recycle(); }

从构造器函数中看出, 当entries属性不为空时, 调用了 setAdapter() 函数!
注意这里, 用到的是 ArrayAdapter 适配器 。还有两个重要的布局文件:
  • simple_spinner_item
  • simple_spinner_dropdown_item
子类Spinner重写了setAdapter 函数
@ Override public void setAdapter(SpinnerAdapter adapter) { // ... super.setAdapter(adapter); // ... mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme())); }

mPopup 是一个接口对象, 里面封装了 设置适配器、显示列表、关闭列表等操作!
不管是Spinner是 dialog 形式还是 dropdown 形式, 都实现了该接口!
private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener

private class DropdownPopup extends ListPopupWindow implements SpinnerPopup

OK, 现在来看 DropDownAdapter
private static class DropDownAdapter implements ListAdapter, SpinnerAdapter { private SpinnerAdapter mAdapter; private ListAdapter mListAdapter; public DropDownAdapter(@ Nullable SpinnerAdapter adapter, @ Nullable Resources.Theme dropDownTheme) {mAdapter = adapter; // 注意这里! ! ! // 省略代吗 }public int getCount() { return mAdapter = = null ? 0 : mAdapter.getCount(); }public Object getItem(int position) { return mAdapter = = null ? null : mAdapter.getItem(position); }public long getItemId(int position) { return mAdapter = = null ? -1 : mAdapter.getItemId(position); }public View getView(int position, View convertView, ViewGroup parent) { return getDropDownView(position, convertView, parent); }public View getDropDownView(int position, View convertView, ViewGroup parent) { return (mAdapter = = null) ? null : mAdapter.getDropDownView(position, convertView, parent); }// 省略代吗 }

弹出来的列表每个item的View渲染通过 getDropDownView 函数!
而mAdapter是通过构造函数传进来的!
再回到这里:
mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));

这时adapter是Spinner父类AbsSpinner构造函数中new出来的 ArrayAdapter
然后看 ArrayAdapter 类中的 getDropDownView 函数
@ Override public View getDropDownView(int position, @ Nullable View convertView, @ NonNull ViewGroup parent) { final LayoutInflater inflater = mDropDownInflater = = null ? mInflater : mDropDownInflater; return createViewFromResource(inflater, position, convertView, parent, mDropDownResource); }

private @ NonNull View createViewFromResource(@ NonNull LayoutInflater inflater, int position, @ Nullable View convertView, @ NonNull ViewGroup parent, int resource) { final View view; final TextView text; if (convertView = = null) { view = inflater.inflate(resource, parent, false); } else { view = convertView; }try { if (mFieldId = = 0) { text = (TextView) view; } else { text = (TextView) view.findViewById(mFieldId); if (text = = null) { throw new RuntimeException(" Failed to find view with ID " + mContext.getResources().getResourceName(mFieldId) + " in item layout" ); } } } catch (ClassCastException e) { Log.e(" ArrayAdapter" , " You must supply a resource ID for a TextView" ); throw new IllegalStateException( " ArrayAdapter requires the resource ID to be a TextView" , e); }// 省略代码! ! ! return view; }

源码看到这里就能发现, 通过映射 mDropDownResource这个布局文件, 来得到Spinner列表的item布局!
而, 恰恰在AbsSpinner 的构造函数中设置了这一布局文件
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);

文字居中
所以, 现在想要改变Spinner的文字居中显示! 则需要设置相应的adapter!
OK。现在就来看这个布局文件
< CheckedTextView xmlns:android= " http://schemas.android.com/apk/res/android" android:id= " @ android:id/text1" style= " ?android:attr/spinnerDropDownItemStyle" android:singleLine= " true" android:layout_width= " match_parent" android:layout_height= " ?android:attr/dropdownListPreferredItemHeight" android:ellipsize= " marquee" />

可以看出item布局文件只是一个 CheckedTextView
我们现在想要把这个列表的文字居中显示!
跟踪style文件发现设置的gravity属性是 center_vertical
< item name= " android:gravity" > center_vertical< /item>

此时尝试把这个布局文件拿出来重写。
simple_spinner_dropdown_item.xml
< CheckedTextView xmlns:android= " http://schemas.android.com/apk/res/android" android:id= " @ android:id/text1" style= " ?android:attr/spinnerDropDownItemStyle" android:layout_width= " match_parent" android:layout_height= " ?attr/listPreferredItemHeightSmall" android:ellipsize= " marquee" android:gravity= " center" // ! ! ! android:maxLines= " 1" />

然后在Activity中对Spinner对象设置适配器!
ArrayAdapter< String> arrayAdapter = new ArrayAdapter< String> (this, android.R.layout.simple_spinner_item, dates); arrayAdapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); spinner.setAdapter(arrayAdapter);

运行之后, 发现并没有居中!
Android 开发 Tip 6 -- Spinner

文章图片

现在, 回过头来看看 mPopup.show() 显示列表的函数
加入我们选择的是dropdown模式!
DropdownPopup.show()
public void show(int textDirection, int textAlignment) { final boolean wasShowing = isShowing(); computeContentWidth(); setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); super.show(); final ListView listView = getListView(); listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); listView.setTextDirection(textDirection); listView.setTextAlignment(textAlignment); setSelection(Spinner.this.getSelectedItemPosition()); // ... 省略代码 }

可以看出, 弹出来的视图就是一个ListView
针对listview设置了 direction 和 textAligment 两个属性! !
好嘛! 将 simple_spinner_dropdown_item.xml 这个布局再修改一下
< CheckedTextView xmlns:android= " http://schemas.android.com/apk/res/android" android:id= " @ android:id/text1" style= " ?android:attr/spinnerDropDownItemStyle" android:layout_width= " match_parent" android:layout_height= " ?attr/listPreferredItemHeightSmall" android:ellipsize= " marquee" android:gravity= " center" android:maxLines= " 1" android:textAlignment= " gravity" />

加入了textAlignment属性!
再次运行效果如下:
Android 开发 Tip 6 -- Spinner

文章图片

【Android 开发 Tip 6 -- Spinner】这个时候发现弹出来的列表文字是居中的! !
那现在另外一个问题是怎么让默认的那一条数据的问题也居中呢! ? ? ? ?
继续来看源码!
我们知道了列表是怎么显示的了! !
那默认的那一条是怎么显示出来的呢! ?
来看Spinner的, onLayout() 函数!
@ Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mInLayout = true; layout(0, false); // !!! mInLayout = false; }

@ Override void layout(int delta, boolean animate) { // ...if (mAdapter != null) { View sel = makeView(mSelectedPosition, true); // !!! // ... }// ... }

先来看下 mSelectedPosition 的值是多少
网上找父类, 在AdapterView 中找到该变量的声明
public static final int INVALID_POSITION = -1; int mSelectedPosition = INVALID_POSITION;

难道 layout(0, false); 的时候 mSelectedPosition 就是 -1 了吗? ? ?
当然不会! 因为我们知道默认选中的是列表数据的第一条
然后又是一顿狂翻代码! ~~
最后在父类的AbsSpinner中找到
@ Override public void setAdapter(SpinnerAdapter adapter) { // ...mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; if (mAdapter != null) { mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); // ...// 当entries不为空时, position = 0 int position = mItemCount > 0 ? 0 : INVALID_POSITION; setSelectedPositionInt(position); // 省略代吗 }

子类Spinner重写了setAdapter() 函数, 所以设置适配器时, 调用了 setSelectedPositionInt(0)
而 setSelectedPosition() 函数是在父类AdapterView中!
void setSelectedPositionInt(int position) { mSelectedPosition = position; mSelectedRowId = getItemIdAtPosition(position); }

好! 分析到这, 就会发现mSelectedPosition 是0!
接着上面的onLayout()函数分析
makeView(0, true);
private View makeView(int position, boolean addChild) { View child; // 省略代码child = mAdapter.getView(position, null, this); setUpChild(child, addChild); return child; }

这里的mAdpater就是系统默认设置或者我们设置的ArrayAdaper 对象。!
而在 ArrayAdapter 类中:
@ Override public @ NonNull View getView(int position, @ Nullable View convertView, @ NonNull ViewGroup parent) { return createViewFromResource(mInflater, position, convertView, parent, mResource); }

与 createViewFromResource() 函数一样也是调用了 createViewFromResource()
只不过这里是mResource 就是我们构造ArrayAdapter时, 构造函数传入的那个布局! !
R.layout.simple_spinner_item

所以, Spinner 默认显示的布局由 ArrayAdapter构造函数中的布局决定!
列表item的布局由 ArrayAdapter 函数setDropDownViewResource() 传入的布局决定!
如果没有调用此方法, 则默认 mResource = mDropDownResource = resource

所以此时, 想要把默认的文字居中, 把系统的 android.R.layout.simple_spinner_item 拷贝出来修改一下!
< TextView xmlns:android= " http://schemas.android.com/apk/res/android" android:id= " @ android:id/text1" style= " ?android:attr/spinnerItemStyle" android:layout_width= " match_parent" android:layout_height= " wrap_content" android:layout_centerInParent= " true" android:ellipsize= " marquee" android:gravity= " center" android:maxLines= " 1" android:textAlignment= " inherit" />

系统并没有为 spinnerItemStyle 设置gravity属性
ArrayAdapter< String> arrayAdapter = new ArrayAdapter< String> (this, R.layout.simple_spinner_item, dates); arrayAdapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); spinner.setAdapter(arrayAdapter);

运行结果为:
Android 开发 Tip 6 -- Spinner

文章图片

ALL RIGHT!
这个时候的结果就是我们想要的了! ! !
箭头居中
如果, 你看着这个布局还不顺眼, 想要把箭头也放到中间去( 文字右边挨着) ! 那么请往下看!
从目前的运行效果来看, 即是对Spinner设置如下属性
android:dropDownWidth= " match_parent"

弹出来的ListView宽度还不是match_parent!
Android 开发 Tip 6 -- Spinner

文章图片

why?
其实右侧那个箭头是Spinner的背景图片里面的一部分!
查看Spinner的style, 不管是v21以下, 还是v21以上, 都是指了默认的background!
v21以下
< item name= " android:background" > @ drawable/abc_spinner_mtrl_am_alpha< /item>

Android 开发 Tip 6 -- Spinner

文章图片

Android 开发 Tip 6 -- Spinner

文章图片

v21及以上
< item name= " background" > @ drawable/spinner_background_material< /item>

spinner_background_material.xml
< layer-list xmlns:android= " http://schemas.android.com/apk/res/android" android:paddingMode= " stack" android:paddingStart= " 0dp" android:paddingEnd= " 48dp" android:paddingLeft= " 0dp" android:paddingRight= " 0dp" > < item android:gravity= " end|fill_vertical" android:width= " 48dp" android:drawable= " @ drawable/control_background_40dp_material" /> < item android:drawable= " @ drawable/ic_spinner_caret" android:gravity= " end|center_vertical" android:width= " 24dp" android:height= " 24dp" android:end= " 12dp" /> < /layer-list>

可以发现就是一个背景, 然后右侧是箭头, 左侧是编辑区域!
可编辑区域是除了箭头区域的左侧, 所以弹出列表时候, 即是设置了match_parent属性, 运行出来的效果也不是宽度全屏!
把箭头去掉很简单, 在布局文件中设置一个background即可!
比如:
android:background= " @ android:color/white"

既然知道了默认显示的这个布局与ArrayAdapter传入的布局有关系。那么还是拿simple_spinner_item开刀!
Android 开发 Tip 6 -- Spinner

文章图片

这是ArrayAdapter中 createViewFromResource() 函数中的片段!
**当mFieldId 是0 时, 将view布局强转成TextView !
不是0时, 通过findViewId找到对应的TextView布局! **
mFieldId 是ArrayAdapter 的四个参数构造函数传入来的值, 默认就是0
所以修改mResource布局即可!
使用一个布局包括TextView, 然后设置ItemView在这个TextView的右边
< RelativeLayout xmlns:android= " http://schemas.android.com/apk/res/android" android:layout_width= " match_parent" android:layout_height= " wrap_content" > < TextView xmlns:android= " http://schemas.android.com/apk/res/android" android:id= " @ android:id/text1" style= " ?android:attr/spinnerItemStyle" android:layout_width= " wrap_content" android:layout_height= " wrap_content" android:layout_centerInParent= " true" android:ellipsize= " marquee" android:gravity= " center" android:maxLines= " 1" android:textAlignment= " inherit" /> < ImageView android:layout_width= " wrap_content" android:layout_height= " wrap_content" android:layout_centerVertical= " true" android:layout_toEndOf= " @ android:id/text1" android:layout_toRightOf= " @ android:id/text1" android:src= " @ drawable/ic_arrow_drop_down_black_24dp" /> < /RelativeLayout>

但是必须要保证构造ArrayAdapter的时候传入这个TextView的ID
ArrayAdapter< String> arrayAdapter = new ArrayAdapter< String> (this, R.layout.simple_spinner_item, android.R.id.text1,dates);

此时的运行效果图如下:
Android 开发 Tip 6 -- Spinner

文章图片

箭头添加波纹效果
针对v21的style设置ripple即可!
< ImageView style= " @ style/Spinner_Arrow" android:layout_width= " wrap_content" android:layout_height= " wrap_content" android:layout_centerVertical= " true" android:layout_toEndOf= " @ android:id/text1" android:layout_toRightOf= " @ android:id/text1" />

这是箭头布局, 定义了一个style – Spinner_Arrow
在 values\\style.xml 中
< style name= " Spinner_Arrow" > < item name= " android:src" > @ drawable/ic_arrow_drop_down_black_24dp< /item> < /style>

直接显示箭头图片
在 values-v21\\style.xml 中
< style name= " Spinner_Arrow" > < item name= " android:src" > @ drawable/ic_arrow_drop_down_ripple< /item> < /style>

在这个drawable布局中定义 layer-list
< ?xml version= " 1.0" encoding= " utf-8" ?> < layer-list xmlns:android= " http://schemas.android.com/apk/res/android" android:paddingEnd= " 48dp" android:paddingLeft= " 0dp" android:paddingMode= " stack" android:paddingRight= " 0dp" android:paddingStart= " 0dp" > < item android:width= " 48dp" android:height= " 48dp" android:drawable= " @ drawable/control_background_40dp_material" android:gravity= " end|fill_vertical" /> < item android:width= " 24dp" android:height= " 24dp" android:drawable= " @ drawable/ic_arrow_drop_down_black_24dp" android:end= " 12dp" android:gravity= " end|center_vertical" /> < /layer-list>

第一个item就是扩散布局, 第二个item是箭头布局
这里 control_background_40dp_material.xml 直接拷贝系统的来用!
< ripple xmlns:android= " http://schemas.android.com/apk/res/android" android:color= " @ color/control_highlight_material" android:radius= " 20dp" />

OK!
此时运行在21以上手机就有波纹效果了!
Android 开发 Tip 6 -- Spinner

文章图片

Android 开发 Tip 6 -- Spinner

文章图片


    推荐阅读