CustomizedHoneycombNotepad 読み

iosched も再開しないとマズい、と思いつつ今日は Android Developer Lab Tokyo 2011 のノートパッドをカスタマイズしてみた。 というエントリで紹介されているプログラムのソースコードを読んでみます。エントリでも記述されている通り、github でもソースコードは配布されています。

  • AndroidManifest の確認

    読む時はまずここから。

    <?xml version="1.0" encoding="utf-8"?>
    <!-- Copyright (C) 2011 Google Inc.
     
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
     
              http://www.apache.org/licenses/LICENSE-2.0
     
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
    <!-- 
         Copyright (C) 2011 Yuki Anzai, uPhyca Inc.
             http://www.uphyca.com
     
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
     
              http://www.apache.org/licenses/LICENSE-2.0
     
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
     
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.uphyca.android.app.honeypad" android:versionCode="1"
        android:versionName="1.0">
     
        <uses-sdk android:minSdkVersion="11" />
        <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
     
            <activity android:name="com.uphyca.android.app.honeypad.NotepadActivity" 
                      android:label="@string/app_name"
                      android:logo="@drawable/logo"
                      android:theme="@style/MyTheme"
                      android:hardwareAccelerated="true">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
     
     
            <!-- notes content provider -->
            <provider android:name="com.uphyca.android.app.honeypad.NotesProvider"
                android:authorities="com.uphyca.android.app.honeypad.notesprovider" />
     
        </application>
     
    </manifest>

    ポイントとしては以下。

    • Activity は一つだけ
    • hardwareAccelerated って何でしょ(とりあえずスルー)
    • ContentProvider も一つ

    今回は NotesProvider はスルーします。ので、NotepadActivity のみ確認。

    NotepadActivity クラス

    クラスの定義は以下なカンジです。

    public class NotepadActivity extends Activity implements NoteListEventsCallback {
    

    普通の Activity クラスですが NoteListEventsCallback インターフェースを実装してます。こちらは NoteListFragment で出てきますので、とりあえずそのインターフェースなメソドが出てくるまでスルーで。

    属性

    ひとつだけ。

        private static final int MENU_ADD_ID = Menu.FIRST;
    

    これはオプションメニューの ID ですね。メニュー項目は一つのみな模様。

    onCreate メソド

    ここでレイアウトを setContentView しています。

        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.notepad);
    

    res/layout/notepad.xml が以下。ちなみにこのアプリは横 (landscape) がデフォらしく、res 配下に layout-port というフォルダがあります。このエントリではスルーしますが、確認してみると面白いと思います。

    <?xml version="1.0" encoding="utf-8"?>
    <!-- Copyright (C) 2011 Google Inc.
     
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
     
              http://www.apache.org/licenses/LICENSE-2.0
     
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
    <!-- 
         Copyright (C) 2011 Yuki Anzai, uPhyca Inc.
             http://www.uphyca.com
     
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
     
              http://www.apache.org/licenses/LICENSE-2.0
     
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
     
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal" 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/component_margin">
     
        <fragment 
            android:name="com.uphyca.android.app.honeypad.NoteListFragment"
            android:id="@+id/list" 
            android:layout_weight="1" 
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_margin="@dimen/component_margin" />
     
        <LinearLayout 
            android:id="@+id/note_detail_container"
            android:orientation="horizontal" 
            android:layout_weight="2"
            android:layout_width="0dip" 
            android:layout_height="match_parent"
            android:background="@color/edit_bg"
            android:layout_margin="@dimen/component_margin" />
     
    </LinearLayout>

    水平方向の LinearLayout に Fragment と LinearLayout が並んでます。内側の LinearLayout なナニには別途で View が inflate されるのでしょうか。Fragment はパケジ修飾されたクラス名が指定されてます。このレイアウトが load された時点でここに組込むための諸々の処理が動くものと思われます。
    この NoteListFragment クラスについては別途確認することとして NotepadActivity クラスの onCreate メソドの続きに戻ると、

    • actionBar の初期設定
      • 3.* 以降の SDK だと actionBar は標準装備なようです
      • optionsMenu 云々で proxy させている模様
    • backGround の初期設定

    というあたりの処理をしている模様。基本的にメソドの解説はざっくりベースで済ませますのでご容赦願います。

    onCreateOptionsMenu メソド

    これが actionBar の初期設定になりますね。

        public boolean onCreateOptionsMenu(Menu menu) {
            super.onCreateOptionsMenu(menu);
            MenuItem add = menu.add(0, MENU_ADD_ID, 0, R.string.menu_add);
            add.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
                    | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
            add.setIcon(R.drawable.ic_menu_add);
            return true;
        }
    

    ええと、setShowAsAction メソドですが、上記コードの通りにすると、アイコン+テキストが actionBar に出力されるとのことです。ので、setIcon してるのかな。

    onMenuItemSelected メソド

    こちらは actionBar な optionsMenu の処理の記述になります。

        public boolean onMenuItemSelected(int featureId, MenuItem item) {
            switch (item.getItemId()) {
            case MENU_ADD_ID:
                showNote(null);
                return true;
            }
            return super.onMenuItemSelected(featureId, item);
        }
    

    Add Note 選択時には showNote というメソドに null を渡してます。

    showNote メソド

    このメソドが呼び出されるケイスとしては

    • 新規作成時
    • 左側にある ListView の項目選択時

    の二つのケイスがあります。また右側の LinearLayout に対して View が add されている場合とそうでなない場合の二つのケイスがあります。順に処理を確認してみます。ざっくり目で。

        private void showNote(final Uri noteUri) {
            // check if the NoteEditFragment has been added
            FragmentManager fm = getFragmentManager();
            NoteEditFragment edit = (NoteEditFragment) fm.findFragmentByTag("Edit");
    

    Edit なタグが付いている fragment を探します。以降、この fragment があるかないかで処理が分岐します。

            if (edit == null) {
                // add the NoteEditFragment to the container
                FragmentTransaction ft = fm.beginTransaction();
                edit = new NoteEditFragment();
                ft.add(R.id.note_detail_container, edit, "Edit");
                ft.commit();
            } else if (noteUri == null) {
                edit.clear();
            }
    

    edit が null であるならとりあえず作って追加します。ft.commit() 必須な模様。null ではなくて新規作成の場合は clear メソドを呼び出します。このメソドは NoteEditFragment で定義されていますので、別途確認します。

            if (noteUri != null) {
                edit.loadNote(noteUri);
            }
            else {
                NoteListFragment list = (NoteListFragment) fm.findFragmentById(R.id.list);
                if(list != null) {
                    list.getListView().clearChoices();
                }
            }
    

    次は新規作成で無い場合、NoteEditFragment の loadNote メソドを呼び出してます。これも別途確認します。最後に新規作成で左側のリストが空で無い場合には、選択マークをクリアしてます。

    onNoteSelected メソド

    これは NoteListEventsCallback インターフェースのメソドになります。

        public void onNoteSelected(Uri noteUri) {
            showNote(noteUri);
        }
    

    リスト選択時にこれが呼びだされるんですかね。

    onNoteDeleted メソド

    こちらは削除時に呼び出されるのでしょうか。コメントによれば、削除処理後には NoteEditFragment は削除、という記述があります。

        public void onNoteDeleted() {
            // remove the NoteEditFragment after a deletion
            FragmentManager fm = getFragmentManager();
            NoteEditFragment edit = (NoteEditFragment) fm.findFragmentByTag("Edit");
            if (edit != null) {
                FragmentTransaction ft = fm.beginTransaction();
                ft.remove(edit);
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }
        }
    

    上記の FragmentTransaction 取得から commit までの流れは詳細に確認してみる必要があると思います。が、ここではスルーします。

    NoteListFragment クラス

    ListFragment クラスを継承しています。

    public class NoteListFragment extends ListFragment {
    

    内部で interface を定義してます。

        public interface NoteListEventsCallback {
            public void onNoteSelected(Uri noteUri);
    
            public void onNoteDeleted();
        }
    

    NotepadActivity が実装しているインターフェースですね。他、諸々の属性とか空のコンストラクタの定義があります。

        private static final int DELETE_ID = Menu.FIRST + 1;
    
        private Cursor mNotesCursor;
    
        private NoteListEventsCallback mContainerCallback;
    
        public NoteListFragment() {
    
        }
    

    DELETE_ID はコンテキストメニュの ID で、Cursor は List 用ですね。NoteListEventsCallback 型の属性は何のためなのか、は以降で出てくると思われます。
    あと、初期処理な callback ですが、以下な順で呼び出されるとのことです。

    • onAttach
    • onCreate
    • onCreateView
    • onActivityCreated
    • onStart
    • onResume

    なんとなく上記を見てると最初に親の Activity に attach されてから View が生成されるんですね。あるいは子供ができて親が、という形になっているのが想像できます。

    onActivityCreated メソド

    親 Activity の生成が完了した時、なのかどうか。定義が以下です。

        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            
            setEmptyText(getActivity().getString(R.string.no_notes));
            
            fillData();
            registerForContextMenu(getListView());
        }
    

    ここではリストの初期設定を行なっているのか。

    • 選択モードの設定
    • 空の場合に出力される文字列
    • データの読み込みと Adapter への設定
    • コンテキストメニュの設定

    などを行なっているものと思われます。

    onAttach メソド

    こちらはいっちゃん最初に呼び出される callback ですね。親 Activity に attach された直後に Activity とのやりとりをする準備ができる、ということで定義が以下。

        public void onAttach(Activity activity) {
            super.onAttach(activity);
            try {
                // check that the containing activity implements our callback
                mContainerCallback = (NoteListEventsCallback) activity;
            } catch (ClassCastException e) {
                activity.finish();
                throw new ClassCastException(activity.toString()
                        + " must implement NoteSelectedCallback");
            }
        }
    

    mContainerCallback 属性に引数をキャストして代入してます。これは後々 NotepadActivity のメソドを呼び出すための準備ですね。

    fillData メソド

    手続き定義を以下に引用。基本的には Notepad と変わりは無いんですが、コメントにあるように startManagingCursor が deprecate されて代わりに CursorLoader クラスを使いなさい、ということになった模様。詳細は Notepad Tutorial を確認して下さい。

        private void fillData() {
            // Get all of the rows from the database and create the item list
            CursorLoader loader = new CursorLoader(getActivity(), NotesProvider.CONTENT_URI, null, null, null, null);
            mNotesCursor = loader.loadInBackground();
    
            // startManagingCursor() is deprecated, use CursorLoader instead
    //        mNotesCursor = getActivity().getContentResolver().query(
    //                NotesProvider.CONTENT_URI, null, null, null, null);
    //        getActivity().startManagingCursor(mNotesCursor);
    
            // Create an array to specify the fields we want to display in the list
            // (only TITLE)
            String[] from = new String[] { NotesProvider.KEY_TITLE };
    
            // and an array of the fields we want to bind those fields to (in this
            // case just text1)
            int[] to = new int[] { android.R.id.text1 };
    
            // Now create a simple cursor adapter and set it to display
            SimpleCursorAdapter notes = new SimpleCursorAdapter(getActivity(),
                    R.layout.list_item, mNotesCursor, from, to);
            setListAdapter(notes);
        }
    

    onCreateContextMenu メソド

    次はコンテキストメニュに関するメソド定義です。Notepad ではリストの項目長押しでコンテキストメニュが表示される形になっています。下記ではメニュの定義をしています。

        public void onCreateContextMenu(ContextMenu menu, View v,
                ContextMenuInfo menuInfo) {
            super.onCreateContextMenu(menu, v, menuInfo);
            menu.add(0, DELETE_ID, 0, R.string.menu_delete);
        }
    

    onContextItemSelected メソド

    コンテキストメニュの項目選択時に呼び出されるメソドとなります。

        public boolean onContextItemSelected(MenuItem item) {
            switch (item.getItemId()) {
            case DELETE_ID:
                AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
                        .getMenuInfo();
                getActivity().getContentResolver().delete(
                        ContentUris.withAppendedId(NotesProvider.CONTENT_URI,
                                info.id), null, null);
                fillData();
                mContainerCallback.onNoteDeleted();
                return true;
            }
            return super.onContextItemSelected(item);
        }
    

    削除する項目の id を取得して ContentProvider に delete 依頼を出してます。このあたり、adapter って便利ですよね。削除したので fillData メソドを呼び出して、親 Activity の onNoteDeleted メソドを呼び出して、右側の Fragment を削除してもらっています。こういった形で連携して動かすのですね。

    onListItemClick メソド

    次はリストの項目が選択された時の処理です。定義を以下に引用。

        public void onListItemClick(ListView l, View v, int position, long id) {
            super.onListItemClick(l, v, position, id);
            Uri noteUri = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, id);
            mContainerCallback.onNoteSelected(noteUri);
            ((CheckedTextView)v).setChecked(true);
        }
    

    親 Activity の onNoteSelected メソドを呼び出して右側のソレに NoteEditFragment を表示してもらいます。Uri の取得方法とかメソドの引数とかが Android ぽくて面白いですね。
    あと、このクラスには onActivityResult メソドが定義されてるのですが、使われていないと思われますので、略します。

    NoteEditFragment クラス

    右側に出てくるソレです。クラス定義の先頭が以下な記述。

    public class NoteEditFragment extends Fragment {
    

    ある意味普通のパターンな模様。こちらは Activity 起動時に load されるのではなくて、手動で明示的に load/unload される Fragment です。
    属性が以下。

        private EditText mTitleText;
        private EditText mBodyText;
        private Uri mCurrentNote;
    
        public NoteEditFragment() {
        }
    

    こいつもコンストラクタが空ですね。あとは出力および更新用の属性でしょうか。

    onCreateView メソド

    これはどのタイミングなのかな。add された時なのか commit された時なのか。確認してみたところ、commit された時点になるようです。以下な部分。

            if (edit == null) {
                // add the NoteEditFragment to the container
                FragmentTransaction ft = fm.beginTransaction();
                edit = new NoteEditFragment();
                ft.add(R.id.note_detail_container, edit, "Edit");
                ft.commit();
            } else if (noteUri == null) {
                edit.clear();
            }
    

    edit が null で NoteEditFragment なオブジェクトを生成して FragmentTransaction#commit を呼び出した時点で onCreateView が起動される模様です。
    で、これらを前提に先頭から順に手続き定義を見ていきます。

        public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
            View v = inflater.inflate(R.layout.note_edit, container, false);
    

    container には note_detail_container な id の LenearLayout なオブジェクトが格納されているのだろうな、と類推。そこに対して note_edit.xml なレイアウトを云々しているものと思われます。レイアウトの引用は略します。
    以降はほぼ通常の Activity の初期設定と同様で

            mTitleText = (EditText) v.findViewById(R.id.title);
            mBodyText = (EditText) v.findViewById(R.id.body);
    
            Button saveButton = (Button) v.findViewById(R.id.save);
            saveButton.setOnClickListener(new View.OnClickListener() {
    
                public void onClick(View view) {
                    InputMethodManager inputMethodManager = 
                        (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
                    inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
    
                    saveNote();
                }
            });
            populateFields();
            return v;
        }
    
    • View の参照を属性に取っておいたり
    • ボタンの callback を定義したり
    • 表示が必要であればデータを読みこんで表示したり

    などを行なっている模様です。
    あ、あと onClick メソドの中の InputMethodManager 云々なあたりは面白そうですね。

    populateFields メソド

    ここで使用されている mCurrentNote 属性は NotepadActivity から NoteEditFragment なメソドが新規作成時あるいは既存レコード編集時に呼び出される時に設定される Uri オブジェクトです。これを基にレコードの表示をしている模様です。

        private void populateFields() {
            if (mCurrentNote != null) {
                Cursor c = null;
                try {
                    c = getActivity().getContentResolver().query(mCurrentNote,
                            null, null, null, null);
                    if (c.moveToFirst()) {
                        mTitleText.setText(c.getString(NotesProvider.TITLE_COLUMN));
                        mBodyText.setText(c.getString(NotesProvider.BODY_COLUMN));
                    }
                } finally {
                    if (c != null) {
                        c.close();
                    }
                }
            }
        }
    

    ちなみに NotepadActivity から呼び出されるメソドはもう少ししたら出てきます。

    buildTitleFromDate メソド

    こちら、title フィールドが空だった場合に強制で title 採番してしまうヘルパメソドです。

        private String buildTitleFromDate() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            sdf.setTimeZone(TimeZone.getDefault());
            String suffix = sdf.format(Calendar.getInstance().getTime());
            return suffix;
        }
    

    saveNote メソド

    保存処理。ここは Notepad Tutorial をさして変わりはないようなのでスルーします。

        private void saveNote() {
            String title = mTitleText.getText().toString();
            if(TextUtils.isEmpty(title)) {
                title = getActivity().getString(R.string.new_note_name, buildTitleFromDate());
            }
            
            // save/update the note
            ContentValues values = new ContentValues(2);
            values.put(NotesProvider.KEY_TITLE, title);
            values.put(NotesProvider.KEY_BODY, mBodyText.getText().toString());
            if (mCurrentNote != null) {
                getActivity().getContentResolver().update(mCurrentNote, values,
                        null, null);
            } else {
    v            getActivity().getContentResolver().insert(
                        NotesProvider.CONTENT_URI, values);
            }
            Toast.makeText(getActivity(), "Note Saved", Toast.LENGTH_SHORT).show();
        }
    

    loadNote メソド

    このあたりが NotepadActivity から呼び出されているメソドになります。loadNote は以下な形で NotepadActivity#showNote メソド呼び出されていますね。

            if (noteUri != null) {
                edit.loadNote(noteUri);
            }
    

    新規作成ではない場合、Uri を渡して表示しておけ、と。

        protected void loadNote(Uri noteUri) {
            mCurrentNote = noteUri;
            if (isAdded()) {
                populateFields();
            }
        }
    

    clear メソド

    こちらも NotepadActivity からの呼び出し箇所を確認してみますと

            } else if (noteUri == null) {
                edit.clear();
            }
    

    showNote メソドで新規作成の場合には空で表示してね、なソレですね。

        protected void clear() {
            mTitleText.setText(null);
            mBodyText.setText(null);
            mCurrentNote = null;
        }
    

    ざっくり気味というかスルーし杉なエントリですがご容赦下さい。こうした List – Detail な形のコンテンツアプリの場合、Fragment で作りこむのはこうした例から見てもとても効果的だと思われます。

    追記

    ざっくりベースで読んじゃいましたが Fragment 関連については @yanzm さんの以下のエントリを確認すればほぼ事足りてますので、是非ご確認を。