小白7码-Android原生开发皮毛系列(4)-Spinner

1,569 阅读9分钟

在前面几章,小白陆续介绍了TextView, Button, Checkbox以及Switch等几种基础控件的基本用法,具体请见:

  1. 小白7码-android原生开发皮毛系列-TextView

  2. 小白7码-android原生开发皮毛系列(2)-Button以及Checkbox

  3. 小白7码-android原生开发皮毛系列(3)-Switch

在这一章,小白需要入坑一款食之无味弃之也不可惜的控件,它就是Spinner,可以简单称为选项控件。一般我们提到选项控件,第一个跳入脑海的应该是dropdown,即所谓的下拉选项卡。Spinner即为android原生世界里类似的控件。当然Spinner要更为复杂一些,这些复杂性感觉源于Google或者早期android团队在移动UI设计开发上一些理念的探索。可惜,Spinner这种探索并没有非常成功。因此在本章的最后小白还是会推荐另一种替代的方法。

Spinner最简单用法

通常我们在项目中需要使用dropdown这样的选项卡控件实现某些例如类型,分类等输入项。同时这些输入项通常都有固定的几个可选项,例如在日历app中,我们通常需要选择输入星期几。这个时候就可以使用Spinner的最简单用法。

  • 定义固定可选项列表

这个时候你需要使用strings.xml文件。如果你阅读过TextView,Switch等章节,应该熟悉使用strings.xml文件定义固定文本配置,用于界面上文本静态显示。但其实strings.xml比小白想象中要强大的多。这里就是一个例子,通过string-array定义固定可选项的值。感觉好像发现了新大陆:

<string-array name="week">
    <item>星期一</item>
    <item>星期二</item>
    <item>星期三</item>
    <item>星期四</item>
    <item>星期五</item>
    <item>星期六</item>
    <item>星期天</item>
</string-array>
  • 定义Spinner并设置选型值

这里你只需要简单给Spinner的entries属性指定上面的文本列表即可,如下图1所示:

                                  图1 设置entries属性

通过上面两个步骤,即可以实现最基本的使用。当然这样的使用的局限性也是显而易见的:首先,Spinner的选项将限定于指定string-array列表元素,无法灵活变更。第二点,通过entries属性绑定数据,无法设置选项的个性化定制。例如你想要每个选项边上有一个图标就无法做到。当然通过这种方式基本能解决掉我们50%的需求,因为Spinner的应用场景通常没有那么高的要求。

Spinner与Adapter

除了最基本的用法外,Spinner还能够和Adapter一起使用。本质上来说,Spinner可以看作是一个ListView,可以说是ListView的直系血亲。通过Adapter的方式,可以做到灵活的选项绑定,而且可以控制每个选项的显示。当然缺点也很明显,使用起来略微复杂。言归正传,介绍一下使用流程。

  • 定义Spinner选项列表元素的布局文件。

选项列表元素布局文件用于布局显示下拉列表中每一个选项卡的界面样子。你可以通过该布局文件作出很复杂的样子,具体操作如下:右键res > new(新建) > XML > Layout XML File。在这里我们定义一个spinner_item.xml文件并用简单TextView控件来实现星期一到星期天的展示,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/spinner_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
  • 定义Spinner选中元素的布局文件。

选中元素布局文件主要用于布局显示选中的元素在spinner控件的界面样子。如果你的展示比较简单,比如仅仅展示文本,那么选中元素布局文件和选项列表元素布局文件可以合并使用一个。但是如果有显示上的差异,那么建议还是分出来比较好。具体操作如同选项列表元素布局文件。spinner_selected_item.xml文件的具体代码如下:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/spinner_selected_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
  • 定义一个数据实体类和SpinnerAdapter的实现。

数据实体类是承载数据的Java类,用于存放Spinner控件中需要使用的具体数据。数据实体类可以定义得很复杂,具体看你的需求实现。这里我们定义一个SpinnerDataEntity类,具体代码如下:

/**
 * Spinner数据实体类
 */
public class SpinnerDataEntity {

    /**
     * 值
     */
    private String value;

    /**
     * 显示文本
     */
    private String text;

    public SpinnerDataEntity(){

    }

    public SpinnerDataEntity(String value, String text){
        this.value = value;
        this.text = text;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

而SpinnerAdapter接口则定义了数据到界面展示的适配,类似于处理数据绑定,显示指定的界面控件。直白的说就是告诉Spinner,选中的元素应该怎么显示,显示什么,下拉列表应该怎么显示,显示什么。SpinnerAdapter接口有很多实现,例如ArrayAdapter,SimpleAdapter等,这里我们通过继承扩展ArrayAdapter实现我们想要的自定义的SpinnerCustomAdapter。代码如下:

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;

/**
 * Spinner自定义Adapter
 */
public class SpinnerCustomAdapter extends ArrayAdapter<SpinnerDataEntity> {

    private int resource;

    private int selectedResource;

    public SpinnerCustomAdapter(@NonNull Context context, int resource, int selectedResource, @NonNull SpinnerDataEntity[] objects) {
        super(context, resource, objects);

        this.resource = resource;
        this.selectedResource = selectedResource;
    }

    @Override
    public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        if(convertView == null){
            convertView = LayoutInflater.from(this.getContext()).inflate(this.resource, null);
        }

        TextView itemView = (TextView) convertView;
        SpinnerDataEntity item = getItem(position);
        itemView.setText(item.getText());
        return convertView;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        if(convertView == null){
            convertView = LayoutInflater.from(this.getContext()).inflate(this.selectedResource, null);
        }

        TextView itemView = (TextView) convertView;
        SpinnerDataEntity item = getItem(position);
        itemView.setText(item.getText());
        return convertView;
    }
}

其中resource 和 selectedResource分别通过布局文件id指定选项列表元素布局文件spinner_item.xml和选中元素布局文件spinner_selected_item.xml。 通过从写getView方法处理绑定选中元素数据界面展示。而通过getDropDownView处理绑定选项列表元素的数据界面展示。

  • 定义Spinner

  • 给Spinner绑定Adapter

最后通过Spinner的setAdapter方法将SpinnerCustomAdapter实例传给Spinner即可。其中需要注意的是,给SpinnerCustomAdapter指定布局文件,应该在R.layout而不是R.id, 切记!在Activity中绑定代码具体如下:

private Spinner weekday;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_spinner_view);

    if(savedInstanceState == null){
        this.initWeekdaySpinner();
    }
}

private void initWeekdaySpinner() {
    SpinnerDataEntity[] dataEntities = new SpinnerDataEntity[]{
            new SpinnerDataEntity("monday", "星期一"),
            new SpinnerDataEntity("tuesday", "星期二"),
            new SpinnerDataEntity("wednesday", "星期三"),
            new SpinnerDataEntity("thursday", "星期一"),
            new SpinnerDataEntity("friday", "星期五"),
            new SpinnerDataEntity("saturday", "星期六"),
            new SpinnerDataEntity("sunday", "星期天")
    };
    ArrayAdapter<SpinnerDataEntity> spinnerDataEntityArrayAdapter = new SpinnerCustomAdapter(getApplicationContext(), R.layout.spinner_item, R.layout.spinner_selected_item, dataEntities);
    this.weekday = findViewById(R.id.weekday);
    this.weekday.setAdapter(spinnerDataEntityArrayAdapter);
}

这样你就可以得到一个自定义灵活的Spinner。如果你想要一个图标你也可以加起来,给你一个太阳吧:

                           

Spinner的dropdown模式和dialog模式

通过上面两个例子我们已经了解了Spinner的一些用法。但是Spinner其实有两种展现方式。一种是dropdown就想上面所看到的例子一样。另一种是dialog,即为弹出框的方式。dropdown是一种很常见的处理方式,尤其在网页中,大家会经常看到用到。但是这种方式在手机移动端就显得不是很友好。例如,下拉菜单究竟显示多大?又比如,如果列表很长怎么办?而显然dialog就是早期android团队用于Spinner前端体验改进的一次试探。之所以称为试探,是因为小白觉得dialog并不是很成功的一次改进,依然留有缺陷。首先对比一下两者的界面样子。

图3 dropdown模式和dialog模式

而两者的代码差别,仅在于spinnerMode属性的不同,默认是dropdown。

<Spinner
    android:id="@+id/weekday"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:spinnerMode=“dialog"/>

而正因为两种模式的存在,所以Spinner的属性也会分两套在不同模式分别起作用.

Spinner Dropdown模式下的属性

Spinner在dropdown模式下,有如下属性:

  1. dropDownWidth属性:这个属性用来控制下拉菜单的宽度,如果你选项元素文本文字比较少,你可以设置缩减。

  2. dropDownVerticalOffset属性:这个属性用于控制下拉菜单的垂直偏移。通常如上面例子中,下拉菜单出现会遮住选中元素,通过这个属性你就可以控制下拉菜单移动一个位置。

  3. dropDownHorizontalOffset属性:这个属性类似dropDownVerticalOffset用于控制下拉菜单水平偏移。

  4. dropDownSelector属性:这个属性用于设置选项元素被单击选中时候的状态变化效果。但是亲测试,除非特别设置,这个属性基本不起作用。

  5. popupBackground属性:设置下拉菜单这个菜单背景颜色。

其中小白以dropDownVerticalOffset属性为例,看一下设置和不设置的效果:

                                           图4 dropDownVerticalOffset的设置与不设置

Spinner Dialog模式下的属性

spinner在dialog模式下就只有一个属性:

prompt属性- 该属性主要用于给弹出模态框一个标题。这个属性很奇怪的地方是,它不接受直接设置字符串,例如prompt=“请选择”这样就会报错。但是接受android:prompt=“@string/spinner_prompt”这样的设置。那么小白想,它是不是也可以接收drawable以及layout类型呢?小白留给你们了。这个效果如下:

                          

                                              图5 prompt设置

仿dropdown的滚动选择器

正如小白在前面提到的,Spinner无论是dropdown还是dialog模式,在移动可视化上都不尽人如意。主要体现在一些尺寸设置上以及列表项略多的问题。但实际上,确实存在比较好的选择器的设计方案,那就是滚动选择。用IOS的用户应该是比较熟悉了。从用户体验的角度,这种设计的选择框是比较不错的选择。当然小白不能说它没有问题。它最大的问题在于苹果的专利。很不幸,据小白所知苹果对滚动选择等UI外观设计作了专利申请。哎,找一个好用的怎么那么难呢!!!

下面简单介绍一款, 来自jaaksi的pickerview:github.com/jaaksi/pick…。 大神啊,膜拜一下!

下面介绍其中一款控件OptionPicker的用法:

  • 在build.gradle引入包:

    implementation 'org.jaaksi:pickerview:3.0.2'

  • 你需要一个仿输入的TextView,用于显示选中元素信息。

  • 你需要定义OptionDataSet用于存储数据。

    package com.white7code.mobile.android.basecourse;

    import org.jaaksi.pickerview.dataset.OptionDataSet;

    import java.util.List;

    public class WeekDayOption implements OptionDataSet {

    private String value;
    
    public WeekDayOption(String value){
        this.value = value;
    }
    
    @Override
    public List<WeekDayOption> getSubs() {
        return null;
    }
    
    @Override
    public CharSequence getCharSequence() {
        return this.value;
    }
    
    @Override
    public String getValue() {
        return this.value;
    }
    

    }

  • 你需要创建TextView单击事件,构建触发OptionPicker显示。

    package com.white7code.mobile.android.basecourse;

    import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.TextView;

    import androidx.appcompat.app.AppCompatActivity;

    import org.jaaksi.pickerview.dataset.OptionDataSet; import org.jaaksi.pickerview.picker.OptionPicker;

    import java.util.ArrayList; import java.util.List;

    public class SpinnerViewActivity extends AppCompatActivity {

    private TextView spinnerSimulator;
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_spinner_view);
        if (savedInstanceState == null) {
            this.initSpinnerSimulator();
        }
    }
    private void initSpinnerSimulator() {
        final List<WeekDayOption> options = new ArrayList<>(7);
        options.add(new WeekDayOption("星期一"));
        options.add(new WeekDayOption("星期二"));
        options.add(new WeekDayOption("星期三"));
        options.add(new WeekDayOption("星期四"));
        options.add(new WeekDayOption("星期五"));
        options.add(new WeekDayOption("星期六"));
        options.add(new WeekDayOption("星期天"));
    
        final Activity self = this;
        this.spinnerSimulator = findViewById(R.id.spinner_simulator);
        this.spinnerSimulator.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                OptionPicker optionPicker = new OptionPicker.Builder(self, 1, new OptionPicker.OnOptionSelectListener() {
                    @Override
                    public void onOptionSelect(OptionPicker picker, int[] selectedPosition, OptionDataSet[] selectedOptions) {
                        if (selectedOptions != null && selectedOptions.length > 0) {
                            spinnerSimulator.setText(selectedOptions[0].getValue());
                        }
                    }
                }).create();
                optionPicker.setData(options);
                optionPicker.show();
            }
        });
    }
    

    }

最终你将得到如图6的效果:

                                    

                                                           图6 滚动选择器

结论

到这里就是小白关于Spinner一些皮毛用法的一些基础介绍。其实小白觉得还有很多方面值得去深挖,受限于个人精力,只能点到为止。需要这些慢慢的知识点沉淀对大家有所参考,有所帮助。也希望小白这个真正的android小白可以通过这种方式可以真正掌握到android原生开发的一些精髓。