Android Spinner与自定义TextView填坑记

平台

  • 应用环境

    1. AndroidStudio: 4.1.2
    2. Gradle Plugin 4.0.0
    3. Gradle 6.1.1
    4. compileSdkVersion 30
    5. buildToolsVersion “30.0.2”
  • 源码环境

    1. Ubuntu20.04
    2. RK3288
    3. 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

ListView DropDownListView PopupWindow DropdownPopup ListPopupWindow PopupWindow mPopup DropDownListView mDropDownList buildDropDown() «Interface» SpinnerPopup setAdapter() show() dismiss() AbsSpinner Spinner SpinnerPopup mPopup performClick() show()

为了简化分析流程, 分别对ListPopupWindowDropDownListView 进行测试.
最终定位问题是出现在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);
    }

效果如图:
在这里插入图片描述
>>>>>>>>>>>>>>>>>>打完收工<<<<<<<<<<<<<<<<<<

引用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值