65.9K
CodeProject 正在变化。 阅读更多。
Home

Spinner 控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年9月13日

CPOL

9分钟阅读

viewsIcon

9018

downloadIcon

143

一个 Spinner 控件中的选择如何决定另一个 Spinner 的内容。

引言

在带有“Android”标签的“快速问答”版块中,我经常看到一个类似的问题:“如何让一个 spinner 的值(或列表)根据另一个 spinner 的选择而改变?”嗯……

很多年前,我做了一个“销售税”应用程序,用于处理我所在州的在线税表。它把所有的县放在一个 spinner 控件中,把所有的城镇放在另一个 spinner 控件中。有 77 个县和数百个城镇,这组合起来数量庞大。除了记住给定县的所有城镇,或者包含给定城镇的所有县,我让每个 spinner 控件的内容响应另一个的选择。一旦两者都有选择,并且输入了价格,就会显示总价和所有销售税(州、县、镇)。它运行得相当不错,而且容纳这一切的数据结构也并不复杂。

鉴于此,我认为一篇比我上面描述的简单得多的文章会是一个受欢迎的补充。在上述问题中,每一个似乎都从不同的地方获取数据。一个会把数据存储在项目的 strings.xml 文件中,另一个会把数据存储在 SQLite 数据库中。在本文中,我将展示三种不同的数据存储方式,如何从中提取数据,以及如何响应 spinner 控件的选择。

几乎任何东西都可以用来填充这两个 spinner 控件:汽车品牌和型号、体育队和球员、国家和语言、州和邮政编码或区号等。在这个练习中,我选择了印度的州和地区。没有其他原因,就是因为数据量很大,而且提问者似乎都来自那里,我从 这里 获得了数据。让我们开始吧!

哈希

HashMap 实现 Map 接口,并将数据存储为键值对。这使得州作为,其地区列表作为的存储媒介非常完美。这给我们带来了类似这样的东西:

HashMap<String, ArrayList<String>> m_hashStates = new HashMap<String, ArrayList<String>>(36);

我选择预先分配是因为我提前知道有多少个州。对于如此小的规模,这可能不会产生任何显著差异。

填充这个 HashMap 非常简单,而且由于这会多次完成,我把它封装在一个函数里,就像这样:

private ArrayList<String> addState( String strName )
{
    ArrayList<String> arrDistricts = new ArrayList<String>();

    try
    {
        m_hashStates.put(strName, arrDistricts);
    }
    catch(Exception e)
    {
        Log.e("Test2", "Error adding state: " + e.getMessage());
    }

    return arrDistricts;
}

请注意,添加到映射中并且返回给调用者的空数组。因此,要添加一个州及其地区到 HashMap,您只需调用该函数,就像这样:

ArrayList<String> arrDistricts = addState("Goa");
arrDistricts.add("North Goa");
arrDistricts.add("South Goa");
...
arrDistricts = addState("Tripura");
arrDistricts.add("Dhalai");
arrDistricts.add("Gomati");
arrDistricts.add("Khowai");
arrDistricts.add("North Tripura");
arrDistricts.add("Sepahijala");
arrDistricts.add("South Tripura");
arrDistricts.add("Unokoti");
arrDistricts.add("West Tripura");

我选择不将这些 string 存储在 strings.xml 文件中,因为它们既不需要翻译也不需要格式化。

在所有数据加载到 HashMap 后,我们需要将它的键(州名)添加到属于 spinner 控件的适配器中。幸运的是,ArrayAdapterArrayListHashMap 都提供了可以很好地结合在一起以完成此操作的构造函数和方法:

m_adapterStates.addAll(new ArrayList(m_hashStates.keySet()));

通常,在向 ArrayAdapter 对象添加项目时,我们会调用其 notifyDataSetChanged() 方法,以便附加的列表刷新自身。在这种情况下,这是不必要的,因为 addAll() 已经为我们完成了。

我们需要做的最后一件事是响应选择更改。这通过调用 spinner 的 setOnItemSelectedListener() 方法并提供一个 onItemSelected() 回调来完成。适配器中选定项目的索引存储在 position 变量中。有了这个值,我们就可以获得选定州(即 HashMap 中的键)的名称。然后,我们可以获得相关地区的列表并更新其 spinner 控件。这看起来像这样:

spinStates.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
    @Override
    public void onItemSelected( AdapterView<?> parent, View view, int position, long id )
    {
        // get the state that was selected and its list of districts
        String strState = (String) spinStates.getItemAtPosition(position);
        ArrayList<String> arrDistricts = m_hashStates.get(strState);

        if (arrDistricts.size() == 0)
            arrDistricts.add("No districts found for the selected state");
        else
            Collections.sort(arrDistricts);

        // update the adapter with the new districts
        adapterDistricts.clear();
        adapterDistricts.addAll(arrDistricts);
    }
}

当加载一个 spinner 控件并让它的选择决定另一个 spinner 控件中显示的内容时,这确实就是这么简单。我见过很多别人尝试实现这个的例子,但代码却复杂得多(曲折)。不要让事情比它需要的更难! 让我们来看一个稍微不同的例子。

JSON

如今,提到任何带有 Java 这个词的东西,尤其是 JavaScript,似乎都会引起争论(就像说你曾经或仍然使用 [Visual]Basic 编程,或者在麦当劳吃饭一样)。每个人都有自己的观点,为什么它应该或不应该被使用,而且在大多数情况下,它们都是正确的。对我来说,如果一个工具能做它该做的事情并满足项目的所有要求,那么它就是正确的工具。总之,使用你熟悉和舒适的存储机制(例如 JSON、XML)。

在上一节中,州和地区的实现是代码本身中的字符串字面量。在本节中,我们将把数据移到一个单独的 JSON 文件中进行处理,然后像以前一样存储在 HashMap 中。这是该文件内容的一小部分:

{
    "Mizoram": [
        "Aizawl",
        "Champhai",
        "Kolasib",
        "Lawngtlai",
        "Lunglei",
        "Mamit",
        "Saiha",
        "Serchhip"
    ],

    "Nagaland": [
        "Dimapur",
        "Kiphire",
        "Kohima",
        "Longleng",
        "Mokokchung",
        "Mon",
        "Peren",
        "Phek",
        "Tuensang",
        "Wokha",
        "Zunheboto"
    ],
    ...
}

在我们的例子中,有 36 对这样的键值对,键是州名,值是该州地区列表的数组。这个文件将存储在项目的 assets 文件夹中,所以打开该文件并将其内容读取到 JSONObject 对象中(读起来总感觉有点不对劲)如下所示:

String strLine;
StringBuilder builder = new StringBuilder("");

InputStream is = getAssets().open("states_districts.json");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));

while ((strLine = reader.readLine()) != null)
    builder.append(strLine);

reader.close();
is.close();

JSONObject obj = new JSONObject(builder.toString());

此时,obj 包含了我们开始提取键及其值所需的一切。与我们在 Hash 部分之前所做的类似,我们需要遍历每个键(即州),并为每个找到的键,将其值列表(即地区)添加到 ArrayList 容器中。JSONObject 类有一个方便的方法 keys(),它会给出我们要遍历的键。然后,我们可以使用 getJSONArray() 方法来获取属于每个键的值。执行此操作的代码如下:

Iterator<String> keys = obj.keys();
while (keys.hasNext())
{
    String sName = keys.next();
    JSONArray arr = obj.getJSONArray(sName);

    ArrayList<String> arrDistricts = new ArrayList<String>(arr.length());

    for (int y = 0; y < arr.length(); y++)
        arrDistricts.add(arr.getString(y));

    // all of the districts for this state have been added, so sort them
    Collections.sort(arrDistricts);

    // now with a state name and an array of districts, we can update the HashMap
    m_hashStates.put(sName, arrDistricts);
}

响应州 spinner 控件的选择更改,onItemSelected() 方法与上面显示的相同,因此为简洁起见,我不在此重复。

SQLite

要讨论的最后一种方法涉及将数据存储在 SQLite 数据库中。这种方法与前几种方法既有相似之处,也有不同之处。添加到数据库的州和地区最初是字符串字面量,而不是用 HashMap 来存储州名和每个地区的地区名称列表,我们将把这些信息存储在两个 SQLite 表中。ArrayList 将再次用于存储州名,并与州的适配器关联。一个简单的数据库查询将检索地区名称。

数据库有两个表,每个表都有一个 _ID 和一个 NAME 字段。states 表中的 _ID 字段将是主键,并且是自增的。districts 表中的 _ID 字段将是带有约束启用的外键。

为了帮助管理数据库,需要一个 SQLiteOpenHelper 的扩展。该类的完整内容如下:

private class DatabaseHelper extends SQLiteOpenHelper
{
    private static final String DATABASE_NAME  = "junk.db";
    private static final int DATABASE_VERSION  = 1;
    public static final String STATES_TABLE    = "states";
    public static final String DISTRICTS_TABLE = "districts";

    // columns
    private static final String KEY_ID   = "_id";
    private static final String KEY_NAME = "name";

    private static final String STATES_TABLE_CREATE =
        "CREATE TABLE " + STATES_TABLE + " ("
            + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + KEY_NAME + " TEXT);";

    private static final String DISTRICTS_TABLE_CREATE =
        "CREATE TABLE " + DISTRICTS_TABLE + " ("
            + KEY_ID + " INTEGER REFERENCES " + STATES_TABLE + "(" + KEY_ID + "), "
            + KEY_NAME + " TEXT);";

    //============================================================

    public DatabaseHelper( Context context )
    {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    //============================================================

    @Override
    public void onConfigure( SQLiteDatabase db )
    {
        super.onConfigure(db);

        db.setForeignKeyConstraintsEnabled(true);
    }

    //============================================================

    @Override
    public void onCreate( SQLiteDatabase db )
    {
        try
        {
            db.execSQL(STATES_TABLE_CREATE);
            db.execSQL(DISTRICTS_TABLE_CREATE);
        }
        catch(SQLiteException e)
        {
            Log.e("Test2", "Error creating tables: " + e.getMessage());
        }
    }

    //============================================================

    @Override
    public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion )
    {
        Log.d("Test2", "Upgrading database from version " + 
        oldVersion + " to version " + newVersion);

        try
        {
            db.execSQL("DROP TABLE IF EXISTS " + STATES_TABLE);
            db.execSQL("DROP TABLE IF EXISTS " + DISTRICTS_TABLE);
        }
        catch(SQLiteException e)
        {
            Log.e("Test2", "Error dropping tables: " + e.getMessage());
        }

        onCreate(db);
    }
}

创建 DatabaseHelper 的实例将调用其 onConfigure()onCreate() 方法。之后,我们就可以开始向数据库添加州和地区了。这看起来像这样:

DatabaseHelper dbHelper = new DatabaseHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();
...
lStateId = addState(db, "Andaman and Nicobar");
addDistrict(db, lStateId, "Nicobar");
addDistrict(db, lStateId, "North and Middle Andaman");
addDistrict(db, lStateId, "South Andaman");

lStateId = addState(db, "Chandigarh");
addDistrict(db, lStateId, "Chandigarh");

lStateId = addState(db, "Dadra and Nagar Haveli");
addDistrict(db, lStateId, "Dadra and Nagar Haveli");

lStateId = addState(db, "Daman and Diu");
addDistrict(db, lStateId, "Daman");
addDistrict(db, lStateId, "Diu");

lStateId = addState(db, "Lakshadweep");
addDistrict(db, lStateId, "Lakshadweep");
...

addState() 方法与上面 Hash 部分显示的方法不同。简而言之,它在一个事务中向 states 表添加一行。新添加行的标识符会返回给调用者(以便可以将链接的行添加到 districts 表中)。

private long addState( SQLiteDatabase db, String sName )
{
    long lRow = -1;

    try
    {
        db.beginTransaction();

        ContentValues values = new ContentValues();
        values.put(DatabaseHelper.KEY_NAME, sName);

        lRow = db.insert(DatabaseHelper.STATES_TABLE, null, values);

        db.setTransactionSuccessful();
    }
    catch(SQLiteException e)
    {
        Log.e("Test2", "Error adding state: " + e.getMessage());
    }
    finally
    {
        db.endTransaction();
    }

    return lRow;
}

addDistrict() 方法看起来差不多,除了它向 districts 表添加一行,如下所示:

private void addDistrict( SQLiteDatabase db, long lStateId, String sName )
{
    try
    {
        db.beginTransaction();

        ContentValues values = new ContentValues();
        values.put(DatabaseHelper.KEY_ID, lStateId);
        values.put(DatabaseHelper.KEY_NAME, sName);

        db.insert(DatabaseHelper.DISTRICTS_TABLE, null, values);

        db.setTransactionSuccessful();
    }
    catch(SQLiteException e)
    {
        Log.e("Test2", "Error adding district: " + e.getMessage());
    }
    finally
    {
        db.endTransaction();
    }
}

此时,敏锐的观察者会注意到,每个表的行插入都包含在一个事务中。这是一个好坏参半的情况。好处是,如果发生错误,只有一行会受到影响;其他行可以独立插入。坏处是,我进行的一些初步测试表明,行插入需要花费可衡量的时。作为回应,我最终将行插入代码放入了一个 AsyncTask 类中,以帮助释放 UI。您可以在附加的源代码中找到它。

现在州和地区已添加到数据库中,我们需要对 states 表执行“select all”查询,其中结果集返回的每个州名都将添加到与州的适配器关联的数组中。这看起来像这样:

SQLiteDatabase db =  dbHelper.getReadableDatabase();
Cursor cursor = db.query(DatabaseHelper.STATES_TABLE,
                         new String[]{ DatabaseHelper.KEY_ID, DatabaseHelper.KEY_NAME },
                         null,
                         null,
                         null,
                         null,
                         DatabaseHelper.KEY_NAME);

if (cursor.moveToFirst())
{
    do
    {
        long id = cursor.getLong(cursor.getColumnIndex(DatabaseHelper.KEY_ID));
        String sName = cursor.getString(cursor.getColumnIndex(DatabaseHelper.KEY_NAME));

        arrStates.add(new StateInfo(id, sName));

    } while (cursor.moveToNext());

    cursor.close();
}

db.close();

作为对上述查询的替代,可以在 addState() 方法中修改 arrStates 数组。

arrStates 中包含的 StateInfo 对象只是一个小的容器类,用于保存 states 表中每一行的名称和标识符。它还包含一个 toString() 方法,当州的适配器需要州名显示时,该方法就会被调用。

private class StateInfo
{
    private long m_lStateId;
    private String m_sName;

    public StateInfo( long lStateId, String sName )
    {
        m_lStateId = lStateId;
        m_sName    = sName;
    }

    @Override
    public String toString()
    {
        return m_sName;
    }
}

最后,我们需要响应州 spinner 控件中的选择更改。对于上面的 HashJSON 部分,我们做的第一件事是使用传递给 onItemSelected() 的 position 值在 HashMap 中查找选定的州名并获取其地区列表。由于州和地区信息现在来自数据库,因此方法略有不同。我们将使用选定州的行 ID 作为 WHERE 子句,对 districts 表执行 SQL 查询。然后,对于结果集中返回的每个地区名称,我们将每个名称添加到与地区适配器关联的数组中。

spinStates.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
    @Override
    public void onItemSelected( AdapterView<?> parent, View view, int position, long id )
    {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        arrDistricts.clear();

        // find only those districts belonging to the selected state
        StateInfo si = arrStates.get(position);
        Cursor cursor = db.query(DatabaseHelper.DISTRICTS_TABLE,
                                 new String[]{ DatabaseHelper.KEY_NAME },
                                 DatabaseHelper.KEY_ID + " = ?",
                                 new String[]{ String.valueOf(si.m_lStateId) },
                                 null,
                                 null,
                                 DatabaseHelper.KEY_NAME);

        if (cursor.moveToFirst())
        {
            do
            {
                String sName = cursor.getString(cursor.getColumnIndex(DatabaseHelper.KEY_NAME));
                arrDistricts.add(sName);

            } while (cursor.moveToNext());

            cursor.close();
        }

        db.close();

        if (arrDistricts.size() == 0)
            arrDistricts.add("No districts found for the selected state");

        adapterDistricts.clear();
        adapterDistricts.addAll(arrDistricts);
    }
});

对于像这样的小型应用程序,其中查询的数据量相对较小(例如,给定州的最大地区数量仅为 75),这可能是一种可接受的方法。对于更大的数据集,或者在频繁发生选择更改的情况下,此查询可能会成为一个不受欢迎的瓶颈。您需要对所有三种方法进行度量,以确定哪种方法最适合您的需求。

结语

这是一次有趣的练习,我希望它能对未来(正在为 spinner 控件选择更改而挣扎)的读者有所帮助。我在处理 SQLite 部分时注意到的一件事是,将所有行插入州和地区表所需的时间。当我尝试在模拟器上测试它时,我只是期望时间比实际设备要慢一些。事实确实如此,但令我感到困惑的是,每次我在模拟器上运行应用程序时,完成时间都会增加几秒钟。我尝试了一些方法来找出可能的原因(例如,在每次运行前删除数据库,每次都删除并重新安装应用程序),但都没有得出明确的结论。如果您有理论,请在下方留言。

是的,该项目的名称是 Junk。因为这是一个周末的、一次性的项目,所以我没有采取所有步骤使其成为一个“漂亮”的应用程序。您还需要更改 AndroidManifest.xml 文件中的 <activity android:name=".MainActivity2"> 来演示这三种方法。

尽情享用!

© . All rights reserved.