Android控件与布局——基础控件Spinner

        最近在用原生的控件和布局绘制一些界面并使用,虽然这些都是Android基本知识,但是有的时候真的感觉力不从心,感觉有必要对Android常用的控件和布局做一个系统的了解。后续一个月甚至更多的时间都会围绕这个主题展开,毕竟这里面还是有不少高级控件的,我也会尽量结合应用深入的进行了解。

项目GitHub地址入口

       上一篇:CheckedTextView      下一篇:ProgressBar

今天,我们的主题是Spinner,下面看一下官方文档的介绍:同时只能展示一个子View,你可以选择不同的子view显示,这些数据来自于该控件的适配器。

* A view that displays one child at a time and lets the user pick among them.
* The items in the Spinner come from the {@link Adapter} associated with
* this view.

可见,这个View是一个ViewGroup,类似于ListView这种,我们可以通过为其设置适配器来展示不同的数据,但是它同时只能展示一条数据,下面就来看看它的用法。

1,基本用法

基本用法中,我们简单介绍一下这个控件的最基本使用,不通过自定义适配器来装载数据,同时还会介绍一些该控件的部分API等

1.1 通过xml文件配合控件属性填充数据

这种方式我们只需要提前编辑好Spinner中的数据(在strings.xml中的string-array标签)配合Spinner控件的android:entries

       <Spinner
            android:id="@+id/spinner_year"
            android:entries="@array/year_list"
            android:tag="year"
            android:layout_width="240dp"
            android:layout_height="wrap_content" />

其中,year_list数据如下:

 <string-array name="year_list">
        <item>年</item>
        <item>1990</item>
        <item>1991</item>
        <item>1992</item>
        <item>1993</item>
        <item>1994</item>
        <item>1995</item>
    </string-array>

运行的效果如下

这种方式使用简单,但是有一个问题,我们没有办法动态的修改数据,于是我们就有了另外一种加载数据的方式——动态加载:

1.2 动态加载数据

使用动态加载,我们需要执行以下几步:

  • 获取(初始化)显示数据
private void initListData() {
        list = new ArrayList<>();
        list.add("年");
        list.add("1990");
        list.add("1991");
        list.add("1992");
        list.add("1993");
        list.add("1994");
        list.add("1995");
    }
  • 绑定数据到系统自定义的适配器
 adapter= new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, list);
  • 设置适配器的drop down view的显示样式
  adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  • 加载适配器到Spinner
yearSpinner.setAdapter(adapter);

 

执行之后的效果和上图是一样的效果,那我们的刚刚提到的动态的改变加载数据功能还没有实现了,不急,其实非常之简单,我们只需要修改绑定到适配器上的数据即可实现对Spinner显示数据的修改,不需要调用任何接口:

我们增加两个按钮,布局代码和点击逻辑实现如下:

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="addData"
            android:text="增加数据" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="deleteData"
            android:text="删除数据" />
    public void addData(View view){
       list.add(String.valueOf(++maxYear));
    }

    public void deleteData(View view){
      list.remove(String.valueOf(maxYear--));
    }

运行的效果如下:

可见,通过动态的数据加载,我们可以很轻易的实现数据的更新。到这里,我们也实现的数据的动态加载,但是,我们对动态加载数据中绑定数据到适配器以及设置适配器中的样式实现有一点疑惑,现在我们把上面的第二和第三步重新拿来看看:

        //获取适配器
        adapter= new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, list);
        //设置样式
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

仔细看看,怎么觉得ArrayAdapter那么眼熟,不行,我们得到源码中看看究竟是什么导致我们会产生这种熟悉的感觉:

* You can use this adapter to provide views for an {@link AdapterView},
* Returns a view for each object in a collection of data objects you
* provide, and can be used with list-based user interface widgets such as
* {@link ListView} or {@link Spinner}.

上述是关于ArrayAdapter的部分介绍,哦哦,原来如此,我们在学习ListView的时候也用到了这个适配器,我说怎么这么眼熟,者,由此,我们心里好像有了一种预感,那就是学习Spinner和ListView有很多相通点,其实就适配器这一块,基本上差不多。其实我们还可以在追的深一些:ArrayAdapter继承至BaseAdapter,BaseAdapter实现了ListAdapter与SpinnerAdapter;下面是BaseAdapter的介绍:

/**
 * Common base class of common implementation for an {@link Adapter} that can be
 * used in both {@link ListView} (by implementing the specialized
 * {@link ListAdapter} interface) and {@link Spinner} (by implementing the
 * specialized {@link SpinnerAdapter} interface).
 */

好了,到这里基本差不多了,不能太偏离主题,我们接着看ArrayAdapter的构造函数中的参数

  • 上下文对象
  • 子view布局
  • 数据

其中第一和第三个参数没有什么好说的,对于子View布局,我们可以到android包中的R.layout中常看相关的布局常量来使用,目前有:

static int activity_list_item
           
static int browser_link_context_header
           
static int expandable_list_content
           
static int preference_category
           
static int select_dialog_item
           
static int select_dialog_multichoice
           
static int select_dialog_singlechoice
           
static int simple_dropdown_item_1line
           
static int simple_expandable_list_item_1
           
static int simple_expandable_list_item_2
           
static int simple_gallery_item
           
static int simple_list_item_1
           
static int simple_list_item_2
           
static int simple_list_item_checked
           
static int simple_list_item_multiple_choice
           
static int simple_list_item_single_choice
           
static int simple_spinner_dropdown_item
           
static int simple_spinner_item
           
static int test_list_item
           
static int two_line_list_item

关于这个参数的介绍如下:

* @param resource The resource ID for a layout file containing a TextView to use when
*                 instantiating views.

这是一个包含TextView的布局,比如:

  • simple_list_item_1.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceListItemSmall"
    android:gravity="center_vertical"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:minHeight="?android:attr/listPreferredItemHeightSmall" />
<?xml version="1.0" encoding="utf-8"?>

<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/listPreferredItemHeightSmall"
    android:textAppearance="?android:attr/textAppearanceListItemSmall"
    android:gravity="center_vertical"
    android:checkMark="?android:attr/textCheckMark"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" />

上述中的设置下拉view样式也是同样的道理。这里就不在多说了。因为很多时候,我们都会使用自定义的布局样式。

 

2,自定义适配器

上面介绍了加载Spinner数据的两种方式,也对部分源码进行了分析,下面我们就来看看使用Spinner的重点——自定义适配器来显示自定义的布局。和ListView一样,Spinner展示数据的样式和数据都是通过通过适配器的getView()方法进行绑定,所以,我们只需要继承BaseAdapter或者其子类,重写getView()方法实现数据与布局的绑定即可。下面就来看看具体的操作。首先,我们先自定义一个适配器继承至我们上面说的BaseAdapter:

package aoto.com.commonwidgetandlayout.basic_widget.spinner;

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;
import java.util.List;

import aoto.com.commonwidgetandlayout.R;

/**
 * author:why
 * created on: 2019/5/18 12:06
 * description:
 */
public class MySpinnerAdaptor extends BaseAdapter{

    Context mContext;
    private List<String> mList;

    public MySpinnerAdaptor(Context context, List<String> list) {
        this.mContext = context;
        this.mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
    /**
     * 绑定数据与View
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView=LayoutInflater.from(mContext).inflate(R.layout.spinner_item, null);
        if(convertView!=null)
        {
            ImageView icon=convertView.findViewById(R.id.item_icon);
            TextView info=convertView.findViewById(R.id.item_info);
            icon.setImageDrawable(mContext.getDrawable(R.drawable.year_icon));
            info.setText(mList.get(position));
        }
        return convertView;
    }
}

自定义的布局很简单,就是一个ImageView加一个TextView组成:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/item_icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginTop="10dp" />

        <TextView
            android:id="@+id/item_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dp"
            android:layout_marginTop="10dp"
            android:textColor="@color/colorPrimary"
            android:textSize="20sp" />
    </LinearLayout>
</LinearLayout>

加载适配器的代码还是一样的,就是不用在设置适配器的布局样式了:

spinner.setAdapter(new MySpinnerAdaptor(this,list));

运行的结果如下:

这样,我们自定义布局的样式就出来了,虽然比较难看,但是功能都是实现了的。好的,到这里,我们总结一下实现步骤

  • 自定义布局文件
  • 自定义Spinner加载适配器
  • 初始化数据,初始化适配器实现自定义布局与数据的绑定
  • Spinner加载适配器在自定义布局中显示我们的数据
  • 其他:可以为Spinner添加OnItemSelectedListener监听等等

下面,我们就结合上面的步骤,优化一下上面的小例子:

步骤1:自定义布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <RelativeLayout
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/item_icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginTop="10dp" />
        <TextView
            android:id="@+id/result_info"
            android:textColor="#FF0000"
            android:visibility="invisible"
            android:textSize="20sp"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="30dp" />

        <TextView
            android:layout_toRightOf="@+id/item_icon"
            android:layout_marginLeft="20dp"
            android:id="@+id/item_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:textColor="@color/colorPrimary"
            android:textSize="20sp" />
    </RelativeLayout>

    <TextView
        android:layout_marginTop="5dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_width="match_parent"
        android:layout_height="1.5dp"
        android:background="@color/colorAccent" />
</LinearLayout>

步骤二:自定义Spinner适配器

package aoto.com.commonwidgetandlayout.basic_widget.spinner;

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;
import java.util.List;

import aoto.com.commonwidgetandlayout.R;

/**
 * author:why
 * created on: 2019/5/18 12:06
 * description:
 */
public class MySpinnerAdaptor extends BaseAdapter{

    Context mContext;
    private List<String> mList;

    public MySpinnerAdaptor(Context context, List<String> list) {
        this.mContext = context;
        this.mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
    /**
     * 绑定数据与View
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView=LayoutInflater.from(mContext).inflate(R.layout.spinner_item, null);
        if(convertView!=null)
        {
            ImageView icon=convertView.findViewById(R.id.item_icon);
            TextView info=convertView.findViewById(R.id.item_info);
            TextView resultInfo=convertView.findViewById(R.id.result_info);
            icon.setImageDrawable(mContext.getDrawable(R.drawable.year_icon));
            info.setText(mList.get(position));
            resultInfo.setText(mList.get(position));
        }
        return convertView;
    }
}

步骤三:初始化数据,初始化适配器实现自定义布局与数据的绑定

 private void initListData() {
        list = new ArrayList<>();
        list.add("1990");
        list.add("1991");
        list.add("1992");
        list.add("1993");
        list.add("1994");
        list.add("1995");
    }

new MySpinnerAdaptor(this,list)

步骤四:Spinner加载适配器在自定义布局中显示我们的数据

spinner.setAdapter(new MySpinnerAdaptor(this,list));

步骤五:为Spinner添加OnItemSelectedListener监听,执行具体逻辑

spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
           @Override
           public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
               view.findViewById(R.id.item_icon).setVisibility(View.INVISIBLE);
               view.findViewById(R.id.item_info).setVisibility(View.INVISIBLE);
               view.findViewById(R.id.result_info).setVisibility(View.VISIBLE);
               Log.e(TAG, "onItemSelected: " );
           }

           @Override
           public void onNothingSelected(AdapterView<?> parent) {

           }
       });

这样设计之后,我们的代码运行效果如下:

 

也可以通过设置android:spinnerMode属性来设置子选项时下拉drop down还是弹出dialog方式:

    <Spinner
        android:id="@+id/spinner_year"
        android:spinnerMode="dialog"
        android:overlapAnchor="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="year" />

这里有几点需要注意:

(1)我是通过RelativeLayout布局中View的隐藏和显示来实现选中项和非下拉项的显示区别,有时会画面抖动,需注意

(2)我这里直接用TextView实现分割线的操作,建议使用自定义View

(3)通常展开子选项的时候会遮挡上次选中项,使用android:overlapAnchor属性配置一下解决,我在源码中找了一下注释,没有任何有用的解释,其中文意思是“重叠锚”,这里设置为false,应该指的是子选项不重叠选中项的意思

好了,到这里,关于Spinner的介绍就介绍了,如果喜欢的话,可以扫码关注一波,谢谢啦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章