Android Spinner与自定义TextView填坑记

简介: Android Spinner与自定义TextView填坑记

平台


应用环境


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微调框提供了一种方法,可让用户从值集内快速选择一个值。默认状态下,微调框显示其当前所选的值。轻触微调框可显示下拉菜单,其中列有所有其他可用值,用户可从中选择一个新值。

image.png


以上来自官方文档


使用


简单的使用, 在官方文档中已经有了很简明的文档.

以下贴入本坑相关代码:


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"/>


运行结果如下:

image.png

点击展开后:

image.png

从这里后, 所有的结果都正常正确, 直到修改了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”, 展开下拉列表后发现前面加的红色圆形背景消失了:

image.png


Android的未解之謎 + 1


尝试修改其它属性值如现测试结果如下

image.png

修改属性android:spinnerMode=“dialog” 显示结果也是正常的

image.png


深入剖析


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

image.png

为了简化分析流程, 分别对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

image.png

由上面一些代码推断, 是否因为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);
    }


效果如图:

image.png

>>>>>>>>>>>>>>>>>>打完收工<<<<<<<<<<<<<<<<<<


引用


微调框 | Android 开发者

ListPopupWindow使用

调试代码 SpinnerTest.tar.gz

相关文章
|
19天前
|
Java API 调度
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
41 0
|
19天前
|
存储 Java Linux
Android Mstar增加IR 自定义遥控头码完整调试过程
Android Mstar增加IR 自定义遥控头码完整调试过程
27 1
|
1月前
|
缓存 测试技术 Android开发
深入探究Android中的自定义View绘制优化策略
【4月更文挑战第8天】 在Android开发实践中,自定义View的绘制性能至关重要,尤其是当涉及到复杂图形和动画时。本文将探讨几种提高自定义View绘制效率的策略,包括合理使用硬件加速、减少不必要的绘制区域以及利用缓存机制等。这些方法不仅能改善用户体验,还能提升应用的整体性能表现。通过实例分析和性能测试结果,我们将展示如何有效地实现这些优化措施,并为开发者提供实用的技术指南。
|
1月前
|
前端开发 Android开发 开发者
深入探究Android中的自定义View组件开发
【4月更文挑战第3天】 在现代Android应用程序的开发过程中,创建具有独特功能和高度定制化的用户界面是一个常见需求。为此,理解并掌握自定义View组件的开发成为了开发者必备的技能之一。本文将深入探讨如何在Android中创建自定义View,从基础的绘制原理到事件处理机制,再到性能优化技巧,旨在为读者提供一个全面的技术视角,并通过实例代码演示如何实现一个功能丰富、响应迅速的自定义View组件。
|
1月前
|
XML Java Android开发
Android实现自定义进度条(源码+解析)
Android实现自定义进度条(源码+解析)
58 1
|
2天前
|
Android开发
android TextView HTML 的效果
android TextView HTML 的效果
|
2天前
|
XML Android开发 数据格式
Android下自定义Button样式
Android下自定义Button样式
10 3
|
2天前
|
XML Java Android开发
如何美化android程序:自定义ListView背景
如何美化android程序:自定义ListView背景
|
2天前
|
搜索推荐 Android开发
自定义Android标题栏TitleBar布局
自定义Android标题栏TitleBar布局
|
2天前
|
XML Android开发 数据格式
关于Android中Spinner的使用
关于Android中Spinner的使用