第七章 Spinner 與 ListView

簡易的下拉式選單在Android稱為Spinner, 較為複雜的選單使用ListView. 不論是Spinner還是ListView, 都需搭配Adapter

Adapter常用的有三種, ArrayAdapter, SimpleAdapter, BaseAdapter

Spinner

為什麼會有下拉式選單? 就是因為有多個項目供使用者選擇, 但又太佔顯示的空間, 所以就設計了只會佔一行空間的元件, 而當使用者按下三角型, 才會列出所有的選項.

那如何把那麼多的選項靈活的放進Spinner裏呢, 這就是頭痛的地方.
1. 用最原始的字串陣列方法, 把要列出的選項全放進字串陣列中.
2. 設計一個要顯示Spinner的畫面layout.
3. 字串陣列及layout 放到Adapter調節器裏.
4. Adpater設定到Spinner中

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        String[] str={"水星", "金星", "地球", "火星", "木星", "土星", "天王星", "海王星", "冥王星"};
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Spinner spinner=(Spinner)findViewById(R.id.spinner);
        spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, str));
    }
}

ArrayAdapter

Adapter 俗稱變壓器, 而在程式裏叫調節器. 這真的是個神奇的東西, 可以把110v的電壓轉成220v, 還可以直接轉成5v供手機充電, 也可以把20A轉成2A, 真不知這是怎麼搞出來的. 反正就是把字串跟layout傳給這調節器, 然後插上Spinner(手機), 就可以充電囉.
Adapter<>(), 一看就知道使用泛型(Java SE 5.0新增的功能). 第一個參數為Context,  第二個參數是顯示的Layout, 第三個是要顯示的項目

Layout可以是系統預設的二個選項, 如下所述. 當然, 也可以寫自己想要的layout

android.R.layout.simple_spinner_dropdwon_item : 空間比較大
android.R.layout.smiple_spinner_item : 空間比較小

第三個參數就學問更大了, 可以使用二種方式

陣列
若更改陣列裏的資料, 再使用adapter.notifyDataSetChanged(), 是會改變Spinner裏的項目. 但因為陣列不能新增刪除, 所以大部份都使用下述說明的List.

List
用add, remove等變更List, 再使用adapter.notifyDataSetCahnged(), 則Spinner也會跟著變更. 請注意, 需要使用原來的List.  若重新建立List, notify會無作用的‧

public class MainActivity extends AppCompatActivity {
    List<String> list;
    String [] str1={"水星", "金星", "地球", "火星", "木星", 
                    "土星", "天王星", "海王星", "冥王星"};
    String [] str2={"Mercury", "Venus", "Earth", "Mars", "Jupiter", 
                    "Saturn", "Uranus", "Neptune", "Pluto"};
    Spinner spinner;
    ArrayAdapter<String> adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list=new ArrayList<>();
        for (String s:str1){list.add(s);}
        adapter=new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, list);
        spinner=(Spinner)findViewById(R.id.spinner);
        spinner.setAdapter(adapter);
        Button btC=(Button)findViewById(R.id.btChinese);
        Button btE=(Button)findViewById(R.id.btEnglish);
        btC.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                list.clear();
                for (String s:str1){list.add(s);}
                adapter.notifyDataSetChanged();
            }
        });
        btE.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                list.clear();
                for (String s:str2){list.add(s);}
                adapter.notifyDataSetChanged();
            }
        });
    }
}

 Spinner元件外觀

使用android:textSize設定Spinner的字体大小時, 可以發現根本是無效的. Spinner使用Adapter設定外觀Layout及項目, 外觀可以自行設定, 所以要設定字体大小時,就需由外觀來設定. 於res/layout按右鍵/new/layout resource file, 設定一個檔案名, 如spinner_down. 然後設定如下

<?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:maxLines="1"
    android:textSize="24sp"
    >

</TextView>

在產生Adapter時, 就可以傳入自己設定的layout :
Adapter<String> adapter=new ArrayAdapter<>(this, R.layout.spinner_down, list);
若只需設定項目的文字, 可以使用adapter.setDropDownViewResource(layout);

Spinner選取事件

Spinner可以使用OnItemSelectedListener監聽選取的事件, 如下程式碼所示

spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
    }
    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
    }
});

Spinner提供常用的方法
getSelectedItemPosition() : 取得選取的編號
getSelectedView() : 取得目前選項的View物件
getCount() : 取得項目數量
getSelection(int) : 設定目前選擇項目的編號, 第一個為0

ListView

listView同spinner, 將顯示項目及Layout傳入Adapter, 再將Adapter設定給ListView即可使用. ListView可使用複雜的元件, 比如加入核取方塊等. 若要顯示的項目只有一種, 則用ArrayAdapter就可以, 但如果項目有多種的話, 就要使用SimpleAdapter或BaseAdapter

ListView能表達的結構, 說穿了也就是二維陣列的資料, ListView每列所顯示的資料的就是陣列每列的欄位.

SimpleAdapter

SimpleAdapter實作BaseAdapter抽像類別. 因為BaseAdapter蠻複雜的, 所以如果是簡易的ListView, 則使用SimpleAdapter就可以應付. 其參數說明如下
1. Context
2. 包含HashMap的List
3. 自訂的Layout
4. 對應HashMap的String
5. 自訂Layout的id

public class MainActivity extends AppCompatActivity {
    String [][]str={
            {"包子", "雞豬牛羊肉","20"},
            {"蘿蔔糕", "用菜頭作的","40","carrot"},
            {"蛋餅", "鴕鳥蛋煎的","40", "egg"},
            {"漢堡", "比臉還大的漢堡","50", "hammber"},
            {"炒麵", "是用炒的","50", "noodle"},
            {"香腸", "傳統小吃","60", "sausage"},
            {"壽司", "日本新鮮空運來台","70","sushi"},
            {"吐司", "上面有灑蔥花, 還有加蛋","20", "toast"},
            };
    int[] img={R.drawable.bun, R.drawable.carrot, R.drawable.egg, R.drawable.hammber,
              R.drawable.noodle, R.drawable.sausage, R.drawable.sushi, R.drawable.toast};
    HashMap<String, String> hashMap;
    List<HashMap<String, String>> list;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list=new ArrayList<>();
        int index=0;
        for (String[] s:str){
            hashMap=new HashMap<>();
            hashMap.put("Title", s[0]);
            hashMap.put("Description", s[1]);
            hashMap.put("Price", s[2]);
            hashMap.put("Image", Integer.toString(img[index++]));
            list.add(hashMap);
        }
        ListView listView=(ListView)findViewById(R.id.listView);
        SimpleAdapter adapter=new SimpleAdapter(
                this,
                list,
                R.layout.list_view,
                new String[]{"Image","Title", "Description", "Price"},
                new int[]{R.id.image, R.id.txtTitle, R.id.txtDescription, R.id.txtPrice});
        listView.setAdapter(adapter);
    }
}

list_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <CheckBox
        android:layout_height="wrap_content"
        android:id="@+id/checkBox"
        android:layout_margin="5dp"
        android:layout_width="30dp" />
    <ImageView
        android:layout_height="100dp"
        app:srcCompat="@drawable/bun"
        android:id="@+id/image"
        android:layout_weight="1"
        android:layout_width="200dp" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1">
        <TextView
            android:text="TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:id="@+id/txtTitle" />
        <TextView
            android:text="TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:id="@+id/txtDescription" />
    </LinearLayout>
    <TextView
        android:text="TextView"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:id="@+id/txtPrice" />
</LinearLayout>

 android_listview


BaseAdapter

BaseAdapter為一抽像類別, 但如果要搞的比較花招一點, 比如在ListView中, 一列紅色, 一列綠色這種交互顯示, 那還是只有BaseAdapter才有辦法.

android_baseadapter

public class MainActivity extends AppCompatActivity {
    String[][] str = {
            {"包子", "雞豬牛羊肉", "20"},
            {"蘿蔔糕", "用菜頭作的", "40", "carrot"},
            {"蛋餅", "鴕鳥蛋煎的", "40", "egg"},
            {"漢堡", "比臉還大的漢堡", "50", "hammber"},
            {"炒麵", "是用炒的", "50", "noodle"},
            {"香腸", "傳統小吃", "60", "sausage"},
            {"壽司", "日本新鮮空運來台", "70", "sushi"},
            {"吐司", "上面有灑蔥花, 還有加蛋", "20", "toast"},
    };
    int[] img = {R.drawable.bun, R.drawable.carrot, R.drawable.egg, R.drawable.hammber,
            R.drawable.noodle, R.drawable.sausage, R.drawable.sushi, R.drawable.toast};
    HashMap<String, String> hashMap;
    List<HashMap<String, String>> list;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = new ArrayList<>();
        int index = 0;
        for (String[] s : str) {
            hashMap = new HashMap<>();
            hashMap.put("Title", s[0]);
            hashMap.put("Description", s[1]);
            hashMap.put("Price", s[2]);
            hashMap.put("Image", Integer.toString(img[index++]));
            list.add(hashMap);
        }
        ListView listView = (ListView) findViewById(R.id.listView);
        listView.setAdapter(new MyAdapter(this));
    }
    class MyAdapter extends BaseAdapter {
        private LayoutInflater mInflater;
        public MyAdapter(Context context){
            this.mInflater = LayoutInflater.from(context);
        }
        @Override
        public int getCount() {
            return list.size();
        }

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

        @Override
        public long getItemId(int position) {
            return 0;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup viewGroup) {
            ViewHolder holder;
            if (convertView == null) {
                holder=new ViewHolder();
                convertView = mInflater.inflate(R.layout.list_view, null);
                holder.image = (ImageView)convertView.findViewById(R.id.image);
                holder.txtTitle = (TextView)convertView.findViewById(R.id.txtTitle);
                holder.txtDescription = (TextView)convertView.findViewById(R.id.txtDescription);
                holder.txtPrice=(TextView)convertView.findViewById(R.id.txtPrice);
                convertView.setTag(holder);

            }else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.image.setImageResource(Integer.valueOf(list.get(position).get("Image")));
            holder.txtTitle.setText(list.get(position).get("Title"));
            holder.txtDescription.setText(list.get(position).get("Description"));
            holder.txtPrice.setText(list.get(position).get("Price"));
            if(position%2==0)
                convertView.setBackgroundColor(Color.rgb(0xaa,0xaa,0xaa));
            else
                convertView.setBackgroundColor(Color.rgb(0xcc, 0xcc, 0xcc));
            return convertView;
        }
        public final class ViewHolder {
            public CheckBox cb;
            public ImageView image;
            public TextView txtTitle, txtDescription, txtPrice;
        }
    }
}

實作BaseAdapter時, 依如下步驟完成
1. 覆蓋如下方法

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

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

2. 將自定Layout中要顯示的欄位以ViewHolder內部類別記錄

    public final class ViewHolder {
        public CheckBox cb;
        public ImageView image;
        public TextView txtTitle, txtDescription, txtPrice;
    }

3. 覆蓋 public View getView()方法

ListView中的每一列, 都是一個View, 將設定改裝好的View, 以convertView變數返回

實作

底下是一個實際的例子, ListView中有TextView及Switch. 此例實際說明如何控制Switch的事件. 並且在其中加入MySQL資料庫的控制方式

listview

下面代碼其實是控制ListView的 OnItemClickListener. 使用所取得的View, 調用findViewById方法, 獲取每一個子項目的元件. 然後設定Switch的開關.

請注意, 必需把Switch的focusable及clickable設為false, 否則ListView會無法獲取事件

package com.asuscomm.mahaljsp.projectcontrol;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.Switch;
import android.widget.TextView;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    String url="jdbc:mysql://ip/db?useSSL=false";
    String account="account";
    String password="password";
    ListView listView;
    List<HashMap<String, String>> list=new ArrayList<>();
    HashMap<String, String> hashMap;
    MyAdapter adapter;
    int mEnabled=0;
    String mName;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView=(ListView)findViewById(R.id.listView);
        adapter=new MyAdapter(this);
        listView.setAdapter(adapter);

        try {
            Class.forName("com.mysql.jdbc.Driver");
        }
        catch(ClassNotFoundException e){
                Log.d("Thomas", e.getMessage());
        }
        new Thread(()->{
            Connection conn=null;
            try {
                conn= DriverManager.getConnection(url, account, password);
                Statement stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery("select * from project");
                while (rs.next()) {
                    HashMap<String, String>m=new HashMap<>();
                    m.put("id", rs.getString("id"));
                    m.put("item", rs.getString("projectName"));
                    m.put("check", rs.getString("enabled"));
                    list.add(m);
                    MainActivity.this.runOnUiThread(()->{
                        adapter.notifyDataSetChanged();
                    });
                }
                rs.close();
            }
            catch (SQLException e){
                Log.d("Thomas", e.getMessage());
            }
            finally {
                if(conn!=null)try {conn.close();} catch (SQLException e) {}
            }
        }).start();

        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Switch sw=(Switch)view.findViewById(R.id.sw);
                mName=((TextView)view.findViewById(R.id.txtItem)).getText().toString();
                if(sw.isChecked()){
                    sw.setChecked(false);
                    mEnabled=0;
                }
                else{
                    sw.setChecked(true);
                    mEnabled=1;
                }
                new Thread(()->{
                    Connection conn= null;
                    try {
                        conn = DriverManager.getConnection(url, account, password);
                        Statement stmt = conn.createStatement();
                        stmt.execute(String.format("update project set enabled=%s where projectName='%s'", mEnabled, mName));
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                    finally {
                        if(conn!=null)try {conn.close();} catch (SQLException e) {}
                    }
                }).start();
            }
        });
    }
    class MyAdapter extends BaseAdapter {
        private LayoutInflater mInflater;
        public MyAdapter(Context context){
            this.mInflater = LayoutInflater.from(context);
        }
        @Override
        public int getCount() {
            return list.size();
        }
        @Override
        public Object getItem(int position) {
            return list.get(position);
        }
        @Override
        public long getItemId(int position) {
            return 0;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup viewGroup) {
            ViewHolder holder;
            if (convertView == null) {
                holder=new ViewHolder();
                convertView = mInflater.inflate(R.layout.list_layout, null);
                holder.txtId = (TextView)convertView.findViewById(R.id.txtId);
                holder.txtItem = (TextView)convertView.findViewById(R.id.txtItem);
                holder.sw=(Switch) convertView.findViewById(R.id.sw);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.txtId.setText(list.get(position).get("id"));
            holder.txtItem.setText(list.get(position).get("item"));
            if(list.get(position).get("check").equals("1"))
                holder.sw.setChecked(true);
            else
                holder.sw.setChecked(false);
            if(position%2==0)
                convertView.setBackgroundColor(Color.rgb(0xcc,0xcc,0x00));
            else
                convertView.setBackgroundColor(Color.rgb(0x00, 0xcc, 0xcc));
            return convertView;
        }
        public final class ViewHolder {
            public TextView txtId, txtItem;
            public Switch sw;
        }
    }
}

activity_main.xml

主畫面設定如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

list_layout.xml

ListView裏的每個子元件畫面如下

<?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:gravity="center"
    android:orientation="horizontal">
    <TextView
        android:id="@+id/txtId"
        android:textSize="16sp"
        android:layout_width="50dp"
        android:layout_marginLeft="5dp"
        android:layout_height="100dp"
        android:gravity="center"/>
    <TextView
        android:id="@+id/txtItem"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="16sp"/>
    <Switch
        android:id="@+id/sw"
        android:layout_width="80dp"
        android:checked="true"
        android:focusable="false"
        android:clickable="false"
        android:layout_height="wrap_content" />
</LinearLayout>

todo

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *