平台
应用环境
AndroidStudio: 4.1.2
Gradle Plugin 4.0.0
Gradle 6.1.1
compileSdkVersion 30
buildToolsVersion “30.0.2”
源码环境
Ubuntu20.04
RK3288
Android7.1
前言
Spinner微调框提供了一种方法,可让用户从值集内快速选择一个值。默认状态下,微调框显示其当前所选的值。轻触微调框可显示下拉菜单,其中列有所有其他可用值,用户可从中选择一个新值。
以上来自官方文档
使用
简单的使用, 在官方文档中已经有了很简明的文档.
以下贴入本坑相关代码:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.ansondroider.testspinner"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:theme="@android:style/Theme.DeviceDefault.Dialog.NoActionBar"> <activity android:name=".SpinnerTest"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> </application> </manifest>
SpinnerTest.java
public class SpinnerTest extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.spinner_test); String[] arr = new String[]{ "ITEM 0", "ITEM 1", "ITEM 2", "ITEM 3 more text" }; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item_spinner, arr); Spinner sp = (Spinner)findViewById(R.id.sp); sp.setAdapter(adapter); } }
自定义TextView: MyTextView.java
public class MyTextView extends TextView { Paint mPaint = new Paint(); public MyTextView(Context context) { super(context); init(); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } void init(){ mPaint.setStrokeWidth(0); mPaint.setColor(Color.RED); } int W, H; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); W = w; H = h; } @Override protected void onDraw(Canvas canvas) { logd("onDraw"); //增加一个红色圆形背景 mPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(45, H/2, 5, mPaint); super.onDraw(canvas); } void logd(String s){ android.util.Log.d("MyTextView", "ALog " + s); } }
Layout文件: item_spinner.xml
<?xml version="1.0" encoding="utf-8"?> <com.ansondroider.testspinner.MyTextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv" android:layout_height="wrap_content" android:layout_width="match_parent" android:textSize="18sp"/>
运行结果如下:
点击展开后:
从这里后, 所有的结果都正常正确, 直到修改了TextView的gravity后:
入坑代码
item_spinner.xml
<?xml version="1.0" encoding="utf-8"?> <com.ansondroider.dailyattendance.MyTextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv" android:layout_height="wrap_content" android:layout_width="match_parent" android:gravity="center" android:textSize="18sp"/>
android:gravity=“center”, 展开下拉列表后发现前面加的红色圆形背景消失了:
Android的未解之謎 + 1
尝试修改其它属性值如现测试结果如下
修改属性android:spinnerMode=“dialog” 显示结果也是正常的
深入剖析
Spinner的点击事件:
frameworks/base/core/java/android/widget/Spinner.java
@Override public boolean performClick() { boolean handled = super.performClick(); if (!handled) { handled = true; if (!mPopup.isShowing()) { mPopup.show(getTextDirection(), getTextAlignment()); } } return handled; }
mPopup由前面的验证可知, 仅在MODE_DROPDOWN下出现显示的问题:
public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode, Theme popupTheme) { super(context, attrs, defStyleAttr, defStyleRes); //省略代码... switch (mode) { case MODE_DIALOG: { //.... } case MODE_DROPDOWN: { final DropdownPopup popup = new DropdownPopup( mPopupContext, attrs, defStyleAttr, defStyleRes); //.... }
DropdownPopup
private class DropdownPopup extends ListPopupWindow implements SpinnerPopup { private CharSequence mHintText; private ListAdapter mAdapter; public DropdownPopup( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setAnchorView(Spinner.this); setModal(true); setPromptPosition(POSITION_PROMPT_ABOVE); setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { Spinner.this.setSelection(position); if (mOnItemClickListener != null) { Spinner.this.performItemClick(v, position, mAdapter.getItemId(position)); } dismiss(); } }); } @Override public void setAdapter(ListAdapter adapter) { super.setAdapter(adapter); mAdapter = adapter; } //.... }
重点从Spinner转移到了, ListPopupWindow
简单修改了代码, 并验证ListPopupWindow是否存在相同的问题, 结果表明问题一样:
public class SpinnerTest extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.spinner_test); final String[] arr = new String[]{ "ITEM 0", "ITEM 1", "ITEM 2", "ITEM 3 more text" }; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item_spinner, arr); Spinner sp = (Spinner)findViewById(R.id.sp); sp.setAdapter(adapter); findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ListPopupWindow mListPop = new ListPopupWindow(SpinnerTest.this); List<String> arrList = new ArrayList<String>(); Utils.addAll(arrList, arr); mListPop.setAdapter(new ArrayAdapter<String>(SpinnerTest.this, R.layout.item_spinner, arrList)); mListPop.setWidth(WindowManager.LayoutParams.WRAP_CONTENT); mListPop.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); mListPop.setAnchorView(v);//设置ListPopupWindow的锚点,即关联PopupWindow的显示位置和这个锚点 mListPop.setModal(true);//设置是否是模式 mListPop.show(); } }); } }
几个核心类关系:
frameworks/base/core/java/android/widget/Spinner.java
frameworks/base/core/java/android/widget/ListPopupWindow.java
frameworks/base/core/java/android/widget/DropDownListView.j
为了简化分析流程, 分别对ListPopupWindow 和 DropDownListView 进行测试.
最终定位问题是出现在DropDownListView
PS: DropDownListView是源码内部隐藏类, 为方便调试, 后续代码在源码编译环境下进程
void dropdownList(View anchor, String[] arr){ PopupWindow popWin = new PopupWindow(SpinnerTest.this); DropDownListView lv = new DropDownListView(SpinnerTest.this, true); AcoreAdapter<String> adapter = new AcoreAdapter<String>(anchor.getContext(), R.layout.item_spinner) { @Override protected Item<String> createItem() { return new Item<String>(){ MyTextView tv; @Override public void initView(View itemRoot) { tv = (MyTextView)itemRoot.findViewById(R.id.tv); } @Override public void fillData(String data, boolean select) { tv.setText(data); } }; } }; adapter.addAll(arr, true); //lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); //lv.setCacheColorHint(0); lv.setAdapter(adapter); popWin.setContentView(lv); popWin.setOutsideTouchable(true); popWin.setWidth(200); popWin.setHeight(200); popWin.showAsDropDown(anchor); }
略去N个调试过程和排错方法, 最终定位问题代码在
frameworks/base/core/java/android/widget/DropDownListView.java
/** * Avoids jarring scrolling effect by ensuring that list elements * made of a text view fit on a single line. * * @param position the item index in the list to get a view for * @return the view for the specified item */ @Override View obtainView(int position, boolean[] isScrap) { View view = super.obtainView(position, isScrap); if (view instanceof TextView) { ((TextView) view).setHorizontallyScrolling(true); } return view; }
((TextView) view).setHorizontallyScrolling(true); 这行代码注释掉则可以解决问题.
为进一步验证, 使用PopupWIndow + ListView
void popupWindow(View anchor, String[] arr){ PopupWindow popWin = new PopupWindow(SpinnerTest.this); ListView lv = new ListView(SpinnerTest.this); AcoreAdapter<String> adapter = new AcoreAdapter<String>(anchor.getContext(), R.layout.item_spinner) { @Override protected Item<String> createItem() { return new Item<String>(){ MyTextView tv; @Override public void initView(View itemRoot) { tv = (MyTextView)itemRoot.findViewById(R.id.tv); //调用问题函数 tv.setHorizontallyScrolling(true); } @Override public void fillData(String data, boolean select) { tv.setText(data); } }; } }; adapter.addAll(arr, true); //lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); //lv.setCacheColorHint(0); lv.setAdapter(adapter); popWin.setContentView(lv); popWin.setOutsideTouchable(true); popWin.setWidth(200); popWin.setHeight(200); popWin.showAsDropDown(anchor); }
成功复现问题
从DropDownListView.obtainView的代码中, 也可以发现, 并不需要修改源码来解决, 在自定义的TextView中, 通过重写setHorizontallyScrolling这个函数同样可以解决
@Override public void setHorizontallyScrolling(boolean whether) { super.setHorizontallyScrolling(false); }
>>>>>>>>>>>>>>>>>>至此,可告一段落<<<<<<<<<<<<<<<<<<
问题既然出现在TextView, 顺便看下相关代码(不作详细说明):
frameworks/base/core/java/android/widget/TextView.java
/** * Sets whether the text should be allowed to be wider than the * View is. If false, it will be wrapped to the width of the View. * * @attr ref android.R.styleable#TextView_scrollHorizontally */ public void setHorizontallyScrolling(boolean whether) { if (mHorizontallyScrolling != whether) { mHorizontallyScrolling = whether; if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } }
查找mHorizontallyScrolling有关联的函数, 找到它:
/** * Make a new Layout based on the already-measured size of the view, * on the assumption that it was measured correctly at some point. */ private void assumeLayout() { int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); if (width < 1) { width = 0; } int physicalWidth = width; if (mHorizontallyScrolling) { width = VERY_WIDE; } makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, physicalWidth, false); }
VERY_WIDE这个变量似乎有点意思
static final int VERY_WIDE = 1024 * 1024; // XXX should be much larger
由上面一些代码推断, 是否因为VERY_WIDE影响了图像绘制的坐标?
先做个猜测: 此时的TextView的有效宽度为1024 * 1024, 而使用Gravity.CENTER 和 Gravity.RIGHT时, 视图的实际坐标已发生了变化?
尝试画一条从 [0, H/2] - [1024* 512, H/2]的线:
@Override protected void onDraw(Canvas canvas) { logd("onDraw"); mPaint.setStyle(Paint.Style.FILL); //canvas.drawCircle(45, H/2, 5, mPaint); canvas.drawLine(0, H/2, 1024* 512, H/2, mPaint); super.onDraw(canvas); }
效果如图:
>>>>>>>>>>>>>>>>>>打完收工<<<<<<<<<<<<<<<<<<
引用
微调框 | Android 开发者
ListPopupWindow使用
调试代码 SpinnerTest.tar.gz