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

相关文章
|
1月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
27 1
|
2月前
|
Android开发 开发者
安卓应用开发中的自定义视图
【9月更文挑战第37天】在安卓开发的海洋中,自定义视图犹如一座座小岛,等待着勇敢的探索者去发现其独特之处。本文将带领你踏上这段旅程,从浅滩走向深海,逐步揭开自定义视图的神秘面纱。
44 3
|
4月前
|
存储 Shell Android开发
基于Android P,自定义Android开机动画的方法
本文详细介绍了基于Android P系统自定义开机动画的步骤,包括动画文件结构、脚本编写、ZIP打包方法以及如何将自定义动画集成到AOSP源码中。
93 2
基于Android P,自定义Android开机动画的方法
|
2月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
100 0
|
4月前
|
供应链 物联网 区块链
未来触手可及:探索新兴技术的趋势与应用安卓开发中的自定义视图:从基础到进阶
【8月更文挑战第30天】随着科技的飞速发展,新兴技术如区块链、物联网和虚拟现实正在重塑我们的世界。本文将深入探讨这些技术的发展趋势和应用场景,带你领略未来的可能性。
|
4月前
|
测试技术 Android开发 Python
探索软件测试的艺术:从基础到高级安卓应用开发中的自定义视图
【8月更文挑战第29天】在软件开发的世界中,测试是不可或缺的一环。它如同艺术一般,需要精细的技巧和深厚的知识。本文旨在通过浅显易懂的语言,引领读者从软件测试的基础出发,逐步深入到更复杂的测试策略和工具的使用,最终达到能够独立进行高效测试的水平。我们将一起探索如何通过不同的测试方法来确保软件的质量和性能,就像艺术家通过不同的色彩和笔触来完成一幅画作一样。
|
1月前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
1月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
41 5
|
2月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
3月前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
72 10