ListAdapterについて調べてみた(2) SimpleAdapter編その1
前回ArrayAdapterを使ってみました。
ArrayAdapterは,一つのTextViewだけセルごとに値を設定する場合,getViewメソッドをオーバーライドしたりせず非常に少ないコード量で書けます。
しかし,TextViewではなくImageViewの要素をセルごとに設定したり,セルごとに複数のTextViewに値を設定したり,画像と文字列両方セルごとに設定したりということは,getViewメソッドのオーバーライドメソッドなしには実現できません。
このようなことはSimpleAdapterを使えば,比較的容易にそしてgetViewメソッドのオーバーライドなしに少ないコード量で書けます。
具体例と一緒に使い方を見ていきます。
やりたいことと設定
こんなクラスがあるとします。
public class ColorItem { private final String nameEnglish; private final String nameJapanese; private final int imageId; public ColorItem(String nameEnglish, String nameJapanese, int imageId) { this.nameEnglish = nameEnglish; this.nameJapanese = nameJapanese; this.imageId = imageId; } public String getNameEnglish() { return nameEnglish; } public String getNameJapanese() { return nameJapanese; } public int getImageId() { return imageId; } }
こんなレイアウトXMLファイルがあるとします。
res/layout/list_item_for_simple_adapter.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="70dp" > <TextView android:id="@+id/name_english_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginLeft="20dp" android:layout_marginTop="15dp" android:text="英語名" > </TextView> <TextView android:id="@+id/name_japanese_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_marginBottom="15dp" android:layout_marginLeft="20dp" android:text="日本語名" > </TextView> <ImageView android:id="@+id/color_image_view" android:layout_width="50dp" android:layout_height="50dp" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_margin="10dp" /> </RelativeLayout>
ColorItemのListのインスタンスがあり,そのインスタンスの情報をListViewに表示したいです。
list_item_for_simple_adapterをセルのレイアウトとし,ColorItemクラスのインスタンスの情報をセットするためのSimpleAdapterインスタンスを作成します。
ColorItemのnameEnglishの文字列の値を上記レイアウトXMLファイル中のidがname_english_text_viewのTextView中にセットし,nameJapaneseの文字列をidがname_japanese_text_viewのTextView中にセットし,imageIdの値をidとする画像リソースをcolor_image_viewというidのImageViewにセットします。
サンプルコード
public class SimpleAdapterSampleActivity extends ListActivity { private static final String KEY_NAME_ENGLISH = "name_english"; private static final String KEY_NAME_JAPANESE = "name_japanese"; private static final String KEY_IMAGE_ID = "image_id"; private static final String[] FROM = new String[] { KEY_NAME_ENGLISH, KEY_NAME_JAPANESE, KEY_IMAGE_ID }; private static final int[] TO = new int[] { R.id.name_english_text_view, R.id.name_japanese_text_view, R.id.color_image_view }; private static final List<ColorItem> COLOR_ITEM; static { COLOR_ITEM = new ArrayList<ColorItem>(); COLOR_ITEM.add(new ColorItem("Blue", "青", R.drawable.blue)); COLOR_ITEM.add(new ColorItem("Cyan", "シアン", R.drawable.cyan)); COLOR_ITEM.add(new ColorItem("Green", "緑", R.drawable.green)); COLOR_ITEM.add(new ColorItem("Magenta", "マゼンタ", R.drawable.magenta)); COLOR_ITEM.add(new ColorItem("Red", "赤", R.drawable.red)); COLOR_ITEM.add(new ColorItem("Yellow", "黄色", R.drawable.yellow)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); List<ColorItem> colorItemList = new ArrayList<ColorItem>(COLOR_ITEM); List<Map<String, ?>> data = colorItemListToMapList(colorItemList); int layoutResourceId = R.layout.list_item_for_simple_adapter; SimpleAdapter adapter = new SimpleAdapter(this, data, layoutResourceId, FROM, TO); setListAdapter(adapter); } private List<Map<String, ?>> colorItemListToMapList(List<ColorItem> colorItemList) { List<Map<String, ?>> data = new ArrayList<Map<String, ?>>(); for (ColorItem colorItem : colorItemList) { data.add(colorItemToMap(colorItem)); } return data; } private Map<String, ?> colorItemToMap(ColorItem colorItem) { Map<String, Object> map = new HashMap<String, Object>(); map.put(KEY_NAME_ENGLISH, colorItem.getNameEnglish()); map.put(KEY_NAME_JAPANESE, colorItem.getNameJapanese()); map.put(KEY_IMAGE_ID, Integer.valueOf(colorItem.getImageId())); return map; } }
次の節から詳しく中身を見ていきます
SimpleAdapterのコンストラクタ
SimpleAdapterコンストラクタは一つで
public SimpleAdapter (Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
のみとなっています。
それぞれの引数の意味を簡単に説明すると,
第2引数は,キーがString型のMapのList(詳しくは後述)
第3引数は,セルのレイアウトXMLファイルのid
第4引数は,第2引数のMapのキーの配列
第5引数は,第3引数のレイアウトXMLファイル内のビューのidの配列
です。
まずColorItemの各フィールドの値とビューのidを紐付ける仲介を行うマップのキーを定義します。
static final String KEY_NAME_ENGLISH = "name_english"; // 英語名のデータとビューのためのキー static final String KEY_NAME_JAPANESE = "name_japanese"; // 日本語名のデータとビューのためのキー static final String KEY_IMAGE_ID = "image_id"; // 画像のデータとビューのためのキー
このキーを使って,SimpleAdapterの第4引数を作ります。
static final String[] FROM = new String[] { KEY_NAME_ENGLISH, KEY_NAME_JAPANESE, KEY_IMAGE_ID };
次はint[]型の第5引数です。
第4引数の配列は,英語名,日本語名,画像idの順番でキーが並んでいます。
その順番で,それぞれのキーと対応するビューのidを要素とするint型の配列を作ります。それそれのビューのidは,第3引数のレイアウト中のビューのidでないといけません。
static final int[] TO = new int[] { R.id.name_english_text_view, R.id.name_japanese_text_view, R.id.color_image_view };
最後に第2引数です。これはキーがString型のMapのListになっています。
SimpleAdapterを使うためには表示したいデータをこの型に変換する必要があります。
コンストラクタの第2引数 キーがString型のMapのリスト
コンストラクタの第2引数はキーがString型のMapのListです。
まずColorItemのインスタンスを,キーがString型のMapに変換するメソッドを作ります。
private Map<String, ?> colorItemToMap(ColorItem colorItem) { Map<String, Object> map = new HashMap<String, Object>(); map.put(KEY_NAME_ENGLISH, colorItem.getNameEnglish()); map.put(KEY_NAME_JAPANESE, colorItem.getNameJapanese()); map.put(KEY_IMAGE_ID, Integer.valueOf(colorItem.getImageId())); return map; }
キーは第4引数にも用いた文字列で,バリューはそのキーと対応するColorItem型のフィールドです。
このメソッドを使って,ColorItemのListを変換するメソッドを定義します。
private List<Map<String, ?>> colorItemListToMapList(List<ColorItem> colorItemList) { List<Map<String, ?>> data = new ArrayList<Map<String, ?>>(); for (ColorItem colorItem : colorItemList) { data.add(colorItemToMap(colorItem)); } return data; }
SimpleAdapterのインスタンスの作成
ActivityのonCreate内で,
List<Map<String, ?>> data = colorItemListToMapList(colorItemList); int layoutResourceId = R.layout.list_item_for_simple_adapter; SimpleAdapter adapter = new SimpleAdapter(this, data, layoutResourceId, FROM, TO);
とすれば,前述のやりたいことを実現できるSimpleAdapterが作成できます。
まとめ
SimpleAdapterを使って,セルごとに複数のTextViewに文字列とImageViewに画像を設定することができました。
ListAdapterについて調べてみた(1) ArrayAdapter編
ArrayAdapterを継承して,getViewをオーバーライドしたり,Adapter内でViewHolderをインナークラスで定義したり,セルのビューのタグにホルダーをセットしたりなどは今回しません。ArrayAdapterを継承せずに,そして非常に少ないコード記述量で使ってみます。
ArrayAdapterクラスのコンストラクタは,TextView単体のレイアウトXMLファイルのidを引数にとったり,レイアウトXMLファイルのidとその中のTextViewのidを引数に取ったりするものがあります。リスト内のセルの構成要素がTextViewだけ,もしくは単一のTextViewの表示内容だけがセルごとに異なる場合,非常に少ないコード記述量でリストを表示できるようです。(もちろんArrayAdapterは,getViewメソッドをオーバーライドすることで,画像や複数のTextViewが一つのセル内にあるようなリストを作ることも可能です。)
シンプルな使い方
(多分一度は見たことがある)ArrayAdapterの基本的な使い方。
下記のようなTextViewのみで構成されているレイアウトXMLファイルを用意します。
res/layout/list_item_text_view.xml
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/label" android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:text="Label" > </TextView>
このレイアウトXMLファイルのidと,文字列の配列からArrayAdapterのインスタンスを作ります。(ActivityのonCreate内。thisはActivityのインスタンス)
String[] strings = new String[] { "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" }; int itemLayoutId = R.layout.list_item_text_view; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, itemLayoutId, strings);
このAdapterを用いてテキストのみの簡単なリストを表示することが可能です。
また,配列でなくリストを用いるコンストラクタも用意されています。
ちなみに,試しにやってみたいけれどTextViewのみで構成されたレイアウトXMLファイルを作るのが面倒な場合,android.R.layout.textを使うといいと思います。
オブジェクトの文字情報をリストに表示
次のようなクラスを定義し,このクラスの文字情報をリスト内のセルに表示しします。
public class Person { private final String name; private final int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return String.format("名前:%s 年齢:%02d歳", name, age); } }
アダプターの作成は以下の通りです。
(ActivityのonCreate内。thisはActivityのインスタンス)
Person[] persons = new Person[] { new Person("太郎", 25), new Person("次郎", 22), new Person("三郎", 19), new Person("四郎", 16), new Person("五郎", 13), }; int itemLayoutId = R.layout.list_item_text_view; ArrayAdapter<Person> adapter = new ArrayAdapter<Person>(this, itemLayoutId, persons);
これをListViewにセットすると先頭のセルには下記のように表示されます。
名前:太郎 年齢:25歳
二番目以降のセルも,それぞれのPerson型のインスタンスのtoStringの結果がセルに表示されています。
実は「オブジェクトの文字情報をリストに表示」のコンストラクタと,最初に紹介した「シンプルな使い方」のコンストラクタは同一のものです。
使っているコンストラクタの定義は,
ArrayAdapter(Context context, int resource, T[] objects)
となっています。
前者はString型を,後者はPerson型の配列をコンストラクタの引数として渡しています。
String[]型の場合も他の型の配列の場合も,セルの表示の際に表示するデータ要素のオジェクトのtoStringメソッドを呼び出しています。
そのため,任意の型の文字情報を表示したい場合,ToStringメソッドを実装すればその情報をセルに表示することが可能です。
リソースで定義した文字列配列を表示する
リソースファイル内に定義した下記のような文字列配列
res/values/arrays.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="day_array"> <item>日曜日</item> <item>月曜日</item> <item>火曜日</item> <item>水曜日</item> <item>木曜日</item> <item>金曜日</item> <item>土曜日</item> </string-array> </resources>
これをリストで表示するためのAdapterを作成します。
(ActivityのonCreate内。thisはActivityのインスタンス)
int itemLayoutId = R.layout.list_item_text_view; int textArrayResId = R.array.day_array; ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, textArrayResId, itemLayoutId);
ArrayAdapterのクラスメソッドを用いてインスタンスを生成出来ました。
上記は3行ですが,実質1行で書けますね。
カスタマイズしたセル中にテキスト情報を表示する
下記のようなレイアウトXMLファイルを定義します。
res/layout/list_item_text_in_layout.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="40dp" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toLeftOf="@+id/label" android:text="曜日:" /> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Label" > </TextView> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:src="@drawable/ic_launcher" /> </RelativeLayout>
このレイアウトXMLファイルと,
ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects)
というコンストラクタを使います。(ActivityのonCreate内。thisはActivityのインスタンス)
String[] strings = new String[] { "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" }; int itemLayoutId = R.layout.list_item_text_in_layout; int textViewId = R.id.label; ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, itemLayoutId, textViewId, strings);
ここで使っているコンストラクタは,第2引数にセルのレイアウトXMLファイルのidを,第3要素にセル内のTextViewのidを取ります。コンストラクタの第3引数で渡した配列の各要素の文字列は,第2要素で渡したidが示すTextView(ここではlabelというidのTextView)に表示されます。
これを使えば,セルに背景を設定したり,画像を複数個表示したり,他のTextViewを持つセルの表示も容易です。
まとめ
ArrayAdapter(もしくはBaseAdapter)のgetViewメソッドをオーバーライドしたり,内部にViewHolderを定義したり,セルのビューのタグにViewHolderをセットしたりして,リストを表示するのは,(定型パターンではありますが)やはり多少面倒です。
試しにささっとデータをリストで表示してみたい場合や,文字列のみが変わるリストを表示したい場合,ArrayAdapterを適切に使い少ないコード量でリストを作成すりことも可能です。
ただし,TextViewではなくImageViewの要素をセルごとに設定したい,セルごとに複数のTextViewの値を設定したい,画像と文字列両方セルごとに設定したい場合は,
getViewメソッドをオーバーライドするか,SimpleAdapterを使う必要があります。
参考:
http://developer.android.com/reference/android/widget/ArrayAdapter.html
Unityのシーンファイルの中身を見てみた (2)
Unityのシーンファイルの中身を見てみた(1)の続きです。
Unity 4.2から,無料版でもテキストベースでシーン・マテリアルなどを保存出来るようになり,バージョン管理システムなどで差分を確認しやすくなりました。Unityのシーンを編集した際に,シーンはテキストベースでその表現がどのように変化するかを確認してみました。
シーン中に空のゲームオブジェクトを置いた時,.unityファイルには,以下のようなテキストが追加されました。
--- !u!1 &1696434231 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 1696434232} m_Layer: 0 m_Name: EmptyGameObject m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!4 &1696434232 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 1696434231} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0}+--- !u!1 &1696434231 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 1696434232} m_Layer: 0 m_Name: MyGameObject m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!4 &1696434232 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 1696434231} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0}
さて,前回のおさらいになりますが,
--- !u!1 &1696434231
これは,!u!1でこのYAMLドキュメントがGameObject型の情報を表し,&の後でこのidが1696434231であることを表しているようです。
--- !u!4 &1696434232
同様に,!u!4でこのYAMLドキュメントがTransform型の情報を表し,&の後でこのidが1696434232であることを表しているようです。
(型と!u!の後のクラスidの対応はこちら。)
ではこのGameObject(以後,MyGameObject)のデフォルト状態のTransformを
- Positionはx = 2, y = 3, z = 4
- Rotationはx = 30, y = 10, z = 20
- Scaleはx = 0.5, y = 0.1, z = 1.5
のように変更しました。
gitで差分を確認すると以下のようになりました。
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} + m_LocalRotation: {x: .268535882, y: .0381345674, z: .144878149, w: .951548576} + m_LocalPosition: {x: 2, y: 3, z: 4} + m_LocalScale: {x: .5, y: .100000001, z: 1.5}
Position,Rotation,Scaleそれぞれの要素がYAMLのフロー形式で表現されているようです。
RotationはEditor上では,x,y,zの3つの入力項目あります。TransformクラスのlocalRotationはQuertanion型で定義されています。YAMLではm_LocatlRotationがクォータニオン型として定義されているためからか,x,y,z,wの4つの項目で表現されています。
設定されているコンポーネントの値を変えるくらいだったら,差分は非常に確認しやすいですね。
次にMyGameObjectにRigidbodyコンポーネントを付与してみましす。
以下のようなRigidbodyコンポーネントの情報・設定を表現する部分が.unityファイルに追加されました。54はRigidbodyクラスのクラスidです。
+--- !u!54 &1696434233 +Rigidbody: + m_ObjectHideFlags: 0 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 1696434231} + serializedVersion: 2 + m_Mass: 1 + m_Drag: 0 + m_AngularDrag: .0500000007 + m_UseGravity: 1 + m_IsKinematic: 0 + m_Interpolate: 0 + m_Constraints: 0 + m_CollisionDetection: 0
これに加えて,MyGameObjectのYAMLドキュメント中に以下のような追加がありました。
(略)
serializedVersion: 4
m_Component:
- 4: {fileID: 1696434232}
+ - 54: {fileID: 1696434233}
m_Layer: 0
m_Name: EmptyGameObject
(略)
m_Componentは,配列になっているようです。
その配列の要素はマップになっていて,キーがコンポーネントのクラスidで,バリューがさらにマップになっているようです。
そのマップは,fileIDという文字列をキーに持ち,YAMLドキュメントのidをバリューに持つようです。
MyGameObjectはTransform型(id : 4)とRigidbody型( id : 54)が付与されていて,それぞれのコンポーネントの値・情報はidが1696434232,1696434233のYAMLドキュメントにあるということが表現されているようです。
実際に,さきほどidが1696434233のRigidbodyの情報を表現するYAMLドキュメントが追加されています。
次に,空のゲームオブジェクトをMyGameObjectに子オブジェクトとして追加します。
+--- !u!1 &141034563 +GameObject: (略) +--- !u!4 &141034564 +Transform: (略) + m_Father: {fileID: 1696434232}
.unityファイルに空のゲームオブジェクトとそのTransformを表すYAMLドキュメントが追記されました。
また,MyGameObjectに
--- !u!1 &1696434231 GameObject: (略) m_LocalScale: {x: .5, y: .600000024, z: 1.5} - m_Children: [] + m_Children: + - {fileID: 141034564} m_Father: {fileID: 0}
以下のような追記がされました。
MyGameObjectのTransformのYAMLドキュメントのidは1696434232。新たに追記した空のゲームオブジェクトのTransformのYAMLドキュメントのidは141034564です。
MyGameObjectのm_Childrenでは,新たに追加した空のゲームオブジェクトのTransfromを参照しています。
新たに追加した空のゲームオブジェクトのm_Fatherでは,MyObjectのTransfromを参照しています。
親子関係は,Transformで関係を作っていることが.unityでもわかります。
コンポーネントのプロパティの値の変更,ゲームオブジェクトへのコンポーネントの追加,子オブジェクトの追加でどのように差分があるのかを確認してみました。
コンポーネントのプロパティの値の変更くらいだったら,差分は追いやすいのですが,ゲームオブジェクトを新規追加すると,一つにつきGameObjectとTransformのYAMLドキュメントが必ず作成されるので差分が非常に多くなりますね。
次は,自作コンポーネントでの差分とプレハブなどの差分を確認したいと思います。
それではまた会えることを祈りつつ
ListViewが一番下までスクロールした時点で追加要素を読み込むようにしたら,スクロールしていないのに要素が1回追加されていた
結論から言うと原因は,AbsListView.OnScrollListenerのonScrollメソッドの仕様を勘違いしていて,このメソッドがスクロールしていなくても呼ばれていたためのようです。
Twitterクライアントのようなものを想定しています。
ListViewが一番下までスクロールした時点で追加要素を取得し,読み込み,リストビューに加えようとしていました。
こちらのブログを参考に,実装させていただきました。
http://visible-true.blogspot.jp/2010/12/listview.html
さて,
ActivityのonCreateメソッド内で次のように書いていました。
listView.addFooterView(LayoutInflater.from(footerViwe); listView.setOnScrollListener(new OnScrollListener() { @Override public void onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { boolean isLastItemVisible = totalItemCount == firstVisibleItem + visibleItemCount; if (isLastItemVisible && !isLoading()) { loadNewItems(); } } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } }); setListAdapter(adapter); loadNewItems();
footerViewにはプログレスバーなどが含まれています。loadItemsメソッド内で,追加要素をロードして,adapterのnotifyDatasetChangedメソッドを読んでいます。isLoading()メソッドは,名前通り追加要素を読み込み中かどうか判断します。onScrollメソッドが複数回呼ばれるため,複数回ロードされるのを防ぐための処理です。
このように,フッターでリストの末尾に常にプログレスバーを表示して,末尾までスクロールしたら追加要素のロードを開始するようにしていました。
さてこのようなコードで初期状態を20個,スクロールするごとに要素を20個追加しようとしました。リストの最後までスクロールするごとに,正しく20個要素が追加されます。
しかし,なぜ初期状態要素が40個になっていました。
どうやらloadNewItemsメソッドがスクロールしていなくても2回呼ばれいるようです。
調べてみると,
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
が,スクロールしていないのに複数回呼ばれていました。
そのうちの最初の1回は引数のfirstVisibleItem, visibleItemCount, totalItemCountは全て0になっており,そのタイミングでloadItemsメソッドが呼ばれてしまっていました。
そのため,onCreate内で1回目とOnScrollListenerのonScroll内で2回目,計2回loadItemsがよばれて初期の状態が40個の要素が入ってしまっていたようです。
また,どうやらユーザーが画面をスクロールしていなくても,ListViewに要素が追加されたタイミングでもonScrollが呼ばれているようです。
このように,onScrollメソッドの呼ばれるタイミングの仕様を勘違いしていたことが原因のようです。onCreateの中のloadItemsメソッドの呼び出しを消して解消しました。
Unityのシーンファイルの中身を見てみた (1)
Unity 4.2から今まで有料のPro版でしか使えなかった,マテリアル、プレハブ、シーンなどのテキストベースでのシリアルライズが無料版でも利用出来るようになりました。
(http://japan.unity3d.com/unity/whats-new/unity-4.2を参照)
無料版において,gitなどを用いたバージョン管理がしやすくなるかと思います。
今回は下記のページの内容を確認したり,シーン(.unityファイル)がどのようにシリアルライズされているかを確認してみました。(リンク先では,Pro専用機能とありますが(2013年7月31日時点),4.2ではしっかりと使えました。)
http://docs-jp.unity3d.com/Documentation/Manual/TextualSceneFormat.html
まず,シリアルライズ形式のYAML形式を非常に簡単に。
YAMLで配列は
- 1 - 2 - 3
という形式で表され,マップは,
a: 1 b: 2 c: 3
となります。
配列の配列は
- - a - b - - c - d - e
で,マップのマップは
U: a: 1 b: 2 V: c: 3 d: 4 e: 5
となるようです。
また,配列のマップは
U: - a - b V: - c - d - e
そして,マップの配列は次の用になります。
- a: 1 b: 2 - c: 3 d: 4 e: 5
フロー形式という記述方法もあるらしく
配列
[1, 2, 3]
マップ
{a: 1 , b: 2, c: 3}
配列の配列
- [a, b] - [c, d, e]
[[a, b], [c, d, e]]
マップのマップ
U: {a: 1, b: 2} V: {c: 3, d: 4, e: 5}
{U: {a: 1, b: 2}, V: {c: 3, d: 4, e: 5}}
配列のマップ
U: [a, b] V: [c, d, e]
{U: [a, b], V: [c, d, e]}
マップの配列
- {a: 1, b: 2} - {c: 3, d: 4, e: 5}
[{a: 1, b: 2}, {c: 3, d: 4, e: 5}]
だそうです。
また,---がYAMLドキュメントの区切りになっていて,これにより下記のように一つのテキストファイルに複数のYAMLドキュメントを含むことができるようです。
--- name: Taro age : 25 --- name: Jiro age : 22 --- name: Saburo age : 19
さて,やっと本題。
Unityのシーンファイル(.unityファイル)がどのようにYAMLで表されているかを見てみました。
先頭は以下の2行のようになっていました。
%YAML 1.1 %TAG !u! tag:unity3d.com,2011:
http://docs-jp.unity3d.com/Documentation/Manual/YAMLSceneExample.html
によるとシーンファイルはこの2行で始まらないといけないようです。
1行目は,YALMLディレクティブ,2行目は,タグディレクティブだそうです。
http://yaml.org/spec/1.2/spec.html#id2781553
http://yaml.org/spec/1.2/spec.html#id2782090
さて,シーン中のゲームオブジェクトそしてコンポーネントは一つのYAMLオブジェクトとして定義されるようです。
---の後に
!u!1 &6
だとか
!u!4 &8
等の文字列が含まれます。
この!u!のあとの最初の数字は下記のYAMLドキュメントのクラスを表し,そのあとの&のはシーンファイル中の一意のID番号だそうです。
下記は空のゲームオブジェクトをEmptyGameObjectという名前で,原点(0,0,0)に置いた際のシーンファイルの増分です。
--- !u!1 &1090478263 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 1090478264} m_Layer: 0 m_Name: EmptyGameObject m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!4 &1090478264 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 1090478263} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0}
GameObjectを表すYAMLドキュメントと,Transfrom表すYAMLドキュメントが確認できます。
それぞれのドキュメントはマップになっていてます。Transformを示すYAMLドキュメントでは,キーがTransformひとつ。そのバリューもマップになっていてTransformクラスのプロパティ名にm_をつけた物が並んでいます。
GameObjectも同じような感じです。
今回はこの辺で。
また次回があることを祈りつつ。
[参考リンク]
http://magazine.rubyist.net/?0009-YAML
http://yaml.org/spec/1.2/spec.html
http://yamllint.com/
http://d.hatena.ne.jp/tociyuki/20121130/1354278408
Photonを用いてオブジェクトの座標を同期してみる
Unity上でPhotonを使って,ネットゲームでのオブジェクトの座標の同期に挑戦してみました。
Photon Cloud公式のチュートリアルを見たところ(http://doc.exitgames.com/photon-cloud-jp/TutorialMarcoPolo/#cat-tutorialsなど)ネットゲームの大切な要素がたくさん詰まって(いそうだということが分かり)ました。(あまりしっかり理解できませんでした。)
大事な要素を少しずつに分けて確認したいと思います。
今回は必要最低限の構成で,プレイヤー間のオブジェクトの座標の同期を行うプロジェクトを作ってみました。
やれることとしては,
- 名称固定な部屋を作れる(詳しくは,こちらを)
- (存在している場合),名称固定な部屋に入れる
- 部屋から抜ける
- スペースキー(Androidの場合は画面タップで)で,Playerオブジェクト(Cupsule)が真上にジャンプ
- 同じ部屋にいる他のユーザーのオブジェクトの状態が見える
です。
次に作成手順です。
Photon Cloud AppIdとプロジェクトの設定は
プロジェクトの設定はこちらのページがとても参考になりました。
作成手順
- プロジェクトを新規作成,AppIdを設定
- 空のsceneを作成
- PlaneとDirectinal Lightをおく
- 空のGameObjectを作成し,名前をGameManagerにする
- 下記のMainMenuとGameManagerをGameManagerに追加
- Capsuleをおき,名前をPlayerに
- PlayerにJumperスクリプトを追加
- PlayerにPhoton Unity NetworkingのPhotonViewスクリプトを追加
- PlayerのPhotoViewのObserverインスペクター中に,Player自身をドラッグアンドドロップ
- Assets以下にResourcesディレクトリを作成。その中にPlayerをプレハブ化
- シーン中のPlayerを削除
こここまでである程度同期するけれど,カクカクしてしまいます。
- NetworkPlayer.csを作成し,Playerにコンポーネント追加
OnPhotonSerializeViewが呼ばれていません。プルプルしてまいす。(オブジェクトが本来あるべき点と原点とを行ったり来たりしているように見える)
- PlayerのインスペクターのObserver変更
Player中のObserにPlayer中のNetworkコンポーネントをドラッグアンドドラッグして,Player中のObserveを変更します。
PlayerやFloorにMaterialをつけた方が見やすいかと。
ソースコードを張っておきます。
おかしい点があったら,教えていただけると嬉しいです。
以下,ソースコード
GameManager.cs
using UnityEngine; using System.Collections; public class GameManager : Photon.MonoBehaviour { public string playerPrefabName = "Player"; void OnJoinedRoom () { StartGame (); } IEnumerator OnLeftRoom () { while (PhotonNetwork.room!=null || PhotonNetwork.connected==false) { yield return 0; } Application.LoadLevel (Application.loadedLevel); } void OnGUI () { if (PhotonNetwork.room == null) { return; } if (GUILayout.Button ("Leave & QUIT")) { PhotonNetwork.LeaveRoom (); } } void StartGame () { PhotonNetwork.Instantiate (playerPrefabName, getPositionRamdomly (), Quaternion.identity, 0); } Vector3 getPositionRamdomly () { return new Vector3 (Random.Range (-3.0f, +3.0f), 1, Random.Range (-3.0f, +3.0f)); } }
Jumper.cs
using UnityEngine; using System.Collections; [RequireComponent(typeof(PhotonView))] [RequireComponent(typeof(Rigidbody))] public class Jumper : Photon.MonoBehaviour { void Awake () { if (!photonView.isMine) { rigidbody.isKinematic = true; } } void Update () { if (Input.GetKeyDown (KeyCode.Space) || IsTapScreen ()) { rigidbody.AddForce (transform.up * 10.0f, ForceMode.Impulse); } } bool IsTapScreen () { if (Input.touchCount <= 0) { return false; } foreach (Touch touch in Input.touches) { if (touch.phase == TouchPhase.Began) { return true; } } return false; } }
MainMenu.cs
using UnityEngine; using System.Collections; public class MainMenu : MonoBehaviour { private const string ROOM_NAME = "JumpingSampleRoom"; void Awake () { if (!PhotonNetwork.connected) { PhotonNetwork.ConnectUsingSettings ("v1.0"); } PhotonNetwork.playerName = PlayerPrefs.GetString ("playerName", "Guest" + Random.Range (1, 9999)); } void OnGUI () { if (!PhotonNetwork.connected) { return; } if (PhotonNetwork.room != null) { return; } GUILayout.BeginArea (new Rect ((Screen.width - 400) / 2, (Screen.height - 300) / 2, 400, 300)); GUILayout.Label ("Main Menu"); { GUILayout.BeginHorizontal (); GUILayout.Label ("Player name:", GUILayout.Width (150)); PhotonNetwork.playerName = GUILayout.TextField (PhotonNetwork.playerName); if (GUI.changed) { PlayerPrefs.SetString ("playerName", PhotonNetwork.playerName); } GUILayout.EndHorizontal (); } GUILayout.Space (15); { GUILayout.BeginHorizontal (); GUILayout.Label ("JOIN ROOM:", GUILayout.Width (150)); GUILayout.Label (ROOM_NAME, GUILayout.Width (150)); if (GUILayout.Button ("GO")) { PhotonNetwork.JoinRoom (ROOM_NAME); } GUILayout.EndHorizontal (); } GUILayout.Space (15); { GUILayout.BeginHorizontal (); GUILayout.Label ("CREATE ROOM:", GUILayout.Width (150)); GUILayout.Label (ROOM_NAME, GUILayout.Width (150)); if (GUILayout.Button ("GO")) { PhotonNetwork.CreateRoom (ROOM_NAME, true, true, 10); } GUILayout.EndHorizontal (); } GUILayout.EndArea (); } }
NetworkCharacter.cs
using UnityEngine; using System.Collections; [RequireComponent(typeof(PhotonView))] public class NetworkCharacter : Photon.MonoBehaviour { private Vector3 correctPlayerPosision = Vector3.zero; void Update () { if (!photonView.isMine) { transform.position = Vector3.Lerp (transform.position, correctPlayerPosision, Time.deltaTime * 5); } } void OnPhotonSerializeView (PhotonStream stream, PhotonMessageInfo info) { if (stream.isWriting) { stream.SendNext (transform.position); } else { correctPlayerPosision = (Vector3)stream.ReceiveNext(); } } }
それでは,また会えることを祈りつつ。
ArrayListの中身を見てみた(1)
完全修飾名で言うと,
いつもお世話になっているこのクラスの実装がどうなっているのか?
気になったので中身を見てみました。
参照したのは,Android SDKの以下の
リファレンスは
docs/reference/java/util/ArrayList.html
ソースコードは,
sources/android-16/java/util/ArrayList.java
ArrayListクラスは,
AbstractListを継承していて,
- Cloneable
- Serializable
- RandomAccess
インターフェースを実装しています。
また,スーパークラスが実装しているインターフェースには,
- Collection
- List
インターフェースなどがあります。
コンストラクタ
とりあえずコンストラクタから。
- public ArrayList(int capacity)
引数capacityの値を初期容量とするコンストラクタ
- public ArrayList()
0を初期容量とするコンストラクタ
- public ArrayList(Coolection collection)
コレクションを引数に取り,そのコレクションの要素を含むインスタンスを作るコンストラクタ
の3種類があります。
2.と3.のコンストラクタについてですが,
Collectionインターフェースのドキュメントによると,
Collectionインターフェースを実装するクラスは少なくとも二つのコンストラクタを持つべきだそうです。
一つは引数なしで空のコレクションを作るコンストラクタ(2番)。
もう一つは,引数のコレクションと同じ要素を初期構成要素とするコレクションを作るコンストラクタ(3番)
3番のコンストラクタは,以下のように使うことができます.
public static void main(String[] args) { ArrayList<String> list; Map<String, String> map = new HashMap<String, String>(); map.put("a", "Taro"); map.put("b", "Jiro"); map.put("c", "Saburo"); list = new ArrayList<String>(map.values()); System.out.println(list); list = new ArrayList<String>(map.keySet()); System.out.println(list); Set<String> set = new HashSet<String>(); set.add("Tokyo"); set.add("Aichi"); set.add("Osaka"); set.add("Fukuoka"); list = new ArrayList<String>(set); System.out.println(set); }
容量を表すsize変数とオブジェクトを格納する配列
ArrayListクラスは次のようなフィールドを持ちます.
- 今いくつの要素を保持しているかを表すint型のsize変数
- オブジェクトを保持するObject[]型のarray
他にもありますが...
それはまた別の機会に。
sizeフィールドは,
例えば
ArrayListが何も保持していなければtrueを返す
public boolean isEmpty()
メソッドの中で,
@Override public boolean isEmpty() { return size == 0; }
のようにs使っていたり,
リスト中の特定の位置の要素を取り出すgetメソッドでは,
@SuppressWarnings("unchecked") @Override public E get(int index) { if (index >= size) { throwIndexOutOfBoundsException(index, size); } return (E) array[index]; }
の用に,
不正な引数のチェックに使ったりしています。
arrayフィールドは,
オブジェクトの保持に使っていて,
@Override public boolean add(E object) { Object[] a = array; int s = size; if (s == a.length) Object[] newArray = new Object[s + (s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)]; System.arraycopy(a, 0, newArray, 0, s); array = a = newArray; } a[s] = object; size = s + 1; modCount++; return true; }
addメソッドは,arrayに追加したい要素を加えます。
保持している要素の最後尾に新しい要素を加えます。
必要であればその途中でより大きな配列を生成して,
今まで保持していた要素をコピーすることで,
arrayのサイズをより大きくします。
ArrayList固有のメソッド
addメソッド・addAllメソッドを使うとフィールドarrayはどんどん大きくなっていきます。
ArrayListインスタンスの要素数・内部のarray型の配列長が10000になったとします。
その後removeメソッドで9000個の要素が削除され,要素数が1000になったとします。
この時,要素数1000に対してarrayの配列長は10000のままです。
このように大きくなってしまった内部の配列arrayのサイズを今の要素数のサイズまで小さくするメソッドがあります。
public void trimToSize()メソッドです。
public void trimToSize() { int s = size; if (s == array.length) { return; } if (s == 0) { array = EmptyArray.OBJECT; } else { Object[] newArray = new Object[s]; System.arraycopy(array, 0, newArray, 0, s); array = newArray; } modCount++; }
内部のarrayを小さくするメソッドもあれば,大きくするメソッドもあります。
addメソッドでちらっと触れたのですが,
要素を追加するときにArrayList型のインスタンスは必要があれば,
内部の配列arrayを大きくします。
非常に大きい配列を以下のようにArrayListに格納する時に,
// ids Integer型の配列 非常に要素数が多い // list Integer型を格納するArrayList for (Integer id : ids) { list.add(id); }
バックグラウンドでは,内部の配列arrayの拡張が複数回実行されてしまいます。
内部の配列の拡張は,より大きな配列の作成と今まで使っていた配列から新たな配列へのコピーが行われます。
内部の配列の拡張は少ないにこしたことはありません。
そこで,この後必要になる要素数が分かっていて,内部の配列がその大きさを確保したいときは,public void ensureCapacity(int minimumCapacity)メソッドが使えます。
public void ensureCapacity(int minimumCapacity) { Object[] a = array; if (a.length < minimumCapacity) { Object[] newArray = new Object[minimumCapacity]; System.arraycopy(a, 0, newArray, 0, size); array = newArray; modCount++; } }
以下のようにすれば,複数回内部の配列の拡張が行われることはありません。
// ids Integer型の配列 非常に要素数が多い // list Integer型を格納するArrayList list.ensureCapacity(ids.length); for (Integer id : ids) { list.add(id); }
また,前述したコンストラクタを以下のように使えば,配列の拡張の回数を減らすことができます.
ArrayList<Integer> list = new ArrayList<Integer>(ids.length); for (Integer id : ids) { list.add(id); }
まだまだ気になることがたくさんありましたので,
何回かArrayListを続けていこうと思います。
では,また会えることを祈りつつ.