Java / Android 下拉选择框 Spinner

本节我们将学习 Android 提供的下拉选择框——Spinner,它也是 Adapter 的常客。不仅仅是在 Android 端,在 Windows 上我们也经常会看到 Spinner 类型的样式。通常它是以下拉的形式存在,Spinner 在下拉列表中包含很多可供用户选择的选项,用户可以通过点击 Spinner 完成选择。

1. Spinner 的特性

Spinner 的功能是提供一个选择框,默认情况下 Spinner 展示的是当前的选项,点击 Spinner 控件将会展示所有可选项供用户点击选择。Spinner 在很多情况下并不是独立存在的,很有可能当前的 Spinner 的选项需要依赖于前一个 Spinner 的选择结果。

比如我们常见的地址选择页面,首先一个 Spinner 展示所有的省份,在你选择省份之后,第二个 Spinner 拿到你的选项生成相应的城市选项,所以 Spinner 是一个常用并且非常灵活的控件,它的实现依然需要 Adapter。

2. Spinner 的基本用法

2.1 Spinner 的相关属性

  • android:gravity:
    设置 Spinner 内部 item 的对齐方式
  • android:dropDownHorizontalOffset:
    设置下拉选择框的水平偏移距离
  • android:dropDownVerticalOffset:
    设置下拉选择框的垂直偏移距离
  • android:dropDownWidth:
    设置下拉列表框的宽度
  • android:dropDownSelector:
    下拉选择框被选中时的背景样式
  • android:popupBackground:
    设置下拉选择框的背景样式
  • android:prompt:
    设置选择框的提示信息,此属性不能直接设置 String,而必须设置一个 string 资源
  • android:spinnerMode:
    选择框的模式,有两个可选值:
    • dialog: 对话框风格
    • dropdown: 下拉列表风格
  • android:entries:
    通过 string 资源的方式设置下拉选择项

2.2 Spinner 选择事件监听器

  • setOnItemSelectedListener:
    为 Spinner 设置选中事件回调,该接口中包含两个回调方法:

    • onItemSelected:
      当 Spinner 中某个选项被选中时回调该方法,在用户选择了 Spinner 中不同于当前已选中的选项或者当前没有任何选项选中时,系统会回调该方法。此时可以通过getItemAtPosition(position)来获取当前被选中的 item 对象,比如文章开头提到的选城市的功能就需要通过此接口实时获取用户的选择。
    • onNothingSelected:
      这个回调方法用的不较少,它是在选项消失的时候被系统回调的,选项消失通常发生在数据清空的时候

    特别说明: 虽然 Spinner 和 ListView、GridView 一样都是 AdapterView,但是在 Spinner 中不能使用setOnItemClickListener,如果使用系统会抛出以下异常:

setOnItemClickListener cannot be used with a spinner

所以在 Spinner 中我们要用setOnItemSelectedListener来监听选择事件。

3. Spinner 使用示例

本节仍然采用“水果列表”的示例,之前是通过 ListView、GridView 将水果类型和图片直接罗列在屏幕上。而本节只会在屏幕上暴露出一个选项,在点击水果的时候弹出所有的选择项等待用户选择,由于 Adapter 完善的 MVC 模式,可以继续在之前的代码上简单修改即可。

3.1 定义 Spinner 布局

布局文件很简单,直接在根布局中放置一个 Spinner 即可:

<?xml version="1.0" encoding="utf-8"?>
<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/spinner"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center" />

Spinner 的属性都比较好理解,大家可以在阅读的同时自行添加尝试。

3.2 编写 Adapter

和上一节的 GridView 一样,我们通过修改 MyAdapter 的getCountgetView两个回调方法来实现水果列表的扩展,代码如下:

package com.emercy.myapplication;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;


public class MyAdapter extends BaseAdapter {

    private Context mContext;
    private String[] mName;
    private int[] mResId;

    public MyAdapter(Context context) {
        mContext = context;
    }

    public void setData(String[] name, int[] resId) {
        mName = name;
        mResId = resId;
    }


    @Override
    public int getCount() {
        return mName.length * 10;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 针对convertView做一个简单的优化
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.list_view, null);
        }
        TextView name = convertView.findViewById(R.id.textView);
        ImageView image = convertView.findViewById(R.id.imageView);
        name.setText(mName[position % mName.length]);
        image.setImageResource(mResId[position % mResId.length]);
        return convertView;
    }
}

细心的读者可能会注意到,相比上一节的例子,在getView当中有一个小小的改动:

 // 针对convertView做一个简单的优化
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.list_view, null);
        }

这个改动是一个简单的优化,可以减少每次 inflate 造成的性能消耗,这样 Adapter 只会在第一次去做 inflate,而后续的getView()回调将直接复用之前的convertView。

3.3 定义数据源

数据源分两部分:水果名称和水果图片,分别用一个 String 数组和 int 数组存放,如下:

String[] mDataName = {"苹果", "梨", "香蕉", "桃子", "西瓜", "荔枝", "橘子"};
    int[] mDataImage = {R.drawable.apple, R.drawable.pear, R.drawable.banana, R.drawable.peach,
            R.drawable.watermelon, R.drawable.lychee, R.drawable.orange, R.drawable.orange};

然后通过 MyAdapter 提供的设置数据的接口设置给 Adapter:

        adapter.setData(mDataName, mDataImage);

3.4 完成 MainActivity

整体的 MainActivity 和之前的逻辑大体相同,但是在 2.2 中我们提到过,Spinner 不能使用setOnItemClickListener接口,所以我们将事件监听器改成setOnItemSelectedListener,最终代码如下:

package com.emercy.myapplication;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import android.widget.Toast;

public class MainActivity extends Activity {

    Spinner mSpinner;
    String[] mDataName = {"苹果", "梨", "香蕉", "桃子", "西瓜", "荔枝", "橘子"};
    int[] mDataImage = {R.drawable.apple, R.drawable.pear, R.drawable.banana, R.drawable.peach,
            R.drawable.watermelon, R.drawable.lychee, R.drawable.orange, R.drawable.orange};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSpinner = findViewById(R.id.spinner);

        MyAdapter adapter = new MyAdapter(this);
        adapter.setData(mDataName, mDataImage);
        mSpinner.setAdapter(adapter);
        mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(getApplicationContext(), mDataName[position / mDataImage.length], Toast.LENGTH_LONG).show();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                Toast.makeText(getApplicationContext(), "onNothingSelected", Toast.LENGTH_LONG).show();
            }
        });
    }
}

运行之后页面中只会有一个默认选项,点击 Spinner 会弹出一个下拉框,任意选中一个会触发onItemSelected回调方法并通过Toast打印当前选择项。选择完成之后,Spinner 会展示新选择的水果名称和图片,效果如下:

Spinner示例

4. 小结

本节继 ListView、GridView 之后又讲解了一个采用 Adapter 实现的 UI 样式,它主要适用的是下拉选择的场景,相比 ListView、GridView 它更省空间,只会在页面上展示已选项,用户需要通过点击才能调起所有的选项。

Spinner 的属性也比较简单,需要特别注意的是它不支持设置setOnItemClickListener接口,取而代之的是setOnItemSelectedListener接口,最后我们仍然采用“水果列表”的例子演示了一个 Spinner 的用法,当然对于 Spinner 还有很多花哨的样式,大家可以在自己的例子代码中设置看看,会有惊喜哦!