返回

AutoCompleteTextView长文本换行?自定义Adapter终极方案

java

搞定 Android AutoCompleteTextView 下拉列表长文本换行

写 App 时,用 AutoCompleteTextView 做输入提示挺常见的,比如选个地区、搜个名字啥的。但有时候吧,提示列表里的文字贼长,默认情况下它就一行显示,后面直接“...”省略掉了,用户体验不太好。就像下面这位朋友遇到的问题:地名太长,想让它在下拉框里自动换行显示,结果设置了 singleLine="false" 也不管用。

看看他的代码:

布局里的 AutoCompleteTextView:

<com.google.android.material.textfield.TextInputLayout
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:layout_marginTop="5dp"
    android:hint="@string/customer">

    <AutoCompleteTextView
        android:id="@+id/customerField"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:inputType="text"/>

</com.google.android.material.textfield.TextInputLayout>

自定义的下拉项布局 dropdown_item_multiline.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    style="?android:attr/dropDownItemStyle"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:ellipsize="none"
    android:singleLine="false"
    android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />

设置 Adapter 的代码:

ArrayAdapter customersAdapter = new ArrayAdapter(MainActivity.this,
                R.layout.dropdown_item_multiline,
                Utils.getCustomersNames(filteredCustomersMap));
customerDropdown.setAdapter(customersAdapter);

明明在 dropdown_item_multiline.xml 文件里,TextView 已经设置了 singleLine="false"ellipsize="none",按理说应该能换行了,可为啥就不行呢?

问题根源在哪?

这事儿吧,问题通常不出在 AutoCompleteTextView 本身,也不全怪你自定义的那个 TextView 布局。关键在于咱们用的 ArrayAdapter

ArrayAdapter 是个方便的玩意儿,能快速把一个数据列表(比如 String 数组或列表)绑定到列表视图上。但它的默认实现有点“傻”,尤其是当只给它一个简单的布局文件 ID(像 R.layout.dropdown_item_multiline)和一个 TextView 的 ID (android.R.id.text1) 时,它内部处理视图(View)的逻辑可能比较简化。

具体来说,ArrayAdaptergetView 方法(负责创建或复用列表项视图)在默认情况下,可能并不会完全尊重你在 XML 布局文件里设置的所有属性,特别是那些影响高度计算和文本布局的属性,比如 singleLine="false" 配合动态高度。它更倾向于将数据(调用数据的 toString() 方法)塞到指定 ID 的 TextView 里,然后按照一些默认的、可能是单行的样式来显示。即便你提供了自定义布局,它内部的处理方式也可能限制了多行显示的能力。

另一个潜在的小坑可能在 dropdown_item_multiline.xml 里的 android:layout_height="?android:attr/listPreferredItemHeight"。这个属性通常会指定一个固定的、符合系统列表项标准的高度。如果文本需要换行,它需要的高度可能就超过了这个固定值,但布局被高度限制住了,自然换不了行。

解决方案

别急,有办法让长文本乖乖换行。主要思路就是:别太依赖 ArrayAdapter 的默认行为,咱们得自己多控制一点。

方案一:改造下拉项布局,并确保高度自适应 (可能不够)

最先想到的就是改改那个 dropdown_item_multiline.xml

  1. 修改高度属性:android:layout_height?android:attr/listPreferredItemHeight 改成 wrap_content。这样 TextView 的高度就能根据内容自己调整了。

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@android:id/text1"  <!-- 保留这个 ID因为默认 ArrayAdapter 找它 -->
        style="?android:attr/dropDownItemStyle" <!-- 这个样式可能影响外观,可以保留或按需修改 -->
        android:layout_width="match_parent"
        android:layout_height="wrap_content" <!-- 关键改动:让高度自适应内容 -->
        android:minHeight="?android:attr/listPreferredItemHeight" <!-- 可以加个最小高度,保持基础视觉效果 -->
        android:gravity="center_vertical" <!-- 让文字垂直居中可能更好看 -->
        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
        android:paddingTop="8dp" <!-- 根据需要调整上下 Padding -->
        android:paddingBottom="8dp"
        android:ellipsize="none"
        android:singleLine="false" <!-- 确认这两个属性没问题 -->
        android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />
    
  2. 重新设置 Adapter: 代码不变,还是用原来的 ArrayAdapter

    ArrayAdapter customersAdapter = new ArrayAdapter(MainActivity.this,
                    R.layout.dropdown_item_multiline, // 使用修改后的布局
                    android.R.id.text1, // 显式指定 TextView 的 ID
                    Utils.getCustomersNames(filteredCustomersMap));
    customerDropdown.setAdapter(customersAdapter);
    

    注意:ArrayAdapter 的构造函数里,最好是显式地提供 TextView 的资源 ID (android.R.id.text1),虽然对于只含一个 TextView 的布局,不指定有时也能工作,但明确指定总没错。

原理和作用:

这个改动让 TextView 在布局层面具备了高度自适应的能力。wrap_content 允许视图的高度根据内容(包括换行后的文本)来扩展。minHeight 可以保证即使文本很短,列表项也不会变得太矮,维持一定的视觉统一性。

效果怎么样?

有时候,光改布局文件就行了。但如果 ArrayAdapter 内部处理视图的方式依然比较“固执”,单靠改 XML 可能还是不灵。这时候就需要更强的手段。

方案二:创建自定义 ArrayAdapter (推荐)

这是最稳妥、最能保证效果的方法。通过继承 ArrayAdapter 并重写 getView 方法,咱们可以完全控制每个下拉项视图的创建和数据绑定过程。

  1. 下拉项布局 (dropdown_item_multiline.xml): 确保这个布局文件设置好了,特别是 layout_height="wrap_content"singleLine="false"ellipsize="none"

    <!-- 和方案一修改后的 XML 类似 -->
    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/textViewItem" <!-- 可以用自定义 ID不一定非得是 text1 -->
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="?android:attr/listPreferredItemHeight"
        android:gravity="center_vertical"
        android:padding="12dp" <!-- 调整 Padding 达到期望效果 -->
        android:ellipsize="none"
        android:singleLine="false"
        android:textAppearance="?android:attr/textAppearanceMedium" /> <!-- 可以换个字体外观 -->
    
    • 注意:这里我把 ID 改成了 textViewItem,一会儿在自定义 Adapter 里会用到。用 @android:id/text1 也可以,看个人习惯。
  2. 创建自定义 Adapter (MultilineArrayAdapter.java):

    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.TextView;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import java.util.List;
    
    public class MultilineArrayAdapter extends ArrayAdapter<String> {
    
        private Context mContext;
        private int mResource;
        private List<String> mItems;
    
        public MultilineArrayAdapter(@NonNull Context context, int resource, @NonNull List<String> objects) {
            // 注意这里的 resource ID 是整个列表项布局的 ID,不再需要 TextView 的 ID
            super(context, resource, objects);
            this.mContext = context;
            this.mResource = resource; // 保存布局文件的资源 ID
            this.mItems = objects;
        }
    
        @NonNull
        @Override
        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
            // getView 是核心,在这里创建和配置每个列表项的视图
            View view = convertView;
            ViewHolder holder;
    
            if (view == null) {
                // 如果没有可复用的 View,就创建一个新的
                LayoutInflater inflater = LayoutInflater.from(mContext);
                view = inflater.inflate(mResource, parent, false); // 使用我们自己的布局文件
    
                holder = new ViewHolder();
                // 找到布局里的 TextView,注意 ID 要匹配你的 XML 文件
                holder.textView = view.findViewById(R.id.textViewItem); 
                view.setTag(holder); // 把 ViewHolder 存起来,方便复用
            } else {
                // 如果有可复用的 View,直接拿 ViewHolder
                holder = (ViewHolder) view.getTag();
            }
    
            // 获取当前位置的数据项
            String itemText = mItems.get(position);
    
            // 设置文本内容
            if (holder.textView != null && itemText != null) {
                holder.textView.setText(itemText);
                // 确保 TextView 的属性是我们想要的(虽然 XML 里应该设置好了,但这里可以再确认一下)
                // holder.textView.setSingleLine(false); // 一般在 XML 设置就够了
            }
    
            return view; // 返回配置好的视图
        }
    
        // ViewHolder 模式,提升列表滚动性能
        private static class ViewHolder {
            TextView textView;
        }
    
        // 如果需要过滤功能(AutoCompleteTextView 通常需要),
        // 默认的 ArrayAdapter 已经实现了 Filterable 接口。
        // 如果你的数据源或过滤逻辑复杂,可能还需要重写 getFilter() 方法。
        // 对于简单的字符串列表,通常不需要动这里。
    }
    
  3. 使用自定义 Adapter:

    // 假设 Utils.getCustomersNames(filteredCustomersMap) 返回一个 List<String>
    List<String> customerNames = Utils.getCustomersNames(filteredCustomersMap);
    
    // 创建自定义 Adapter 实例
    MultilineArrayAdapter customersAdapter = new MultilineArrayAdapter(MainActivity.this,
            R.layout.dropdown_item_multiline, // 传入自定义布局 ID
            customerNames); // 传入数据列表
    
    // 设置给 AutoCompleteTextView
    customerDropdown.setAdapter(customersAdapter);
    

原理和作用:

这个方案的核心在于我们接管了 getView 方法。

  • 完全控制布局加载: inflater.inflate(mResource, parent, false) 确保了我们的 dropdown_item_multiline.xml 布局文件被正确加载。
  • 手动绑定数据: 我们手动找到布局中的 TextView (view.findViewById(R.id.textViewItem)),然后调用 setText() 把数据填进去。
  • 尊重布局属性: 因为是我们自己加载和控制 TextView,它在 XML 中定义的 singleLine="false"layout_height="wrap_content" 等属性能得到完全的尊重和应用。Android 的布局系统会根据文本内容计算 TextView 需要的高度,并据此调整整个列表项的高度。
  • ViewHolder 优化: 使用 ViewHolder 模式避免了每次都调用 findViewById,提高了列表滚动的流畅度,这是 Android 列表开发的标准实践。

进阶使用技巧:

  • 复杂数据类型: 如果你的数据源不是简单的 String 列表,而是包含多个字段的对象(比如一个 Customer 对象,里面有名字、ID 等),自定义 Adapter 就更有优势了。你可以在 getView 里根据需要显示对象的不同属性。
  • 自定义过滤逻辑: AutoCompleteTextView 的精髓在于过滤。虽然 ArrayAdapter 默认提供基于 toString() 的过滤,但如果你的需求更复杂(比如,想同时根据名称和编号过滤),可以重写自定义 Adapter 里的 getFilter() 方法,返回一个自定义的 Filter 对象来实现特定的过滤规则。

方案三:只用系统提供的简单列表项布局?(不太可能满足需求)

Android SDK 提供了一些内置的简单列表项布局,比如 android.R.layout.simple_dropdown_item_1lineandroid.R.layout.simple_list_item_2simple_dropdown_item_1line 明确是单行。simple_list_item_2 有两行 TextView,但主要是为了显示两段不同的信息(比如标题和子标题),直接用它来显示单段长文本并自动换行,效果通常不理想,而且样式可能和你 App 不搭。所以,对于长文本换行需求,这个方案基本可以忽略。

总结一下

遇到 AutoCompleteTextView 下拉框长文本不换行的问题,别只盯着 singleLine="false"

  1. 检查并修改下拉项的布局文件 (.xml) : 确保 TextViewandroid:layout_height 设置为 wrap_content,允许高度自适应。同时 singleLine 设为 falseellipsize 设为 none
  2. 强烈推荐使用自定义 Adapter : 继承 ArrayAdapter (或其他合适的 Adapter,如 CursorAdapter 等),重写 getView 方法。在 getView 中,自己加载布局、查找 TextView 并设置文本。这是最能保证多行文本正确显示的方法。
  3. 别忘了 ViewHolder : 在自定义 Adapter 的 getView 中使用 ViewHolder 模式优化性能。

通过自定义 Adapter,你就能完全掌控下拉列表项的视图创建和数据绑定逻辑,让那些长长的地名或其他文本,都能优雅地换行展示了。