Android 开发 Tip 6 -- Spinner 文字 & icon 居中显示
转载请注明出处:http://blog.csdn.net/crazy1235/article/details/70903974
设置Spinner 文字居中
默认情况下,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 adapter = new ArrayAdapter( 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_verticalitem>
此时尝试把这个布局文件拿出来重写。
simple_spinner_dropdown_item.xml
"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 arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, dates);arrayAdapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);spinner.setAdapter(arrayAdapter);
运行之后,发现并没有居中!
现在,回过头来看看 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属性!
再次运行效果如下:
这个时候发现弹出来的列表文字是居中的!!
那现在另外一个问题是怎么让默认的那一条数据的问题也居中呢!????
继续来看源码!
我们知道了列表是怎么显示的了!!
那默认的那一条是怎么显示出来的呢!?
来看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 arrayAdapter = new ArrayAdapter(this, R.layout.simple_spinner_item, dates); arrayAdapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); spinner.setAdapter(arrayAdapter);
运行结果为:
ALL RIGHT!
这个时候的结果就是我们想要的了!!!
箭头居中
如果,你看着这个布局还不顺眼,想要把箭头也放到中间去(文字右边挨着)!那么请往下看!
从目前的运行效果来看,即是对Spinner设置如下属性
android:dropDownWidth="match_parent"
弹出来的ListView宽度还不是match_parent!
why?
其实右侧那个箭头是Spinner的背景图片里面的一部分!
查看Spinner的style,不管是v21以下,还是v21以上,都是指了默认的background!
v21以下
<item name="android:background">@drawable/abc_spinner_mtrl_am_alphaitem>
v21及以上
<item name="background">@drawable/spinner_background_materialitem>
spinner_background_material.xml
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" /> list>
可以发现就是一个背景,然后右侧是箭头,左侧是编辑区域!
可编辑区域是除了箭头区域的左侧,所以弹出列表时候,即是设置了match_parent属性,运行出来的效果也不是宽度全屏!
把箭头去掉很简单,在布局文件中设置一个background即可!
比如:
android:background="@android:color/white"
既然知道了默认显示的这个布局与ArrayAdapter传入的布局有关系。那么还是拿simple_spinner_item开刀!
这是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 arrayAdapter = new ArrayAdapter(this, R.layout.simple_spinner_item, android.R.id.text1, dates);
此时的运行效果图如下:
箭头添加波纹效果
针对v21的style设置ripple即可!
"@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 style>
直接显示箭头图片
在 values-v21\style.xml 中
<style name="Spinner_Arrow"> <item name="android:src">@drawable/ic_arrow_drop_down_ripple 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 直接拷贝系统的来用!
"http://schemas.android.com/apk/res/android" android:color="@color/control_highlight_material" android:radius="20dp" />
OK!
此时运行在21以上手机就有波纹效果了!
更多相关文章
- Android中的五大布局和logcat打印日志
- Android 如何实现带滚动条的TextView,在更新文字时自动滚动到最后
- Android相对布局管理器RelativeLayout
- android开发小技巧(2)背景按钮等布局的好朋友shape应用
- [Android1.5]标签TabHost图片文字覆盖的问题
- android 4.0横屏重复调用onCreate()函数
- Android RecyclerView 的瀑布流式布局