平台
-
应用环境
- 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
尝试修改其它属性值如现测试结果如下
属性值 | 测试结果 |
---|---|
left | 显示 |
right | 不显示 |
top | 显示 |
bottom | 显示 |
center_horizontal | 不显示 |
center_vertical | 显示 |
center | 不显示 |
修改属性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.java
为了简化分析流程, 分别对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);
}
效果如图:
>>>>>>>>>>>>>>>>>>打完收工<<<<<<<<<<<<<<<<<<