ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

2013年8月11日日曜日

ListAdapterのgetViewが呼び出される階層を調べてみた

ListViewを継承して独自のカスタムViewを作ろうとしてた時に必要になったので、 ListAdapterのgetViewが呼び出される階層を調べてみた。
android.widget.ListAdapter#getView
↑
android.widget.AbsListView#obtainView
↑
android.widget.ListView#makeAndAddView
↑
android.widget.ListView#fillDown
↑
android.widget.ListView#fillGap
↑
android.widget.AbsListView#trackMotionScroll
↑
android.widget.AbsListView#scrollIfNeeded
↑
android.widget.AbsListView#onTouchEvent
まだ続いてるけどここまでわかればOKでしょ。 ちなみにこれ4.2.2の挙動ね。

2013年5月25日土曜日

Android StudioでRecent Projectのリストをクリアする

Android Studioでいろいろ触りまくっているとRecent Projectがいっぱいになってすごい邪魔になる。IntelliJだとRecent Projectをクリアするメニューがあるんだけど、Android Studioだと見つからなかった。なので、無理やりRecent Projectをクリアする方法をメモ。基本的にはIntelliJで無理やりやる方法と一緒。ちなみにMacね。

以下のファイルを開く
/Users/<username>/Library/Preferences/AndroidStudioPreview/options/other.xml

んで以下のあたりのrecentPathsのlistの中身を消す。
<component name="RecentProjectsManager">
  <option name="recentPaths">
    <list>
      <option value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
    </list>
  </option>
  <option name="names">
    <map>
      <entry key="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" value="" />
      <entry key="$USER_HOME$/dev/github/ActionBarSherlock/actionbarsherlock-samples/fragments" value="" />
    </map>
  </option>
  <option name="lastPath" value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
</component>

以下が修正後。
<component name="RecentProjectsManager">
  <option name="recentPaths">
    <list>
    </list>
  </option>
  <option name="names">
    <map>
      <entry key="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" value="" />
      <entry key="$USER_HOME$/dev/github/ActionBarSherlock/actionbarsherlock-samples/fragments" value="" />
    </map>
  </option>
  <option name="lastPath" value="$USER_HOME$/AndroidStudioProjects/SampleUseLibrary" />
</component>

2013年1月3日木曜日

Androidで「Unknown error merging manifest」

AndroidでライブラリプロジェクトのAndroidManifest.xmlをメインプロジェクトに自動でマージさせるにはproject.propertiesにmanifestmerger.enabled=trueを加えるだけで良い。 しかし、メインプロジェクトのandroid:targetSdkVersionがライブラリプロジェクトより低いと以下のようなエラーメッセージが表示される。
Unknown error merging manifest
もっとわかりやすいメッセージを表示してほしいよね。

2012年5月18日金曜日

さくらVPSのUbuntu10.04(64bit)にgit + maven + Jenkinsな環境構築をしてAndroidのCIが出来るまでのメモ

Androidに限らずgit + Jenkins + mavenな環境はよく使うと思うけど、初回のビルドはだいたいこける。
環境が整備出来ていないという理由でね。
ということでAndroidプロジェクトがCI出来るまでにやったことをメモしておく。

Ubuntuインストール・設定


さくらVPSにカスタムOSのUbuntu 10.04をインストール+セキュリティ設定などをした時のメモ
を参考にインストール・設定を行う。
(ここではsshのport変更はやってない)

Jenkinsインストール


Jenkinsをインストールするとjenkinsユーザーが自動で作成されるけどなんとなく先に作っておいた。
$ sudo adduser jenkins
インストールはここを参考に行う。

ビルド環境構築


openjdk-6-jdkとmavenをインストールする。
$ sudo apt-get install openjdk-6-jdk
mavenインストールはここを参考に行う。

Android SDKインストール


まず、32ビット版のソフトを動かすためにia32-libsをインストールする。(ハマりポイント)
$ sudo apt-get install ia32-libs

jenkinsユーザーから実行出来るようにする必要があるのでjenkinsユーザーで行う。
$ mkdir -p /var/lib/jenkins/tools
$ cd /var/lib/jenkins/tools
$ wget http://dl.google.com/android/android-sdk_r18-linux.tgz // バージョンは適宜変更
$ tar zxvf android-sdk_r18-linux.tgz
$ ./android-sdk-linux/tools/android update sdk -u

gitosisインストール・設定


まず、gitosisをインストールする。今までapt-getで今回はaptitudeだけど気にしないw
$ sudo aptitude install gitosis

gitosisの初期化の際に一人目の管理者の公開鍵が必要なのでgitosisユーザーから見える箇所に配置する。
(ここでは同サーバー内のubuntuユーザーを一人目の管理者とする)
$ cp -p /home/ubuntu/.ssh/id_rsa.pub /tmp/.
$ sudo su - gitosis
$ gitosis-init < /tmp/id_rsa.pub

gitリポジトリ作成


gitosis管理者ユーザーで管理情報リポジトリをクローンする。(クローン済みならpullする)
$ git clone gitosis@ホスト名:gitosis-admin.git

gitosis.confを修正する。(XXXXは任意ユーザー)
[gitosis]
loglevel=DEBUG

[group gitosis-admin]
writable = gitosis-admin
members = XXXX

[group リポジトリ名]
writable = リポジトリ名
members = XXXX jenkins

修正が終わったらjenkinsユーザーのssh公開鍵をkeydir配下に配置する。
(ssh公開鍵はパスフレーズなしで作成)
コミットしてプッシュしたら実際にリポジトリを作成する。
$ cd /srv/gitosis/repositories
$ sudo mkdir リポジトリ名.git
$ cd リポジトリ名.git
$ sudo git init --bare --shared=true
$ sudo chown -R gitosis:gitosis ./

jenkinsユーザーまたはmembersに記載した任意のユーザーから以下コマンドを実行してクローン出来ることを確認する。
$ git clone gitosis@ホスト名:リポジトリ名.git

Jenkinsセットアップ


jenkinsユーザーがGitリポジトリと通信するためknown_hostsに追加する必要がある。(重要)
(ここではGitリポジトリとJenkinsが同サーバ)
$ sudo -u jenkins ssh gitosis@localhost // yesを答える

続いてクローン時に必要になるgitconfigの設定を行う。
$ sudo su - jenkins
$ git config --global user.email "jenkins@jenkins-server"
$ git config --global user.name "jenkins"

http://your-jenkins-server:8080にアクセスする。
Jenkinsの管理 -> プラグインの管理から以下の3つを選択してインストールする。
  • Git plugin
  • Android Emulator Plugin
  • Android Lint Plugin
続いてJenkinsの管理 -> システムの設定から以下を設定する。
  • Android
    • Android SDK root : /var/lib/jenkins/tools/android-sdk-linux
  • Maven
    • Name : Default(任意)
    • MAVEN_HOME : /usr/local/maven

ビルドしてみる


上記で作成したリポジトリにプロジェクトを追加してプッシュする。
android-archetypesのandroid-with-testを使用してプロジェクトを作成する。
正常に作成出来たらリポジトリにプッシュする。(Jenkinsからクローン出来るように)
mvn archetype:generate \
  -DarchetypeArtifactId=android-with-test \
  -DarchetypeGroupId=de.akquinet.android.archetypes \
  -DarchetypeVersion=1.0.8 \
  -DgroupId=your.group \
  -DartifactId=your-project-name \
  -Dpackage=your.package

http://your-jenkins-server:8080にアクセスし新規ジョブ作成よりMaven2/3プロジェクトのビルドのジョブを作成する。

ソースコード管理システムでGitを選択し、Repository URLにgitosis@ホスト名:リポジトリ名.gitを入力する。

ビルドのルートPOMにリポジトリから見たルートのpom.xmlのパスを入力する。
ゴールとオプションにinstallを入力する。
ビルド環境の"Run an Android emulator during build"にチェックを入れ、エミュレーターの設定項目を入力する。
"Show emulator window"のチェックを外す。(重要)
ビルド後の処理の追加をクリックして成果物を保存を選択する。
保存するファイルにメインプロジェクトのapkを指定すればいい。
保存、ビルド実行して"Finished: SUCCESS"がコンソールに出力されればOK。
最後に成果物としてapkが存在していれば完了〜

2012年3月6日火曜日

android-maven-pluginのライフサイクル

android-maven-pluginのcomponents.xmlで定義されているライフサイクルを表にまとめてみた。

packagingがapkの場合(通常のAndroidアプリケーション)


phase goal
generate-sources android:generate-sources
process-resources resources:resources
compile compiler:compile
process-classes android:proguard
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
prepare-package android:dex
package jar:jar, android:apk
install install:install
pre-integration-test android:internal-pre-integration-test
integration-test android:internal-integration-test
deploy deploy:deploy

packagingがapklibの場合(Androidライブラリ)


phase goal
generate-sources android:generate-sources
process-resources resources:resources
compile compiler:compile
process-classes android:proguard
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
package jar:jar, android:apklib
install install:install
deploy deploy:deploy

components.xml見るとprepare-packageフェイズでandroid:emmaを呼び出しているように見えるけど
android:emmaゴールなんて見つからんぞー!?

2012年2月19日日曜日

AndroidのSpinner内のテキストってselector効かなくね?


上記のように条件によってSpinnerを選択不可にしたいパターンはよくあると思う。
ついでにSpinner内のテキストの色も選択 / 選択不可に応じて色を変えたい場合もあるだろう。
通常の例に沿って下記のようなセレクタとスタイルを用意してテキストカラー変更を試みてみた。
color/spinner_text.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_enabled="false" android:color="@android:color/darker_gray" />
    <item android:color="@android:color/black" />
</selector>
values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme" parent="@android:style/Theme">
        <item name="android:spinnerItemStyle">@style/SpinnerItem</item>
    </style>

    <style name="SpinnerItem" parent="@android:style/Widget.TextView.SpinnerItem">
        <item name="android:textColor">@color/spinner_text</item>
    </style>
</resources>

だけど、なぜか選択不可状態の色が反映されず...
ということで代替案で対応させてみたのが下記。
SpinnerActivity.java
public class SpinnerActivity extends Activity implements
        OnCheckedChangeListener {

    private Spinner mSpinner;
    private RadioGroup mRadioGroup;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mSpinner = (Spinner) findViewById(R.id.spinner);
        String[] brands =
                new String[] { "LARK", "Seven Stars", "MILD SEVEN", "etc" };
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_spinner_item, brands);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        mSpinner.setAdapter(adapter);

        mRadioGroup = (RadioGroup) findViewById(R.id.group);
        mRadioGroup.setOnCheckedChangeListener(this);
    }

    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        RadioButton radioButton = (RadioButton) findViewById(checkedId);
        if (radioButton.isChecked()) {
            spinnerControl(checkedId);
        }
    }

    /**
     * Spinnerの選択状態を制御する
     * @param checkedId
     */
    private void spinnerControl(int checkedId) {

        int color = Color.BLACK;
        switch (checkedId) {
        case R.id.yes:
            mSpinner.setEnabled(true);
            break;

        case R.id.no:
            mSpinner.setEnabled(false);
            color = Color.GRAY;
            break;
        }
        // SpinnerからTextViewを取り出してテキストカラーを設定
        TextView textView = (TextView) mSpinner.getChildAt(0);
        textView.setTextColor(color);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        int checkedId = mRadioGroup.getCheckedRadioButtonId();
        spinnerControl(checkedId);
    }
}

はい完成。ポイントは50行目〜51行目。
誰かセレクタで出来た人いたらやり方おせ〜て〜。

2012年2月18日土曜日

パッケージが更新された(バージョンが上がった)時のBroadcastについて

パッケージが更新された場合、PACKAGE_REMOVED→PACKAGE_ADDED→PACKAGE_REPLACED
の順番でBroadcastが投げられる。
個人的には違和感ありまくりなんだけど、内部的には一旦削除してから追加していて
このような挙動になっているんだと思う。(確かめてはないけど...)

しかし、パッケージ更新時のみPACKAGE_REMOVEDやPACKAGE_ADDEDの処理を
スキップしたいことはあると思う。そんな時は以下のようにインテントからデータを
抜き出して判定してやればいい。
public class PackageMonitor extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();

        // パッケージ更新の場合はスキップ
        if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
            return;
        }

        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            // パッケージが追加された時にしたい処理
        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
            // パッケージが削除された時にしたい処理
        }
    }

}

2012年2月10日金曜日

RenamingDelegatingContextを使ってみた

昨年(2011年)の8月に開催されたAndroidテスト祭りで@ussy00さんの発表を聞いてRenamingDelegatingContextの存在を
知ってから結構経ってしまったけど、ようやく仕事でDBを使う機会が出来たので使ってみた。

なにが出来るの?


RenamingDelegatingContextを使うことでテスト用のプレフィックスのついたSQLiteファイルが用意され、
毎回クリーンなデータベース環境が手に入る。

使ってみる


一応テスト対象のクラス等も紹介しておく。

Employee.java(DTO的な何か。必要に応じてシリアライズ可能に。)
※ ゲッター/セッター、import省略
public class Employee {

    public static class EmployeeColumns {

        public static final String ID = "_id"; // 社員ID

        public static final String NAME = "name"; // 社員名

        public static final String DEPARTMENT = "department"; // 部署
    }

    public static final String TABLE_NAME = "employee";

    private int id;
    private String name;
    private String department;
}

テーブル構成も簡単にしてみた。
DatabaseHelper.java(ヘルパー)
※ import省略
public class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "company.db";
    private static final int DATABASE_VERSION = 1;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE " + Employee.TABLE_NAME + "(" +
                Employee.EmployeeColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                Employee.EmployeeColumns.NAME + " TEXT NOT NULL," +
                Employee.EmployeeColumns.DEPARTMENT + " TEXT NOT NULL" +
                ")");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

EmployeeDao.java(データベースアクセスクラス。今回のテスト対象クラス。)
※ import省略
public class EmployeeDao {

    private DatabaseHelper mHelper;

    public EmployeeDao(Context context) {
        mHelper = new DatabaseHelper(context);
    }

    /**
     * 部署を条件に社員一覧を取得する。
     * 順不同
     * @param department
     * @return 社員一覧 0件の場合は空のリスト
     */
    public List<Employee> getEmployeeByDepartment(String department) {
        List<Employee> employees = new ArrayList<Employee>();
        SQLiteDatabase db = mHelper.getReadableDatabase();

        try {
            Cursor c = db.query(Employee.TABLE_NAME,
                    new String[]{ EmployeeColumns.ID,
                            EmployeeColumns.NAME,
                            EmployeeColumns.DEPARTMENT },
                    EmployeeColumns.DEPARTMENT + " = ?",
                    new String[]{ department }, null, null, null);
            c.moveToFirst();
            while (!c.isAfterLast()) {
                Employee employee = new Employee();
                employee.setId(c.getInt(c.getColumnIndex(EmployeeColumns.ID)));
                employee.setName(c.getString(c.getColumnIndex(EmployeeColumns.NAME)));
                employee.setDepartment(c.getString(c.getColumnIndex(EmployeeColumns.DEPARTMENT)));
                employees.add(employee);
                c.moveToNext();
            }
            c.close();
        } finally {
            db.close();
        }
        return employees;
    }
}

そして今回のポイントとなるテストクラス。
EmployeeDaoTest.java
※ import省略
public class EmployeeDaoTest extends AndroidTestCase {

    private static final String TEST_PREFIX = "test_";
    private DatabaseHelper mHelper;
    private RenamingDelegatingContext mContext;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
        // テストメソッド毎に空のテスト用DBを用意
        mHelper = new DatabaseHelper(mContext);
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mHelper.close();
    }

    /**
     * getEmployeeByDepartmentのテスト
     */
    public void testGetEmployeeByDepartment() {
        // RenamingDelegatingContextを渡してテスト用DBを使用する
        EmployeeDao dao = new EmployeeDao(mContext);
        List<Employee> employees = dao.getEmployeeByDepartment("人事部");
        assertNotNull(employees);
        // これ以降のテストは省略
    }
}

ソースを見ればわかると思うけど手順は簡単で
  • Context と文字列を渡してRenamingDelegatingContextを生成
  • 生成したRenamingDelegatingContextを渡してヘルパーを生成
以上。

これを実行すると。。。
テスト用のDBがちゃんとあるね。

2012年1月26日木曜日

AndroidでDBの存在確認

AndroidでDBの存在確認の仕方は他にもあるけど、
データベースの保存場所がストレージであれば「/data/data/パッケージ名/databases/DB名」
に保存されるのでファイルの存在確認するのが手っ取り早い。
String DB_NAME = "test.db";

File file = new File(context.getDatabasePath(DB_NAME).getPath());
boolean dbExists = file.exists();

2012年1月11日水曜日

Androidでステータスバー(通知バー)を起動する

ステータスバーとか通知バーとかいろんな言い方あるけど、とにかくコレのこと↓

これを起動するのにちょいと手間がかかる。かかるといってもたいしたことはないけど。
まず、AndroidManifest.xmlに下記パーミッションを追加。
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />

あとはこんな感じのソースを書いて呼べばOK。
private void showNotifications() {
    try {
        Object service = getSystemService("statusbar");
        if (service != null) {
            Method expand = service.getClass().getMethod("expand");
            expand.invoke(service);
        }
    } catch (Exception e) {
    }
}

ちなみにHideなAPI使ってるのでいきなり使えなくなるかも?

2011年12月26日月曜日

Androidでファイルの入出力

汎用的なユーティリティー系の処理はその都度書いていては時間の無駄なので
ファイルの入出力の処理をコピペ出来るようにここに貼付けておく。
ちなみにファイルの入出力先は「/data/data/パッケージ名/files/」

FileUtils.java
package yourpackage;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

import android.content.Context;

public class FileUtils {

    /**
     * ファイルへ文字列を書き込み
     * @param context
     * @param str ファイル出力文字列
     * @param fileName ファイル名
     */
    public static void writeFile(Context context, String str, String fileName) {
        writeBinaryFile(context, str.getBytes(), fileName);
    }

    /**
     * ファイルへバイナリデータを書き込み
     * @param context
     * @param data バイトデータ
     * @param fileName ファイル名
     */
    public static void writeBinaryFile(Context context, byte[] data, String fileName) {
        OutputStream out = null;
        try {
            out = context.openFileOutput(fileName, Context.MODE_PRIVATE);
            out.write(data, 0, data.length);
        } catch (Exception e) {
            // 必要に応じて
//            throw e;
        } finally {
            try {
                if (out != null) out.close();
            } catch (Exception e) {
            }
        }
    }

    /**
     * ファイルから文字列を読み込む
     * @param context
     * @param fileName ファイル名
     * @return 文字列 ファイルがない場合はnull
     */
    public static String readFile(Context context, String fileName) {
        String str = null;
        byte[] data = readBinaryFile(context, fileName);
        if (data != null) {
            str = new String(data);
        }
        return str;
    }

    /**
     * ファイルからバイナリデータを読み込む
     * @param context
     * @param fileName
     * @return バイトデータ ファイルがない場合はnull
     */
    public static byte[] readBinaryFile(Context context, String fileName) {
        // ファイルの存在チェック
        if (!(new File(context.getFilesDir().getPath() + "/" + fileName).exists())) {
            return null;
        }

        int size;
        byte[] data = new byte[1024];
        InputStream in = null;
        ByteArrayOutputStream out = null;

        try {
            in = context.openFileInput(fileName);
            out = new ByteArrayOutputStream();
            while ((size = in.read(data)) != -1) {
                out.write(data, 0, size);
            }
            return out.toByteArray();
        } catch (Exception e) {
            // エラーの場合もnullを返すのでここでは何もしない
        } finally {
            try {
                if (in != null) in.close();
                if (out != null) out.close();
            } catch (Exception e) {
            }
        }

        return null;
    }
}

2011年11月20日日曜日

いつからかEclipse上でAndroidプロジェクトのMavenビルドが出来なくなったので…

久々にMavenを使用してAndroidアプリを作成しようと思ったらなぜかビルドが出来ない。。
ADTやらSDKを更新したタイミングで使えなくなったのだろうと思い、いろいろ思考錯誤してみた。
そしてようやくビルドが出来る環境を作成する手順が確立出来たので忘れずメモ。

ちなみに私の環境は以下の通り。
Mac OS X 10.6.8
ADT 15 & SDK15
Maven 3.0.3
Eclipse Indigo

m2eのインストール

[Help] -> [Install New Software...]
プルダウンからIndigoのアップデートサイトを選択しCollaborationを展開する。
「Maven Integration for Eclipse」と「slf4j over logback loggind」にチェックし、-> [Next]
あとは指示通り進んでインストール完了後Eclipseを再起動。

m2e-androidのインストール

[Help] -> [Eclipse Marketplace...]
「m2e-android」と検索すると「Android Configuration for M2E」がヒットするので
Installボタンをクリックし指示通り進んでインストール完了後Eclipseを再起動。

Archetypeの追加

[New] -> [Project...] -> [Maven Project]
Workspace locationに任意の場所を指定 -> [Next] -> [Add Archetype...]
下記のような画面になるので各項目を入力 -> [OK]
Archetype Group Id: de.akquinet.android.archetypes
Archetype Artifact Id : android-quickstart
Archetype Version: 1.0.6
Repository URL: 空白
上記で追加したandroid-quickstartを選択しAndroidプロジェクトを作成する。

m2e connectorのインストール

Androidプロジェクトを作成すると、下記画面のようにpom.xmlにエラーマークがつくので
POMエディターで開く。
pom.xmlを開くと上記に赤くメッセージが表示されるのでその箇所をクリック。
下記のような黄色いウィンドウが表示されるので「Discover new m2e connectors」をクリック。
m2e Marketplaceなるものが開くので「Application」と「Maven」にチェックをし、
「Lifecycle Mappings」と「embedded maven runtimes」をインストールしEclipseを再起動。

pom.xmlの修正

<artifactId>maven-android-plugin</artifactId>
<version>2.8.4</version>
↓
<artifactId>android-maven-plugin</artifactId>
<version>3.0.0-alpha-14</version>
以上でEclipse上でAndroidプロジェクトのMavenビルドが出来るようになるはず。

ちなみにpom.xmlの全体はこんな感じ。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.u1aryz.products.hoge</groupId>
  <artifactId>HogeSample</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>apk</packaging>
  <name>HogeSample</name>

  <dependencies>
    <dependency>
      <groupId>com.google.android</groupId>
      <artifactId>android</artifactId>
      <version>2.1.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>${project.artifactId}</finalName>
    <sourceDirectory>src/main/java</sourceDirectory>
    <plugins>
      <plugin>
        <groupId>com.jayway.maven.plugins.android.generation2</groupId>
        <artifactId>android-maven-plugin</artifactId>
        <version>3.0.0-alpha-14</version>
        <configuration>
          <sdk>
            <!-- SDKのパスはsettings.xmlで定義していれば不要 -->
            <path>"your ANDROID_HOME"</path>
            <platform>7</platform>
          </sdk>
          <manifest>
            <debuggable>true</debuggable>
          </manifest>
        </configuration>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

2011年11月18日金曜日

RoboGuiceでAndroidアプリのDI開発

今更ながらRoboGuiceを使ってみたので忘れずメモ。
RoboGuiceについて簡単に触れておくとGoogle GuiceベースのAndroid用のDIコンテナである。
roboguice - Project Hosting on Google Code

細かいViewの制御は抜きにして以下のようなアプリを例としてRoboGuiceを使用して作成してみる。
  • トップ画面より入力された名前を次の画面で文字列を付け足して表示する。




  • まず前エントリーを参考にMavenプロジェクトからAndroidプロジェクトを作成。
    pom.xmlを追記
    <dependency>
      <groupId>org.roboguice</groupId>
      <artifactId>roboguice</artifactId>
      <version>1.1.2</version>
    </dependency>
    

    TopActivity
    public class TopActivity extends RoboActivity {
    
        @InjectResource(R.string.message)
        String message;
    
        @InjectView(R.id.txt_msg)
        TextView mTextView;
    
        @InjectView(R.id.edt_name)
        EditText mEditText;
    
        @InjectView(R.id.btn_decision)
        Button mButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
    //        mTextView.setText(message); // ここでViewにアクセスするとエラー
            setContentView(R.layout.top); // ここでViewがInjectされる
            mTextView.setText(message);
            mButton.setOnClickListener(new OnClickListener() {
    
                public void onClick(View v) {
                    Intent intent = new Intent(TopActivity.this, HelloActivity.class);
                    intent.putExtra("name", mEditText.getText().toString());
                    startActivity(intent);
                }
            });
        }
    
    }
    
    
    RoboActivityを継承してアクティビティを作成する。※1.1からGuiceActivityからRoboActivityに変更された
    ListActivityの場合はRoboListActivity、TabActivityの場合はRoboTabActivityなどそれぞれ対応したもの
    が存在する。
    ソースを見るとわかる通り、@InjectView(resouceId)でViewをInjectしてくれる。
    リソースを参照したい場合は@InjectResource(resourceId)を使用すればいい。
    他にもこんなことが可能である。
    Drawable icon = getResources().getDrawable(R.drawable.icon);
    ↓
    @InjectResource(R.drawable.icon)
    Drawable icon;
    
    LocationManager loc = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);
    ↓
    @Inject
    LocationManager loc;
    

    HelloActivityを実装する前にインターフェースと実装クラスを使用して
    少しDIっぽい処理を入れてみる。
    RoboSampleService
    public interface RoboSampleService {
    
        public String hello(String name);
    }
    
    

    RoboSampleServiceImpl
    public class RoboSampleServiceImpl implements RoboSampleService {
    
        public String hello(String name) {
            return "Hello " + name;
        }
    
    }
    
    

    続いて上記サービスをバインド
    MyApplication
    public class MyApplication extends RoboApplication {
    
        @Override
        protected void addApplicationModules(List<Module> modules) {
            modules.add(new MyModule());
        }
    
        static class MyModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してRoboSampleServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(RoboSampleServiceImpl.class);
            }
    
        }
    }
    
    ※こちらもGuiceApplicationからRoboApplicationに変更されているので注意

    アプリケーションのエントリポイントとして上記クラスを呼び出す必要があるので
    AndroidManifest.xmlに以下を追加
    <application...
        android:name=".MyApplication"
        ...>
    

    最後にHelloActivityの実装
    HelloActivity
    public class HelloActivity extends RoboActivity {
    
        @InjectExtra("name")
        String name;
    
        @InjectView(R.id.hello_msg)
        TextView textView;
    
        @Inject
        RoboSampleService service;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.hello);
            textView.setText(service.hello(name));
        }
    
    }
    

    @InjectExtra("name")でTopActivityでputExtraした値が入る。
    また、RoboSampleServiceに@Injectアノテーションをつけることで上記でバインドした
    RoboSampleServiceImplをInjectしてくれるのでインスタンスを生成せず使用することが出来る。

    続いてDIの長所を活かしてHelloActivityのテストを書いてみる。
    テストプロジェクトを作成する前にテスト対象のプロジェクトのクラスパスをエクスポート。

    通常通りAndroidテストプロジェクトを作成する。
    今回作成したサンプルプログラムのhelloメソッドは静的な文字列を付加するだけの
    シンプルな作りであるが、実際は動的であったり、バグが混入していたり、
    決まっていなかったりするので依存するクラスの振る舞いを固定化するモックを作成する。

    MockServiceImpl
    public class MockServiceImpl implements RoboSampleService {
    
        @Override
        public String hello(String name) {
            return "test";
        }
    
    }
    

    続いてテストコードの実装
    HelloActivityTest
    public class HelloActivityTest extends RoboActivityUnitTestCase<HelloActivity> {
    
        private Context mContext;
    
        public HelloActivityTest() {
            super(HelloActivity.class);
        }
    
        class MockApplication extends RoboApplication {
    
            public MockApplication(Context context) {
                super();
                attachBaseContext(context);
            }
    
            @Override
            protected void addApplicationModules(List<Module> modules) {
                modules.add(new MockModule());
            }
    
        }
    
        class MockModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してMockServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(MockServiceImpl.class);
            }
    
        }
    
        @Override
        protected void setUp() throws Exception {
            super.setUp();
            mContext = getInstrumentation().getContext();
            setApplication(new MockApplication(mContext));
        }
    
        @MediumTest
        public void testShouldBeHelloMsg() {
            Intent intent = new Intent(mContext, HelloActivity.class);
            intent.putExtra("name", "name");
            HelloActivity a = startActivity(intent, null, null);
            assertNotNull(a);
    
            // Mockで返す文字列を"test"にしているため
            assertEquals(((TextView)a.findViewById(R.id.hello_msg)).getText(), "test");
        }
    }
    

    RoboActivityUnitTestCase<対象アクティビティ>を継承してテストコードを作成する。
    RoboSampleServiceのInject要求に対してモックを返すようにバインドさせ、
    setApplicationでセットすることで依存したオブジェクトの振る舞いを固定化させることが出来る。

    Run As -> Android JUnit Testで...
    これで一通り完了。案外使いやすいかも?

    2011年9月25日日曜日

    Androidでオブジェクト/XMLマッピング

    TwitterクライアントなどXMLやJSONをWeb上から取得してアプリに表示することは少なくない。
    しかし、XMLやJSONをパースするような単調な処理は出来るだけ無くしたいので、
    Spring AndroidとSimple(xmlシリアルフレームワーク)を使用してXMLデータを取得し、
    アプリに表示させてみた。
    今回はイベント開催支援ツールのATNDのAPIを使用してAndroid系イベントを取得して
    イベントタイトルとイベント日時をアプリにリスト表示してみる。
    実際に使用するAPI→http://api.atnd.org/events/?keyword=android&format=xml
    サンプルプロジェクトのダウンロードリンクは記事の最後。

    まず前エントリーを参考にMavenプロジェクトからAndroidプロジェクトを作成。

    pom.xmlを追記
    <dependency>
      <groupId>org.simpleframework</groupId>
      <artifactId>simple-xml</artifactId>
      <version>2.6.1</version>
      <exclusions>
        <exclusion>
          <artifactId>stax</artifactId>
          <groupId>stax</groupId>
          </exclusion>
          <exclusion>
            <artifactId>stax-api</artifactId>
            <groupId>stax</groupId>
          </exclusion>
          <exclusion>
            <artifactId>xpp3</artifactId>
            <groupId>xpp3</groupId>
          </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.android</groupId>
      <artifactId>spring-android-rest-template</artifactId>
      <version>1.0.0.M4</version>
    </dependency>
    
    <repositories>
      <repository>
        <id>org.springframework.maven.milestone</id>
        <name>Spring Maven Milestone Repository</name>
        <url>http://maven.springframework.org/milestone</url>
        <snapshots><enabled>false</enabled></snapshots>
      </repository>
    </repositories>
    

    次にXMLデータをマッピングさせるオブジェクトを作成する。
    要素には@Element、属性には@Attributeをつける。
    @Element、@Attributeのnameは要素名(属性名)と変数名が同じであれば省略可能。
    XMLのすべての要素をオブジェクトにマッピングしない場合は@Rootアノテーションにstrict = false
    をつける必要がある。
    Event.java
    package jp.u1aryz.products.atndsearch;
    
    import org.simpleframework.xml.Element;
    import org.simpleframework.xml.Root;
    
    @Root(name = "event", strict = false)
    public class Event {
    
        @Element
        private String title;
    
        @Element(name = "started_at")
        private String eventDate;
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setEventDate(String eventDate) {
            this.eventDate = eventDate;
        }
    
        public String getEventDate() {
            return eventDate;
        }
    }
    

    リストの場合は@ElementListアノテーションを使う。
    EventList.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.List;
    
    import org.simpleframework.xml.ElementList;
    import org.simpleframework.xml.Root;
    
    @Root(name = "events", strict = false)
    public class EventList {
    
        @ElementList
        private List<Event> events;
    
        public void setEvents(List<Event> events) {
            this.events = events;
        }
    
        public List<Event> getEvents() {
            return events;
        }
    }
    

    そしてXMLデータからオブジェクトにマッピングさせる処理を作成する。
    HTTP通信は通常、UIスレッドとは別のスレッドで行うため、今回のポイントとなる処理も
    doInBackground内に入れる。
    GetAndroidEventActivity.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.client.RestTemplate;
    
    import android.app.ListActivity;
    import android.app.ProgressDialog;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.util.Log;
    
    public class GetAndroidEventActivity extends ListActivity {
    
        private static String TAG = GetAndroidEventActivity.class.getSimpleName();
        private ProgressDialog progressDialog;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            // ATND APIの呼び出しを非同期で行う
            new GetEventTask().execute();
        }
    
        private void refreshEvents(List<Event> events) {
            if (events != null) {
                EventListAdapter adapter = new EventListAdapter(this, events);
                setListAdapter(adapter);
            }
        }
    
        public void showProgressDialog() {
            if (progressDialog == null) {
                progressDialog = new ProgressDialog(this);
                progressDialog.setIndeterminate(true);
            }
    
            progressDialog.setMessage("Loading. Please wait...");
            progressDialog.show();
        }
    
        public void dismissProgressDialog() {
            if (progressDialog != null) {
                progressDialog.dismiss();
            }
        }
    
        private class GetEventTask extends AsyncTask<Void, Void, List<Event>> {
    
            @Override
            protected void onPreExecute() {
                // プログレスバーを表示
                showProgressDialog();
            }
    
            @Override
            protected List<Event> doInBackground(Void... params) {
                try {
                    // Android関連のイベントを検索するURL
                    String url =
                        "http://api.atnd.org/events/?keyword=android&format=xml";
                    // Acceptヘッダに"application/xml"をセット
                    HttpHeaders requestHeaders = new HttpHeaders();
                    List<MediaType> acceptableMediaTypes =
                        new ArrayList<MediaType>();
                    acceptableMediaTypes.add(MediaType.APPLICATION_XML);
                    requestHeaders.setAccept(acceptableMediaTypes);
    
                    HttpEntity<?> requestEntity =
                        new HttpEntity<Object>(requestHeaders);
                    RestTemplate restTemplate = new RestTemplate();
    
                    // HTTPのGETリクエストを実行(マッピングさせるクラスを指定する)
                    ResponseEntity<EventList> responseEntity =
                        restTemplate.exchange(url,
                                            HttpMethod.GET,
                                            requestEntity,
                                            EventList.class);
    
                    // イベントリストを取得
                    EventList eventList = responseEntity.getBody();
    
                    return eventList.getEvents();
                } catch (Exception e) {
                    Log.e(TAG, e.getMessage(), e);
                }
    
                return null;
            }
    
            @Override
            protected void onPostExecute(List<Event> result) {
                // プログレスバーを非表示
                dismissProgressDialog();
    
                // 結果を返す
                refreshEvents(result);
            }
        }
    }
    

    最後にViewに取得したデータをセットするAdapterの作成。
    特別な処理はしていないので、通常のAndroidの知識で理解出来ると思う。
    EventListAdapter.java
    package jp.u1aryz.products.atndsearch;
    
    import java.util.List;
    
    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 EventListAdapter extends ArrayAdapter<Event> {
    
        private LayoutInflater mInflater;
    
        public EventListAdapter(Context context, List<Event> objects) {
            super(context, 0, objects);
            mInflater = LayoutInflater.from(context);
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
    
            if (convertView == null) {
                convertView =
                    mInflater.inflate(android.R.layout.simple_list_item_2, null);
                holder = new ViewHolder();
                holder.title =
                    (TextView) convertView.findViewById(android.R.id.text1);
                holder.eventDate =
                    (TextView) convertView.findViewById(android.R.id.text2);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
    
            Event event = getItem(position);
            holder.title.setText(event.getTitle());
            holder.eventDate.setText(event.getEventDate());
    
            return convertView;
        }
    
    }
    class ViewHolder {
        TextView title;
        TextView eventDate;
    }
    

    <uses-permission android:name="android.permission.INTERNET" />を忘れなければ
    下記のようにイベントを取得出来る。
    日時のフォーマットが見づらいけど。。。
    これで単調なパース処理とおさらば〜
    時間があればJSONバージョンもやるかも!?

    ダウンロードはこちらから

    2011年9月23日金曜日

    MavenでAndroidアプリケーションの構成管理をはじめる手順

    最近ではサードパーティー製のAndroidフレームワークやライブラリが増加してきており、
    構成管理にMavenを使用すると便利である。
    Maven初心者ながらAndroidプロジェクトの構成管理を始めてみたのでその時の手順をメモ。
    2011年11月20日に更新しました。

    <事前環境>
    Apache Maven 3.0.3
    Eclipse 3.7 + ADT v12
    ※環境のインストール手順については割愛
    1. M2_REPOの設定
      EclipseからMavenを利用するために下記コマンドを実行
      mvn -Declipse.workspace=<path-to-eclipse-workspace> eclipse:add-maven-repo
    2. Eclipseプラグインのインストール
      Update siteから下記の2つのプラグインを追加
      ( [Help] -> [Install New Software...] -> [Add...] )
      • Maven Integration for Eclipse
        http://m2eclipse.sonatype.org/sites/m2e
      • Maven Integration for Android Development Tools
        https://svn.codespot.com/a/eclipselabs.org/m2eclipse-android-integration/updates/m2eclipse-android-integration/
    3. Archetypeの追加
      現在ではGitHub等に多数Archetypeが公開されているので、今回はこちらを利用する。
      [New] -> [Project...] -> [Maven Project]
      Workspace locationに任意の場所を指定 -> [Next] -> [Add Archetype...]
      下記のような画面になるので各項目を入力 -> [OK]


      Archetype Group Id: de.akquinet.android.archetypes
      Archetype Artifact Id : android-quickstart
      Archetype Version: 1.0.5
      Repository URL: 空白
    4. Androidプロジェクトの作成
      上記で追加したandroid-quickstartを選択 -> [Next]
      下記のような画面になったら各項目に任意の値を入力 -> [Finish]

    以上でAndroidアプリもMavenで構成管理が出来るようになるはず。

    Androidへのデプロイはプロジェクト配下で下記コマンドを実行する。
    mvn clean install android:deploy

    2011年7月14日木曜日

    AndroidのXMLで2次元配列を定義してソースから呼び出す

    Androidでは文字列などのリソースをXMLで定義すると何かと便利なので、
    2次元配列をXMLで定義してソースからアクセスする方法を忘れずにメモ。

    XML
    <array name="array_parent">
        <item >@array/array_sub1</item>
        <item >@array/array_sub2</item>
    </array>
    
    <string-array name="array_sub1" >
        <item >data1</item>
        <item >data2</item>
        <item >data3</item>
    </string-array>
    
    <string-array name="array_sub2" >
        <item >data4</item>
        <item >data5</item>
        <item >data6</item>
    </string-array>
    

    Java
    TypedArray typedArray = getResources().obtainTypedArray(R.array.array_parent);
    // 配列の要素数(ここではarray_parentの子要素の数)
    int length = typedArray.length();
    
    // 子要素の配列のリソースIDを取得(ここではarray_sub1のリソースID)
    int resourceId = typedArray.getResourceId(0, 0);
    
    // 配列の値を取得(ここではarray_sub1の各値)
    String[] array = getResources().getStringArray(resourceId);
    


    これに多少の応用を利かせればDrawable配列などの多次元配列も可能となる。

    2011年5月29日日曜日

    Androidでスクリーンロック解除しなくても操作可能な画面をつくる&呼び出す


    音楽アプリなどで音楽再生中にスクリーンロックがかかった場合、
    再度音楽を操作するのにいちいちスクリーンロックを解除するのはかなりの手間である。

    Androidマーケットで常に上位のPowerAMPはこのように操作可能になっている。
    このようなアプリではユーザビリティを考慮する上でも必要な機能となる。

    そこで今回はスクリーンロック解除しなくても操作可能な画面&呼び出しを
    ミニマム構成で作成してみた。
    サンプルプロジェクトのダウンロードリンクは記事の最後。

    構成は以下の通り。
    1. 画面の電源ONの通知を受け取るレシーバーの登録/登録解除を行うサービス
      ScreenStateService
    2. スクリーンロック解除画面より手前に表示させる画面
      ScreenLockEnabledActivity
    3. "2"の画面を表示させるか否かを設定する画面
      SetActivity
    まず、"画面の電源ONの通知を受け取るレシーバーの登録/登録解除を行うサービス"を作成する。
    画面の電源が入るとACTION_SCREEN_ONが通知される。
    ACTION_SCREEN_ONを受け取るには明示的にregisterReceiverする必要がある。
    ScreenStateService.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Service;
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.IBinder;
    
    public class ScreenStateService extends Service {
    
        private BroadcastReceiver mScreenOnListener = new BroadcastReceiver() {
    
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
    
                // 画面の電源が入ったらActivityを起動
                if (action.equals(Intent.ACTION_SCREEN_ON)) {
                    Intent i = new Intent(context, ScreenLockEnabledActivity.class);
                    i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(i);
                }
            }
        };
    
        @Override
        public void onStart(Intent intent, int startId) {
            super.onStart(intent, startId);
            // ACTION_SCREEN_ONを受け取るBroadcastReceiverを登録
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_ON);
            registerReceiver(mScreenOnListener, filter);
        }
    
        @Override
        public void onDestroy() {
            // BroadcastReceiverを登録解除
            unregisterReceiver(mScreenOnListener);
    
            super.onDestroy();
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
    
    サービスが起動されたらレシーバーの登録、サービスが停止されたらレシーバーの登録解除を行う。
    かなり簡略化してシンプルな作りにしている。

    そして今回のポイントとなる"Lock解除画面より手前に表示させる画面"を作成する。
    ScreenLockEnabledActivity.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.Window;
    import android.view.WindowManager;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    
    public class ScreenLockEnabledActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.lock);
    
            // Lock解除画面より手前に表示させる
            final Window win = getWindow();
            win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
    
            Button btnRelease = (Button) findViewById(R.id.btn_release);
            btnRelease.setOnClickListener(new OnClickListener() {
    
                @Override
                public void onClick(View v) {
                    // Activityを終了することでLock解除画面に移る
                    finish();
                }
            });
        }
    }
    
    こちらもかなりミニマムであるが、このActivityに各々操作できるウィジェット(View)等を
    配置するといいと思う。

    続いて"上記の画面を表示させるか否かを設定する画面"を作成する。
    通常はPreferenceActivityなどで実装すると良い。
    SetActivity.java
    package jp.u1aryz.products.screenlockenable;
    
    import android.app.Activity;
    import android.content.Intent;
    import android.content.SharedPreferences;
    import android.os.Bundle;
    import android.widget.CheckBox;
    import android.widget.CompoundButton;
    import android.widget.CompoundButton.OnCheckedChangeListener;
    
    public class SetActivity extends Activity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            // チェックボックスの値を保存するために使用
            final SharedPreferences pref = getSharedPreferences("pref", MODE_PRIVATE);
            final Intent intent = new Intent(SetActivity.this, ScreenStateService.class);
    
            CheckBox chbEnable = (CheckBox) findViewById(R.id.chb_enable);
            chbEnable.setChecked(pref.getBoolean("is_lockEnable", false));
            chbEnable.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    // チェックされたらサービスを起動
                    if (isChecked) {
                        pref.edit().putBoolean("is_lockEnable", isChecked).commit();
                        startService(intent);
                    // チェックが外されたらサービスを停止
                    } else {
                        pref.edit().putBoolean("is_lockEnable", isChecked).commit();
                        stopService(intent);
                    }
                }
            });
    
        }
    }
    
    端末の再起動のパターンやその他もろもろ対応しきれてないけどあしからず〜

    ダウンロードはこちらから

    2011年3月27日日曜日

    ListViewにグループタイトルをつける


    上記のようにアルファベットなどでグルーピングされたリストに
    グループタイトルをつける方法を考えてみた。
    (PreferenceCategoryのタイトルのようなものです)

    まず、ListViewの1行分のレイアウトを用意する。
    その際、グループタイトル+通常要素(アルバム)となるようにする。
    上記のようなアルバム一覧の場合、下記のようなレイアウトを用意する。


    具体的なレイアウトのXMLは下記のようになる。
    list_item.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >
        <!-- グループタイトル -->
        <TextView
            android:id="@+id/groupTitle"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:background="#280030"
            android:textColor="#c62b00"
            android:textStyle="bold"
            android:paddingLeft="5dp"
            />
        <!-- アルバム名 -->
        <TextView
            android:id="@+id/albumName"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="24sp"
            android:paddingLeft="5dp"
            />
        <!-- アーティスト名 -->
        <TextView
            android:id="@+id/artistName"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="5dp"
            />
    </LinearLayout>
    

    続いてArrayAdapterを継承したクラスを実装する。
    MyAdapter.java
    package jp.u1aryz.products.grouptitle;
    
    import java.util.List;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.TextView;
    
    class BindData {
        String groupTitle;
        String albumName;
        String artistName;
    
        public BindData(String groupTitle, String albumName, String artistName) {
            this.groupTitle = groupTitle;
            this.albumName = albumName;
            this.artistName = artistName;
        }
    }
    
    class ViewHolder {
        TextView groupTitle;
        TextView albumName;
        TextView artistName;
    }
    
    public class MyAdapter extends ArrayAdapter<BindData> {
    
        private LayoutInflater inflater;
    
        public MyAdapter(Context context, List<BindData> objects) {
            super(context, 0, objects);
            this.inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
    
        @Override
        public boolean isEnabled(int position) {
            // BindDataのgroupTitleが!nullの場合、選択不可にする
            return getItem(position).groupTitle == null;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
    
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.list_item, parent, false);
                holder = new ViewHolder();
                holder.groupTitle = (TextView) convertView.findViewById(R.id.groupTitle);
                holder.albumName = (TextView) convertView.findViewById(R.id.albumName);
                holder.artistName = (TextView) convertView.findViewById(R.id.artistName);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
    
            BindData data = getItem(position);
            // グループタイトル
            if (!isEnabled(position)) {
                holder.groupTitle.setVisibility(View.VISIBLE);
                holder.groupTitle.setText(data.groupTitle);
                holder.albumName.setVisibility(View.GONE);
                holder.artistName.setVisibility(View.GONE);
            // アルバム
            } else {
                holder.groupTitle.setVisibility(View.GONE);
                holder.albumName.setVisibility(View.VISIBLE);
                holder.albumName.setText(data.albumName);
                holder.artistName.setVisibility(View.VISIBLE);
                holder.artistName.setText(data.artistName);
            }
            return convertView;
        }
    }
    

    ここでは選択不可の項目を設定する際、BindDataのgroupTileが!nullだった場合と
    ルール決めをしている。
    ポイントは63行目〜75行目になり、View#setVisibility(int visibility)を使用し、
    各項目を表示、非表示と切り替える。

    最後にActivityから以下のようにしてadapterを設定する。
    package jp.u1aryz.products.grouptitle;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import android.app.ListActivity;
    import android.os.Bundle;
    
    public class MyActivity extends ListActivity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            setTitle("アルバム一覧");
    
            List<BindData> list = new ArrayList<BindData>();
            // ここにlistに項目を追加する処理が入る
    
            MyAdapter adapter = new MyAdapter(this, list);
            setListAdapter(adapter);
        }
    }
    

    正直もっといい方法がありそうなので、より良い方法が見つかったら更新するかも。。。

    2011年3月6日日曜日

    ListViewのスクロール位置のあれこれ


    AndroidMarketアプリやYouTubeアプリ、Gmailアプリなどでは
    ListViewを最後までスクロールすると自動的に次の数件を取得し表示される。

    android.widget.AbsListView.OnScrollListener#onScrollを利用すると
    表示されている先頭のインデックス(firstVisibleItem)、
    表示されているリストの数(visibleItemCount)、リストのトータル数(totalItemCount)
    が引数として渡ってくるので、firstVisibleItem + visibleItemCount = totalItemCount
    になる時、最後までスクロールされたと判定出来る。


    上図で赤枠部分が画面に表示されている部分になる。
    最後までスクロールしている右図でfirstVisibleItem + visibleItemCount = totalItemCount
    になっていることがわかる。

    下記がソースの一部。
    ListView listView = (ListView) findViewById(android.R.id.list);
    listView.setOnScrollListener(new OnScrollListener() {
    
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            // 最後までスクロールされたかどうかの判定
            if (totalItemCount == firstVisibleItem + visibleItemCount) {
                // ここに次の数件を取得して表示する処理を書けばいい
            }
        }
    });
    

    リストの追加などでListViewを更新する際、スクロール位置が毎回戻ってしまっては
    ユーザービリティの悪いものになってしまう。
    ListViewのスクロール位置の取得と設定は下記のようにして実現出来る。

    取得
    int position = listView.getFirstVisiblePosition();
    int y = listView.getChildAt(0).getTop();
    

    設定
    listView.setSelectionFromTop(position, y);
    

    インターネット上からデータを取得する際に使用する頻度が多いかも。