戌印-INUJIRUSHI- (Androidあれこれ)

Androidのプログラミングをメインにしてます。記事に貼られたソースコードはダブルクリックすることで行番号をはずしてコピーすることができます。

 
1
2
3
4
5
6
7
8
10
11
12
13
14
15
16
17
19
20
21
23
24
25
26
27
28
29
30
31
01

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

状態リスト:selector

Androidアプリを触っていると ListView や Button など未選択・選択中で色や画像が変わることがあります。
これは selector を使うことで設定することができます。

参考:
ソフトウェア技術ドキュメントを勝手に翻訳 - 7.5.2 カラー状態リストリソース
ソフトウェア技術ドキュメントを勝手に翻訳 - 7.5.3 Drawable リソース

■ selector 定義 -----------------------------------------
selector はXMLで定義します。
設定は上が優先されるので、下にいくほど設定を少なくすることができます。

【背景色の場合】
<item> タグに <color> タグを追加して記述する。
res/color/selector_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false"
android:state_selected="false">
<color android:color="#f000" />
</item>
<item android:state_pressed="false"
android:state_selected="true">
<color android:color="#ff00" />
</item>
<item android:state_pressed="true">
<color android:color="#f0f0" />
</item>
</selector>


【文字色の場合】
色は <item> タグに記述する。
res/color/selector_text.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#ff0f"
android:state_pressed="false"
android:state_selected="false"/>
<item android:color="#fff0"
android:state_pressed="false"
android:state_selected="true"/>
<item android:color="#f0ff"
android:state_pressed="true"/>
</selector>


【描画(drawable)を指定する場合】
res/drawable/selector_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/color_black"
android:state_pressed="false"
android:state_selected="false"/>
<item android:drawable="@android:drawable/list_selector_background"
android:state_pressed="false"
android:state_selected="true"/>
<item android:drawable="@android:drawable/list_selector_background"
android:state_pressed="true"/>
</selector>



■ selectorの使い方 -----------------------------------------
【XMLで静的に指定】

<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_background"
android:textColor="@drawable/selector_text"
android:text="TextView"/>


Javaで文字色を指定するときは Resources#getColorStateList(int id) を使います。
Resources#getColor(int id) で指定すると初期状態以外の色変更が行われません。
【Javaで動的に指定】

// TextViewを生成する
TextView text = new TextView(this);

// 背景色を設定する
text.setBackgroundResource(R.color.selector_background);
//text.setBackgroundResource(R.drawable.selector_drawable);

// 文字色を設定する
text.setTextColor(getContext().getResources().getColorStateList(R.color.selector_text));

スポンサーサイト

ListView をカスタマイズする

情報を一覧表示する ListView ですが、アプリを作るとなると既存のレイアウトでは少し物足りなくなってしまいます。
そこで今回は ListView のカスタマイズの方法を紹介します。

カスタマイズの説明については、以下の手順で説明します。

①行のレイアウトを定義する
②レイアウトを表示するAdapterクラスを作成する
③アクティビティにカスタマイズしたListViewを配置する

■ 完成イメージ -----------------------------------------------
さて、いきなり手順から離れてしまいますが(手順には書きませんでしたが)
カスタマイズする前にどのようなレイアウトにするか考えなければいけません。

今回は簡単ですが、左端に画像、そのすぐ右に文字列を表示するレイアウトを作成します。
完成イメージはこんな感じです。
android_ListView_custom1.png

■ 行レイアウトを定義する ------------------------------------
カスタマイズ作業にあたって、まず始めに ListView に表示する行のレイアウトを定義します。
完成イメージで書いたように画像を表示するレイアウトを作成します。

sample_row.xml
<?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" >
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textSize="24sp" />
</LinearLayout>

※ 小さな画像を表示することを前提としているため、画像サイズは指定していません

■ ArrayAdapterを定義する ------------------------------------
次に ListView を表示するための Adapter クラスを定義します。
ArrayAdapter や SimpleAdapter など色々な Adapter がありますが、ここでは ArrayAdapter クラスを使用します。

Adapterクラスを作成するにあたって、以下の点に注意してください。
①行の値は毎回設定する必要がある
Adapterクラスには、行を表示するための View を返すメソッドとして getView() があります。
しかし、ここで表示される View(第2引数の convertView)は画面外に行ったViewを再利用しているため正しい順番で表示されるとは限りません(再描画やスクロールをすると前回と違う順番で表示されます)。
そこで、行位置(第1引数の position)を元に getItem(position) で行の正しい情報を取得して View の要素を書き換える必要があります。

参考サイト:hyoromoの日記 - Adapter#getViewの挙動について

②ViewHolderを使用して高速化する
ViewHolderとは ListView を高速化するために View 要素を使いまわす手法を言います(たぶん)
毎行、レイアウト定義したXMLをインフレートするとパフォーマンスが落ちてしまうので、XMLで定義されているViewを保持する独自クラスを作成、使用して処理を簡略化します。

と、このような問題を考慮してAdapterクラスを作成します。
ここで作られるクラスは1つのファイルですが、クラスは3つ定義しています(分けてもOK)

CustomAdapter :メインのAdapterクラス
ViewHolder :②で説明したやつ
SampleData :Adapterに表示する情報をまとめたクラス

CustomAdapter.java
package jp.inujirushi.sample.activity;

import java.util.List;

import jp.inujirushi.sample.R;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class CustomAdapter extends ArrayAdapter {

LayoutInflater mInflater;

/**
* コンストラクタ
*
* @param context
* コンテキスト
* @param objects
* 行データ
*/
public CustomAdapter(Context context, List objects) {
// 親のコンストラクタを呼び出す
// ※ 2番目の引数はレイアウトのリソースIDだがgetViewで指定するのでここでは0を設定
super(context, 0, objects);

// インフレーターを取得する
this.mInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// ホルダークラスを定義する
// ※ ホルダークラスを使うことで再表示時にレイアウト内のビュー検索が無くなり高速化されます
ViewHolder holder;

// ビューを設定する
if (convertView == null) {
// はじめて呼ばれた時はビューはnullが設定されている
// ビューに定義したレイアウトをインフレートする
convertView = this.mInflater.inflate(R.layout.sample_row, parent,
false);

// ホルダークラスを生成する
holder = new ViewHolder();

// ホルダークラスにレイアウト内のビューを設定する
holder.textView = (TextView) convertView
.findViewById(R.id.textView);
holder.imageView = (ImageView) convertView
.findViewById(R.id.imageView);

// タグにホルダークラスを設定する
convertView.setTag(holder);
} else {
// 2回目以降はビューが設定されている
// タグからホルダークラスを取得する
holder = (ViewHolder) convertView.getTag();
}

// 指定された位置のアイテムを取得する
SampleData data = getItem(position);

// ホルダークラスのビューの値を設定する
// ※ convertViewに設定されている内容とgetItem(position)の内容が同じではないので再設定が必要
holder.imageView.setImageResource(data.resourceId);
holder.textView.setText(data.text);

// 表示するビューを返す
return convertView;
}

/**
* ホルダクラス
*/
class ViewHolder {
/** イメージビュー */
ImageView imageView;

/** テキストビュー */
TextView textView;
}
}

/**
* アダプタに設定するデータ
*/
class SampleData {
/** 画像のリソースID */
int resourceId;

/** 文字列 */
String text;

/**
* コンストラクタ
*
* @param resourceId
* 画像のリソースID
* @param text
* 文字列
*/
public SampleData(int resourceId, String text) {
this.resourceId = resourceId;
this.text = text;
}
}


■ アクティビティにListViewを配置する --------------------------
カスタマイズした Adapter クラスを使用した ListView を表示するためのレイアウトXMLを定義します。
といっても、カスタマイズしたのは表示処理の任されている Adapter クラスのため、受け皿は普通の ListView を使います。

アクティビティクラスは特に説明なし。
画像は特に考えずにプロジェクト作成時に作られるアイコンをひたすら表示させているだけです。

sample_listview.xml
<?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" >

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

</LinearLayout>


CustomListActivity.java
package jp.inujirushi.sample.activity;

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

import jp.inujirushi.sample.R;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

public class CustomListActivity extends Activity {

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

// リストビューに表示する項目を生成する
List list = new ArrayList();
for (int i = 0; i < 10; i++) {
list.add(new SampleData(R.drawable.icon, "項目" + i));
}

// リストビューを取得する
ListView listView = (ListView) findViewById(R.id.listView);

// リストビューにカスタムしたリストアダプタを設定する
listView.setAdapter(new CustomAdapter(this, list));
}
}


とまあ、こんな感じ。

画面回転時の再構築対応

Androidでは画面を回転させると画面の向きに合わせたレイアウトを読み込もうとしてアクティビティを再構築(onDestory→onCreate)します
アクティビティが再構築されるとその画面で入力された値などがクリアされてしまうため、対策を行う必要があります。

対策としては以下の4つの方法があります(俺が知る限りでは…)
・アクティビティの設定変化をハンドリング
・画面の向きを固定化
・Bundleに設定(一時保存)
・SQLiteやPreferenceなどへの保存(永続保存)


最後のSQLiteやPreferenceなどへの保存は画面の回転に限った話ではないので今回は除外します。
以下、その他3つの設定方法です。

参考サイト:ソフトウェア技術ドキュメントを勝手に翻訳 - 7.3 実行時の変化への対処

【アクティビティの設定変化をハンドリング】
アクティビティの設定の変更ハンドリングを宣言することで、画面の再構築を行わないようにします。
ハンドリングは、AndroidManifestのタグに android:configChanges を指定するとできます。
ハンドリングされた設定が変更されたとき Activity#onConfigurationChanged(Configuration newConfig) が呼ばれます。
変更時に何か処理が必要な場合は、このメソッドをオーバーライドして処理を記述してください。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.inujirushi.sample"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<application
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name"
android:name="SampleActivity" >
<intent-filter >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

android:screenOrientation
"orientation" ユーザーがデバイスを回転させた
"keyboardHidden" キーボードへのアクセスが変更された

画面が回転されたときなので orientation を設定するだけでいいのですが、エミュレータなどでは keyboardHidden の設定も行わなければ再構築が発生してしまうので注意してください。

【画面の向きを固定化】
画面を縦、または、横に固定することでアクティビティの再構築を行わないようにします。
画面の固定は、AndroidManifestのタグに android:screenOrientation を指定するとできます。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.inujirushi.sample"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<application
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity
android:label="@string/app_name"
android:name="SampleActivity"
android:screenOrientation="portrait" >
<intent-filter >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

android:screenOrientation
"landscape" 横向き
"portrait" 縦向き

※ 一部端末(エミュレータ含む)では画面を回転させたときに keyboardHidden が呼ばれてしまうため再構築が行われてしまいます。
前述の【アクティビティの設定変化をハンドリング】にて android:configChanges="keyboardHidden" の設定を追加する必要があります。

【Bundleに設定】
Androidのライフサイクルではアクティビティが破棄されるとき、そのときの状態を保存するために onSaveInstanceState(Bundle outState) が呼び出されます。
このメソッドの引数の Bundle に値を設定することで破棄されるアクティビティの状態を一時的に保持することができます。
Bundleに設定された状態は、再構築後に呼ばれる onRestoreInstanceState(Bundle savedInstanceState) または onCreate(Bundle savedInstanceState) の引数のBundleから取得することができます。

package jp.inujirushi.sample;

import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.widget.EditText;
import android.widget.LinearLayout;

public class SampleActivity extends Activity {

/** EditTextのID */
private static final int ID_EDIT = 1;
/** EditTextのキー */
private static final String KEY_EDIT = "text";

/** アクティビティを生成した時に呼ばれます。 */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// サンプル用のレイアウトを生成する
LinearLayout layout = new LinearLayout(this);
EditText editText = new EditText(this);
editText.setId(ID_EDIT);
layout.addView(editText);
// レイアウトを画面に設定する
setContentView(layout);

//// 再構築時に前の状態に戻す
//if (savedInstanceState != null) {
// // Bundleに保存した値を取得する
// String value = savedInstanceState.getString(KEY_EDIT);
// // EditTextに値を設定する
// editText.setText(value);
//}
}

/** アクティビティがデータを一時保存する時に呼ばれます。 */
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);

// EditTextの値を取得する
EditText editText = (EditText) findViewById(ID_EDIT);
Editable value = editText.getText();

// Bundleに値を保存する
outState.putString(KEY_EDIT, value.toString());
}

/** アクティビティが一時保存されたデータを読み込む時に呼ばれます。 */
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);

// Bundleに保存した値を取得する
String value = savedInstanceState.getString(KEY_EDIT);

// EditTextに値を設定する
EditText editText = (EditText) findViewById(ID_EDIT);
editText.setText(value);
}
}

なお、再構築時に呼ばれるライフサイクルの順番は以下のようになります(エミュレータで確認)
onSaveInstanceState() → onPause() → onStop() → onDestroy() → onCreate() → onStart() → onRestoreInstanceState() → onResume()

アクティビティのライフサイクルを考えるとコレ(Bundle)が正しい手法だと思われますが、ネットでは最初に紹介した【アクティビティの設定変化をハンドリング】を利用している方が多く見られます。

縦横でレイアウトを分けるのならともかく、分けないのであれば設定一つで終わるハンドリングが楽でいいですね。
Bundleは面倒なので俺もこちらを使っています。

あけましておめでとうございます

少し遅れましたが、新年明けましておめでとうございます。

当ブログは今後もAndroidプログラミングに関した記事を書いていきます。
今年こそは何かしたアプリの紹介やらを書けたらいいなーと思っています。

Tips的な記事もチラホラ書いているのですが、過去記事修正とかブログではやりづらい面もあるのでやっぱりホームページ持った方がいいのだろうかと考え中です。
まあ、ここで書くものはネットで検索するか本買えば終わるようなものばかりなのですが…。

何はともあれ、ブログのPingのおかげで一見さん 来訪者もそこそこおられるようなので少しでも役立てる記事を書けたらと思います。
今後ともよろしくお願いいたします。
プロフィール

とむ・やむくん

Author:とむ・やむくん
管理人について

Windows 7 / 64bit
Eclipse 4.2 Juno (日本語パッチ済)

スポンサーサイト
最新トラックバック
検索フォーム
ブロとも申請フォーム
QRコード
QR
Twitter
2013/01/04 19:00 カウント開始

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。