安卓第八夜 瑪麗蓮夢露

Vamei發表於2014-08-13

作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段宣告。謝謝! 

 

上一講說明了資料庫中存取資料的方法。這一講將以條目的檢視方式,來以相似的檢視方式,顯示多個資料物件。這種方式特別適合於顯示從資料庫中取出的多個結構相似的資料,比如多個聯絡人,或者多個聯絡人分類。

《瑪麗蓮夢露》,這是一副現代藝術作品。聽到瑪麗蓮夢露自殺的訊息後,現代藝術家沃霍爾深為震驚。他通過重複瑪麗蓮夢露的形象,創作了這幅波普藝術的名作。每一個形象既是重複,又有變化。

 

描述

多個條目的檢視方式在應用中很常見,比如聯絡人目錄。我們經常會根據資料的數量,動態的調整顯示條目的個數。譬如一個社交應用顯示好友資訊。當好友數目增加或減少時,安卓需要動態的增加或減少顯示好友條目。我將介紹ListView和ListAdapter,兩者結合,可以動態的顯示條目。我將利用它們,建立一個條目頁面,顯示所有的聯絡人類別。相關知識點:

  • onClickListener介面。實現點選監聽的一種新方式。
  • ListView。這是一個View Group,用於包含多個條目。
  • ArrayAdapter。它讓資料以特定的條目檢視格式顯示出來。

 

Activity實施OnClickListener介面

我將修改MainActivity,增加一個按鈕,通向新的頁面。新的頁面中將包含條目檢視。在activity_main.xml中增加按鈕元素:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView 
        android:id="@+id/welcome"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
    <Button
        android:id="@+id/author"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Edit Profile" />
    
    <Button
        android:id="@+id/category"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Contact Categories" />
</LinearLayout>

上面id為category的元素為新增按鈕。

 

在MainActivity中監聽新的按鈕。之前的事件監聽方式,是將新建的OnClickListener物件傳遞給檢視元素。實際上,OnClickListener只是一個介面(interface)。我讓MainActivity實施OnClickListener介面,並讓MainActivity物件負責監聽:

package me.vamei.vamei;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener {
    private SharedPreferences sharedPref;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        sharedPref = this.getSharedPreferences("me.vamei.vamei", 
                Context.MODE_PRIVATE);
        
        Button btn1 = (Button) findViewById(R.id.author);
        btn1.setOnClickListener(this);
        Button btn2 = (Button) findViewById(R.id.category);
        btn2.setOnClickListener(this);        
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        TextView nameView = (TextView) findViewById(R.id.welcome);
        
        // retrieve content from shared preference, with key "name"
        String   welcome  = "Welcome, " + sharedPref.getString("name", "unknown") + "!";
        nameView.setText(welcome);
    }

    // method for interface OnClickListener
    @Override
    public void onClick(View v) {
        Intent intent;
        // Routing to different view elements
        switch(v.getId()) {
            case R.id.author:
                intent = new Intent(this, 
                        SelfEditActivity.class);
                startActivity(intent);
                break;
            case R.id.category:
                intent = new Intent(this,
                        CategoryActivity.class);
                startActivity(intent);
                break;
        }
    }
}

MainActivity實施了OnClickListener介面,因此也是一個OnClickListener型別的物件。OnClickListener介面有一個規定的方法onClick()。事件發生後,安卓將呼叫的該方法。我們用setOnClickListener的方法,讓MainActivity同時監聽兩個按鈕的點選事件。當事件觸發後,安卓呼叫onClick()方法。通過switch結構,安卓瞭解到底是哪個按鈕被點選,並針對不同的情況,啟動了不同的下游Activity。

我們當然也可以用之前的new OnClickListener()的方法,為兩個按鈕分別建立監聽物件,但會相對比較繁瑣。

 

可以看到,點選id為category的按鈕後,安卓將啟動CategoryActivity按鈕。這就是我們下一步將要編寫的。

 

使用ArrayAdapter

CategoryActivity將以條目的方式來顯示資料庫中儲存的所有Category,即聯絡人的類別。我在上一講中,已經將資料儲存到了SQLite資料庫中。我需要把資料取出,並放入到CategoryActivity的檢視中。

困難的地方在於,我無法預知資料庫中有多少個Category,因此,我沒法在設計佈局的時候靜態的說明所有的檢視元素。這個問題可以通過動態佈局的方式,用addView()方法,把檢視元素加到檢視樹中。檢視元素的動態新增,會導致安卓本身的效率會變慢。

我將使用ListView來重複利用構圖方式。ListView是一個View Group,用於管理多條佈局相似的檢視元素。例如:

可以看到,在ListView中,雖然每個條目的具體資料不同,但它們的構圖方式都相同。這樣,我不用微觀的操作每個條目,就可以把注意力放在資料的變更上。

我們建立CategoryActivity將要使用的佈局檔案activity_category.xml:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/categoryList"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

這裡只有一個ListView便籤,作為高層框架,用於容納多個條目。至於每個條目的具體內容和顯示格式,將在下面的CategoryActivity中說明。

 

使用ArrayAdapter

現在,有了檢視,我們要考慮資料。當我們取出多個資料後,最自然的方式是記錄為一個表或陣列。我們需要根據小條目的佈局,為資料賦予顯示格式。最後,再把影象化的多個條目合成到ListView上。安卓提供了ArrayAdapter類,可以綜合以上功能。它可以為每個資料元素賦予相同的檢視格式。將ListView與ArrayAdapter繫結後,安卓就可以動態的調整條目了。

為資料賦予檢視格式

 

我在CategoryActivity.java中使用ArrayAdapter:

package me.vamei.vamei;

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

import me.vamei.vamei.model.Category;
import me.vamei.vamei.model.ContactsManager;
import android.os.Bundle;
import android.app.Activity;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class CategoryActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_category);
        
        ListView listview = (ListView) findViewById(R.id.categoryList);
        
        // retrieve data from the database
        ContactsManager cm        = new ContactsManager(this);
        List<Category> categories =  cm.getAllCategories();

        // transform data to a list of strings
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < categories.size(); ++i) {
            list.add(categories.get(i).getName());
        }
        
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_list_item_1, list);
        
        // bind the listview and the adapter
        listview.setAdapter(adapter);
    }
}

程式碼中新建了一個ArrayAdapter物件。ArrayAdapter構造器接收三個引數,第一個為Context,第二個說明了條目的具體構圖,第三個為包含有資料的表。由於資料是字串型別的表,ArrayAdapter也有一個String的型別引數。一個ArrayAdapter中包含了資料和條目的具體格式。

需要注意的是第二個引數android.R.layout.simple_list_item_1,它是安卓框架自己提供的一個簡單的XML佈局,包含了一個TextView元素。未來的字串型資料按照該檢視元素規定的格式顯示。這個佈局的原始碼可參考連結。安卓還提供了其它一些簡易的佈局,參考連結。我們當然可以用自己的佈局來替代它。

最後,通過ListView的setAdapter()方法,把ArrayAdapter所形成的多個條目檢視(包含檢視格式和資料),放置在ListView這個大容器中:

 

繼承ArrayAdapter

我上面從Category型別的表中,提取出一個字串型別的表,作為資料傳遞給ArrayAdapter。ArrayAdapter隨後自動的把字串資料加工為simple_list_item_1格式。我也可以通過繼承ArrayAdapter,來建立一個新的Adapter型別。在該過程中,我可以更自由的控制對資料和ListView的繫結。下面的CategoryAdapter繼承了ArrayAdapter。它將允許我:

  • 使用Category表中的資料。資料不用提前轉換為字串型別的表。
  • 使用更復雜的檢視格式。控制Category物件中的多個屬性的顯示方式。

 

我在me.vamei.vamei中新增CategoryActivity.java。它包含了類CategoryAdapter:

package me.vamei.vamei;

import java.util.List;

import me.vamei.vamei.model.Category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class CategoryAdapter extends ArrayAdapter<Category> {
    private int viewId;
    private Context context;
    private List<Category> objects;
    
    // Constructor
    public CategoryAdapter(Context context, int viewId, List<Category> objects) {
        super(context, viewId, objects);
        this.context = context;
        this.viewId  = viewId;
        this.objects = objects;
    }
    
    // For each row, control the view assigned to the data
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        
        // Inflate the row view, if it doesn't exist.
        // ViewList is capable of reusing the views.
        if(v == null) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = inflater.inflate(viewId, parent, false);
        }
        
        Category category = objects.get(position);
        
        // Add data to views
        if(category != null) {
            TextView tv1 = (TextView) v.findViewById(R.id.seq);
            if(tv1 != null) { 
                tv1.setText(Integer.toString(category.getId())); 
            }
        
            TextView tv2 = (TextView) v.findViewById(R.id.name);
            if(tv2 != null) { 
                tv2.setText(category.getName()); 
            }
        }
        
        return v;
    }
}

這是一個ArrayAdapter的子類。我通過編寫getView()方法,來說明每個Category物件和對應條目檢視的繫結方式。該方法的第一個引數代表了條目的編號,第二個引數是條目的檢視,第三個引數代表了母檢視,也就是整個ListView。需要注意的是第二個引數,即convertView。隨著使用者上下滑動螢幕,ListView的條目可能消失。安卓會重複利用消失條目的檢視樹,以節省重新建立條目檢視所需要的時間。convertView中就包含了這樣一個重複利用的條目檢視。如果沒有可以重複利用的條目檢視,那麼該引數就為null。此時,我們需要如if結構中那樣,重建新的條目檢視。

 

我將要賦予給條目的檢視佈局儲存在list_category.xml中。它在位於一行中包含了兩個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="wrap_content"
    android:orientation="horizontal" >
    <TextView
        android:id="@+id/seq"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

 

我們在CategoryActivity.java,來利用新建的CategoryAdapter類。在建立物件時,我把上面的條目佈局,即R.layout.list_category作為引數傳給構造器:

package me.vamei.vamei;

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

import me.vamei.vamei.model.Category;
import me.vamei.vamei.model.ContactsManager;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class CategoryActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_category);
        
        ListView listview = (ListView) findViewById(R.id.categoryList);
        
        ContactsManager cm              = new ContactsManager(this);
        final List<Category> categories = cm.getAllCategories();
        
        CategoryAdapter adapter = new CategoryAdapter(this,
            R.layout.list_category, categories);
        
        listview.setAdapter(adapter);
        
        listview.setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(AdapterView<?> parent, View view,int position, long id) {
                    // When clicked, show a Toast text
                    Toast.makeText(getApplicationContext(),
                    "id:" + categories.get(position).getId(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

通過新的CategoryAdapter類物件,並借用setAdapter()方法,我就把Category表中的資料和條目檢視組織到了ListView中。此後,我還通過setOnItemClickListener()方法,監聽每個條目的點選事件。

 

使用setTag()優化CategoryAdapter

上面已經提到,ArrayAdapter可以通過重複利用條目檢視,來優化安卓應用的效率。在ArrayAdapter中,我還可以用setTag()的方式,儲存條目中具體檢視元素的引用,從而減少使用findViewId()方法的次數。這也能提高應用的執行效率。

setTag()用於把物件“粘附”在某個檢視元素上。由於ListView中消失的條目會通過convertView引數來重複利用,我們可以為convertView附加兩個TextView元素(R.id.seq, R.id.name)的引用。當convertView被重複利用時,粘附於其上的兩個檢視元素的引用也會被重複利用,從而減少了呼叫findViewById()進行檢索的次數。

 

 

為了實踐上面的想法,我修改CategoryAdapter.java如下:

package me.vamei.vamei;

import java.util.List;

import me.vamei.vamei.model.Category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class CategoryAdapter extends ArrayAdapter<Category> {
    private int viewId;
    private Context context;
    private List<Category> objects;
    
    // Constructor
    public CategoryAdapter(Context context, int viewId, List<Category> objects) {
        super(context, viewId, objects);
        this.context = context;
        this.viewId  = viewId;
        this.objects = objects;
    }
    
    private class Holder {
        TextView tv1;
        TextView tv2;
        public Holder(TextView tv1, TextView tv2) {
            this.tv1 = tv1;
            this.tv2 = tv2;
        } 
    }
    // For each row, control the view assigned to the data
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        Holder holder;
        
        // inflate the row view, if it doesn't exist.
        // ViewList is capable of reusing the views.
        if(v == null) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = inflater.inflate(viewId, parent, false);
            holder = new Holder((TextView) v.findViewById(R.id.seq), 
                    (TextView) v.findViewById(R.id.name));
            v.setTag(holder);
        } else {
            holder = (Holder) v.getTag();
        }
        
        Category category = objects.get(position);
        
        
        // add data to views
        if(category != null) {
            holder.tv1.setText(Integer.toString(category.getId())); 
            holder.tv2.setText(category.getName()); 
        }
        
        return v;
    }
}

上面程式碼中的Holder型別的物件用於儲存兩個TextView型別的引用。在if(convertView == null)的結構中可以看出,如果條目被重複利用,粘附在條目上的Holder物件將藉助getTag()方法取出。我們可以重複利用該Holder物件中包含的兩個TextView引用,從而減少了findViewById()的呼叫次數。

 

總結

ArrayAdapter, getView()

setAdapter()

setOnItemClickListener()

setTag(), getTag()

 

歡迎繼續閱讀“Java快速教程”系列文章  

相關文章