‘Android’ カテゴリーのアーカイブ

ISW11FをADBドライバで認識させる方法

Eclipseで開発をするときに、Arrows Z(ISW11F)がPCから認識しなくて困っていました。

PCからはFujitsu Mass storage USB Deviceと認識されてしまう場合の対処法です。

まずはドライバダウンロードします。
http://spf.fmworld.net/oss/usb_driver/isw11f/index.html

解凍したらReadme.txtは必ず読んでください。

インストール前に確認するべき項目は次の通り。

  • C:\Users\(ユーザー名)\.android\adb_usb.iniがあるか
  • 上記adb_usb.iniに「0x04C5」という行があるか(なければ行追加)
  • 端末がUSBデバッグになっているか

それとPCでデバイスマネージャーを起動しておきましょう。

これでUSBで端末を接続して、問題なくFujitsu HSUSB Deviceという項目が出現すればいいのですが、出現せず、下の画像のようになる場合があります。

WS000015

基本的には、すでに誤認識されてしまっているデバイスを削除してやればいいのですが、Fujitsu Mass storage USB Deviceの2行を削除しても、再接続すると元に戻ってしまいました。

なので、このデバイスの親ごと消す必要があります。作業は端末を接続したままで行います。

「ユニバーサル シリアル バス コントローラー」の下の「USB 大容量記憶装置」を右クリックして、削除します。ただし、複数デバイスがある場合は以下のように、デバイスマネージャーの表示を、「デバイス(接続別)」にしてやると、目的のデバイスがグループ化されるので、間違えずに消せるようになります。

WS000011

消したあとは、一度端末を外して、再度USBで接続すると、自動的にドライバのインストールが始まります。

WS000013

こうなれば成功です。ドライバーが見つかりませんと表示されますが、デバイス自体は認識されています。

WS000014

あとは、このFujitsu HSUSB Deviceを選択して、ドライバの更新をして、先ほどダウンロードして解凍したドライバファイルを選択すればAndroid Composite ADB Interfaceとして認識されます。

WS000016

ちなみにMass storage USB Deviceを直接「ドライバーの更新」すれば良いという情報があったのですが、試そうとしたら64bitドライバがない旨のメッセージが出て、進めませんでした。

今回はこちらのサイトが参考になりました。

AndroidがUSBデバッグで接続できない場合(USBストレージとしてしか認識されない場合) | コラビットの中の人


MediaPlayerで通知音を鳴らす

AndroidのMediaPlayerで通知音を鳴らすときに、音量設定を反映させます。

MediaPlayer.createメソッドでインスタンスを作成した状態だと、「メディア音量」というものが反映されるので、setAudioStreamTypeメソッドで再生するオーディオのタイプを指定してやります。これでシステムの設定画面で指定した音量が反映されます。

が、setAudioStreamTypeメソッドをMediaPlayer.createで作成したインスタンスに対して実行すると、
error (-38, 0)
とか
prepareAsync called in state 8
とか
setAudioStream called in state 8
とかエラーが起こります。

これはMediaPlayer.createメソッドが、内部的にすでにprepare(準備)メソッドを呼んでいるためで、ストリームタイプの変更は、準備の前に実行しておく必要があります。

コードはこんな感じになりました。

    public static void playSound(Context context, String url) {

        if (url != null) {
            Uri uri = Uri.parse(url);
            // MediaPlayer.createはprepareを実行してしまうのでnew MediaPlayer()を使う
            // MediaPlayer mp = MediaPlayer.create(context, uri);
            MediaPlayer mp = new MediaPlayer(); 
            try {
                mp.setDataSource(context, uri);
                // setAudioStreamTypeはprepare前に実行する必要がある
                mp.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); 
                mp.setLooping(false); 
                // prepareの前後で使えるメソッドが異なる
                mp.prepare(); 
                mp.seekTo(0);
                mp.start(); 
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

ちなみにマナーモードのときは、何も状態をチェックしなくてもストリームタイプを通知音にしておけば、音は鳴りません。MediaPlayer.createで作ると、メディアになってしまうので、マナーモードが反映されません。

それと、一部機種(音が長いと?)ではsetDataSourceが失敗する場合があるそうです。詳しくは以下。

Ringtoneを再生する時にFileDescriptorを使いたい – 日本Androidの会 | Google グループ


Androidのキーストアから証明書をエクスポートする

業務でAndroid開発をしていて、都合上、後から鍵ペアを人に渡さなければいけない場合があります。

新しいapkのたびにキーストアファイルを作っているのなら、別にそれごと渡してしまえばいいのですが、自分用のキーストアに作ってしまった場合、キーストアごと渡すわけにはいきません。

そこでキーストアファイルから証明書をエクスポートして相手に渡すことを考えますが、apkに署名するには、証明書の他に秘密鍵が必要です。

先に結論だけ書いておきます。keytoolコマンドのimportkeystoreを使います。

> keytool -importkeystore -srckeystore .\mykeystore.jks -srcstorepass <<自分のキーストアのパスワード>> -destkeystore app1.jks -deststorepass <<新規作成するキーストアのパスワード>> -deststoretype jks -srcalias app1
<app1> の鍵パスワードを入力してください。

これでapp1.jksというキーストアファイルが新規作成され、自分のキーストアの中から特定のエイリアスの鍵ペアだけをコピーできます。

以下は説明です。

例えば自分用のキーストアの中身がこうなっていたとします。

> keytool -list -v -keystore .\mykeystore.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 2 エントリが含まれます。

別名: app1
作成日: 2012/03/08
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3


*******************************************
*******************************************


別名: app2
作成日: 2012/03/08
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f57f300
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3

証明書のエクスポートとインポートは以下のコマンドでできますが、秘密鍵がエクスポートできません。

> keytool -exportcert -keystore .\mykeystore.jks -storepass <<キーストアのパスワード>> -alias app1 -file .\app1.cer
証明書がファイル <.\app1.cer> に保存されました。

> keytool -import -keystore app1.jks -storepass <<新規作成するキーストアのパスワード>> -file .\app1.cer
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3
この証明書を信頼しますか? [no]:  yes
証明書がキーストアに追加されました。

この状態のキーストアはこうなっています。

> keytool -v -list -keystore .\app1.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 1 エントリが含まれます。

別名: app1
作成日: 2012/03/31
エントリのタイプ: trustedCertEntry

所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3


*******************************************
*******************************************

エントリのタイプがtrustedCertEntryになっています。このエントリでapkを署名しようとするとjarsignerがエラーを出します。

jarsigner: 次の証明連鎖が見つかりません: app1。  app1 は、非公開鍵および対応する公開鍵証明連鎖を含む有効な KeyStore 鍵エントリを参照する必要があります。
(英語の場合)
jarsigner: Certificate chain not found for: app1.  app1 must reference a valid KeyStore key entry containing a private key and corresponding public key certificate chain.

Eclipseの場合は、何も言われず、Finishボタンが押せません。

ここで最初の結論のところで書いたimportkeystoreを使って、キーストアからキーストアへのインポートをすることで、秘密鍵を持ったまま新しいキーストアに証明書をコピーできます。コピー後のキーストアファイルはこうなります。

> keytool -list -v -keystore .\app1.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 1 エントリが含まれます。

別名: app1
作成日: 2012/04/01
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3

エントリタイプがPrivateKeyEntryになっていれば、通常通りapkの署名に使えます。

あと、importkeystoreが使えるのはJDK6以降のようなので、バージョンは確認してください。


Conversion to Dalvik format failed with error 1

[2012-03-05 08:32:06 - Test] Dx 
trouble processing "javax/net/SocketFactory.class":

Ill-advised or mistaken usage of a core class (java.* or javax.*)
when not building a core library.

This is often due to inadvertently including a core library file
in your application's project, when using an IDE (such as
Eclipse). If you are sure you're not intentionally defining a
core class, then this is the most likely explanation of what's
going on.

However, you might actually be trying to define a class in a core
namespace, the source of which you may have taken, for example,
from a non-Android virtual machine project. This will most
assuredly not work. At a minimum, it jeopardizes the
compatibility of your app with future versions of the platform.
It is also often of questionable legality.

If you really intend to build a core library -- which is only
appropriate as part of creating a full virtual machine
distribution, as opposed to compiling an application -- then use
the "--core-library" option to suppress this error message.

If you go ahead and use "--core-library" but are in fact
building an application, then be forewarned that your application
will still fail to build or run, at some point. Please be
prepared for angry customers who find, for example, that your
application ceases to function once they upgrade their operating
system. You will be to blame for this problem.

If you are legitimately using some code that happens to be in a
core package, then the easiest safe alternative you have is to
repackage that code. That is, move the classes in question into
your own package namespace. This means that they will never be in
conflict with core system classes. JarJar is a tool that may help
you in this endeavor. If you find that you cannot do this, then
that is an indication that the path you are on will ultimately
lead to pain, suffering, grief, and lamentation.

[2012-03-05 08:32:06 - Test] Dx 1 error; aborting
[2012-03-05 08:32:06 - Test] Conversion to Dalvik format failed with error 1

Package Explorerからプロジェクトを右クリックして、Properties選択。

Java Build Path→Librariesを開いて、表示されているライブラリを全部Remove。

OKでウィンドウを閉じて、再度Package Explorerを右クリックして、Android ToolsからFix Project Propertiesを選択。

解決。


SimpleDateFormatがメモリを食い尽くす件

java.text.SimpleDateFormatのインスタンスを毎回作ってRFC2822の日付文字列からDate型に変換していたら、GCされずにメモリを食いつぶしてしまったので対策。

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";
		SimpleDateFormat format = new SimpleDateFormat(pattern);
		Date date = null;
		synchronized (format) {
			try {
				date = (Date) format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}
		format = null;
		return date;
	}

上は元のコード。下は修正版。

	private static SimpleDateFormat format = null;

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";

		Date date = null;
		if (format == null) {
			format = new SimpleDateFormat();
		}
		synchronized (format) {
			format.applyPattern(pattern);
			try {
				date = format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}

		return date;
	}

完全なスレッドセーフにはなっていないけど、メモリリークは解消できたので良し。


loadImage時にcontains a path separatorのIllegalArgumentExceptionが発生する原因

Processing for Android 1.5.1でPApplet.loadImageを実行するときにエラー。

java.lang.IllegalArgumentException: File /mnt/sdcard/Android/data/com.sample.android.apptest/hoge.jpg contains a path separator

ファイルが存在しない場合に発生します。
エラーメッセージわかりにくいね。
loadImageメソッドは色々なソースからファイルが取得できるかを確認しに行くのでこういうことになっているのだと思います。


ProcessingをAndroidで使う(導入編)

ひと通りProcessingを利用してAndroidアプリを作ってみたので、色々書いておこうと思います。

まずは一番簡単なProcessingのIDEを使った方法です。

Download \ Processing.org

まずはProcessingをダウンロードします。執筆時点での最新安定バージョンは1.5.1です。Android SDKはインストールしておく必要があります。

実行すると、ProcessingのIDE(統合開発環境)が開きます。

WS000025

Processingについてはあまり説明しません。とりあえずはAndroidモードに切り替えます。右上の「STANDARD」と表示されているボタンを押すと、リストが表示されます。ただのラベルじゃないので、押せます。

WS000026

WS000027

Androidを選ぶと、IDEの色が変わります。右上のモードが「ANDROID」になっていることを確認します。

WS000028

メニューのFileからExamplesを開きます。

WS000003

ここで適当にサンプルを選んで、ダブルクリックで開きます。

ソースコードを読み込んだあとに、左上の再生ボタンを押せばエミュレータで動作を試せます。

WS000004

が、残念ながら、今はAndroid SDKのバージョンが上がったことで、エラーが出て実行できなくなっています。

BUILD FAILED
C:\Users\test\AppData\Local\Temp\android397981578064449760.pde\build.xml:16:

Error. You are using an obsolete build.xml
You need to delete it and regenerate it using
    android update project

Total time: 0 seconds

このままProcessing IDEで遊びたい場合は、まだ開発中のProcessing 2.0a4などを使えば上記の問題も解決しているようです。恐らく近日中にリリース版が出るかと。

tadpolizemedia.blog | processing1.5.1 と Android エミュレータ(android-sdk_r14-windows.zip)が連動しない件

詳しくは上記サイトにありましたので参考に。2.0a4でも、再生ボタンでそのまま実行はできませんでした。先にエミュレータを起動しておけば大丈夫です。

実機に転送する場合はメニューのSketchから、Run on Deviceを実行してください。

Processing IDEで開発するのは本質ではないので、この辺で。次回はProcessing IDEで書いたコードをEclipseのプロジェクト形式にエクスポートする方法を紹介します。


AndroidとTwitter4Jで公式画像アップロードAPIを使う

Twitter4Jが2.2.5になって、Twitter APIのstatuses/update_with_mediaが使えるようになったというので、さっそく実験してみました。

Twitter4J 2.2.5 released – Twitter4J | Google グループ

とりあえずダウンロードして解凍します。twitter4j-android-2.2.5.zipの方です。

http://twitter4j.org/en/index.html#download

Eclipseで適当にプロジェクト作ります。プロジェクト作ったらプロジェクトのプロパティを開いて、Java Build Pathの設定画面を表示します。

image

Librariesタブを開いて、Add External JARsをクリックします。

image

先ほど展開したフォルダから、libの下にあるtwitter4j-core-android-2.2.5.jarとtwitter4j-media-support-android-2.2.5.jarを選択してJARを取り込みます。

image

あとはこんな感じで実験。ギャラリーの画像を選択してメッセージつけてポストします。

UpdateWithMediaActivity.java

package info.loadlimits.android.updatewithmedia;

import java.io.File;

import twitter4j.TwitterException;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;
import twitter4j.media.ImageUpload;
import twitter4j.media.ImageUploadFactory;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class UpdateWithMediaActivity extends Activity implements
		OnClickListener {
	private final static int REQUEST_PICK = 1;
	private final static String CONSUMER_KEY = "(書き換えてください)";
	private final static String CONSUMER_SECRET = "(書き換えてください)";
	private final static String ACCESS_TOKEN = "(書き換えてください)";
	private final static String ACCESS_TOKEN_SECRET = "(書き換えてください)";

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

		Button buttonPost = (Button) findViewById(R.id.buttonPost);
		buttonPost.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		Intent intent = new Intent(Intent.ACTION_PICK);
		intent.setType("image/*");
		startActivityForResult(intent, REQUEST_PICK);
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if (requestCode == REQUEST_PICK && resultCode == RESULT_OK) {
			Uri uri = data.getData();
			ContentResolver cr = getContentResolver();
			String[] columns = { MediaStore.Images.Media.DATA };
			Cursor c = cr.query(uri, columns, null, null, null);

			c.moveToFirst();
			File path = new File(c.getString(0));
			if (!path.exists())
				return;

			ConfigurationBuilder builder = new ConfigurationBuilder();
			builder.setOAuthConsumerKey(CONSUMER_KEY);
			builder.setOAuthConsumerSecret(CONSUMER_SECRET);
			builder.setOAuthAccessToken(ACCESS_TOKEN);
			builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET);
			// ここでMediaProviderをTWITTERにする
			builder.setMediaProvider("TWITTER");

			Configuration conf = builder.build();

			ImageUpload imageUpload = new ImageUploadFactory(conf)
					.getInstance();

			EditText textTweet = (EditText) findViewById(R.id.textTweet);
			String tweet = textTweet.getText().toString();

			try {
				imageUpload.upload(path, tweet);
			} catch (TwitterException e) {
				e.printStackTrace();
			}
		}
	}
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/textTweet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/buttonPost"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_upload" />

</LinearLayout>

manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.loadlimits.android.updatewithmedia"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="9" />

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".UpdateWithMediaActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ポイントはsetMediaProviderでTWITTERを選択することだけです。

image

image

こんな感じでpic.twitter.comにアップロードできました。

OAuth認証ページとか組み込みたければ、こちらを参考に。

[Android] Android+Twitter4JでOAuthするためのソースコード – adakoda


オプションメニューのアイコンと文字を横並びにしたかった

前回オプションメニューの文字色とフォントを変更するエントリを書きましたが、アイコンの隣に文字を表示したい要件が出てきました。

前回
OptionsMenuの文字色を変更する : blog.loadlimit – digital matter –

で、色々やってみましたが、手詰まりしてしまいました。やっぱりアイコンと文字をセットにした画像を用意してしまって、文字は消してしまう方が確実です。

惜しいところまで行った現状の記録を残しておきます。

device-2011-10-22-174653

なんですかね、心が離れてしまったんですかね。

ソース

public class CustomizedMenuActivity extends Activity {
	public static final int MENU_ID_COFFEE = 1;
	public static final int MENU_ID_LOVE = 2;
	public static final int MENU_ID_RECYCLE = 3;

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

	protected TextView getMenuItemView(MenuItem item) {
		try {
			Class<?> c = item.getClass(); // MenuItemImplのインスタンス
			Class<?>[] paramTypesGetItemView = { int.class, ViewGroup.class };
			Method method;
			method = c.getDeclaredMethod("getItemView", paramTypesGetItemView);
			// getItemViewはprivateメソッドなのでアクセス可能に変更する
			method.setAccessible(true);
			// IconMenuItemViewを取得できる
			TextView view = (TextView) method.invoke(item, new Object[] { 0,
					null });
			return view;
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// メニューアイテムの追加
		MenuItem menuItemCoffee = menu.add(Menu.NONE, MENU_ID_COFFEE,
				Menu.NONE, this.getText(R.string.menu_coffee)).setIcon(
				R.drawable.icon_coffee);
		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				this.getText(R.string.menu_love)).setIcon(R.drawable.icon_love);
		MenuItem menuItemRecycle = menu.add(Menu.NONE, MENU_ID_RECYCLE,
				Menu.NONE, this.getText(R.string.menu_recycle)).setIcon(
				R.drawable.icon_recycle);

		try {
			TextView viewCoffee = getMenuItemView(menuItemCoffee);
			TextView viewLove = getMenuItemView(menuItemLove);
			TextView viewRecycle = getMenuItemView(menuItemRecycle);

			// テキストの色を変える
			viewCoffee.setTextColor(0xFFB5985A);
			viewCoffee.setTextSize(14);
			viewCoffee.setTextScaleX(0.8f);

			viewLove.setTextColor(0xFFCF7D5B);
			viewLove.setTextSize(22);
			viewLove.setTypeface(Typeface.DEFAULT_BOLD);

			viewRecycle.setTextColor(0xFF8A6381);
			viewRecycle.setTextSize(16);
			viewRecycle.setTypeface(Typeface.create(Typeface.SERIF,
					Typeface.BOLD_ITALIC));

			// アイコン画像を取得
			Field field = viewLove.getClass().getDeclaredField("mIcon");
			field.setAccessible(true);
			Drawable icon = (Drawable) field.get(viewLove);
			// アイコンの描画領域を取得
//			Rect iconRect = icon.getBounds();
//			iconRect.left += 50;
//			iconRect.right += 50;
//			icon.setBounds(iconRect); // ※IconMenuItemViewクラスのonLayoutで上書きされてしまう
			// アイコンを文字の左側に表示する
			viewLove.setCompoundDrawables(icon, null, null, null);
			// 文字の配置を中央寄せにする
			viewLove.setGravity(Gravity.CENTER_VERTICAL
					| Gravity.CENTER_HORIZONTAL);

		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}

		return super.onCreateOptionsMenu(menu);
	}

}

問題はIconMenuItemViewクラスでpositionIconメソッドがアイコンの位置を決定しているのですが、onLayoutのタイミングで呼び出されるので、上書きされてしまうようなのですね。

ActivityのViewでonLayoutをオーバーライドして、うまくいけばアイコンが描画される前に位置変更差し込めるのかなぁ?

ちょっともう面倒なので画像にしてしまいます。

icon_love2

device-2011-10-22-182542

あ、いいですね。まずは画像登録とテキストを空に。

		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				null).setIcon(R.drawable.icon_love2);

文字の分の空白が下に空いてしまうので、文字サイズを1にしておきます。0は効かないようです。

viewLove.setTextSize(1);

これで一応できました。

MenuView.ItemViewを実装すればこんな事しなくてもViewをまるごとカスタマイズできるのかな…

ちょっともう少し調べてみます。

Menu::add→Menu::addInternal→new MenuItemImpl

で、MenuItemImplのgetItemViewを呼び出したタイミングでMenuItemImpl::createItemViewが呼び出されます。

    private MenuView.ItemView createItemView(int menuType, ViewGroup parent) {
        // Create the MenuView
        MenuView.ItemView itemView = (MenuView.ItemView) getLayoutInflater(menuType)
                .inflate(MenuBuilder.ITEM_LAYOUT_RES_FOR_TYPE[menuType], parent, false);
        itemView.initialize(this, menuType);
        return itemView;
    }

getLayoutInflaterは同じクラスのメソッド。

    public LayoutInflater getLayoutInflater(int menuType) {
        return mMenu.getMenuType(menuType).getInflater();
    }

リフレクションで呼んで何が返ってくるのか見て見ました。menuTypeはアイコンメニューなので0。

			Class<?>[] param = {int.class};
			method = c.getDeclaredMethod("getLayoutInflater", param);
			LayoutInflater mLayoutInflater = (LayoutInflater) method.invoke(item, new Object[] { 0 });
			Log.d(TAG, "" + mLayoutInflater.getClass().getName());

com.android.internal.policy.impl.PhoneLayoutInflater

public class PhoneLayoutInflater extends LayoutInflater

えー…

PhoneLayoutInflaterでinflateメソッドはオーバーライドされていないので、LayoutInflaterのinflateを見る。呼ばれたのはこれかな。

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        if (DEBUG) System.out.println("INFLATING from resource: " + resource);
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

resourceは、com.android.internal.R.layout.icon_menu_item_layoutですね。

frameworks/base/core/res/res/layout/icon_menu_item_layout.xml

<com.android.internal.view.menu.IconMenuItemView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/title"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="1dip"
    android:paddingLeft="3dip"
    android:paddingRight="3dip"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:fadingEdge="horizontal" />

あー、ここにあったのか。

割り込める可能性があるとすれば、MenuItemImplクラスのgetItemViewメソッド。

    View getItemView(int menuType, ViewGroup parent) {
        if (!hasItemView(menuType)) {
            mItemViews[menuType] = new WeakReference<ItemView>(createItemView(menuType, parent));
        }
        
        return (View) mItemViews[menuType].get();
    }

つまり、mItemViewsにリフレクションで先にViewを入れておけばいいってことですかね。

うーん…


OptionsMenuの文字色を変更する

Activityで端末のメニューボタンを押したときに出てくるオプションメニューですが、文字の色が変えられなくて困っていました。

Themeのandroid:panelTextAppearanceで変えられるのかと思っていたのだけど、なぜかうまく行かず。

背景画像はテーマで変更できるのですが。

リフレクションを使ってテキストカラーの変更をしてみました。Android API 8で確認しています。

デフォルト

device-2011-10-22-120919

文字色変更

device-2011-10-22-135742

Activityのコード

public class CustomizedMenuActivity extends Activity {
	public static final int MENU_ID_COFFEE = 1;
	public static final int MENU_ID_LOVE = 2;
	public static final int MENU_ID_RECYCLE = 3;

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

	private TextView getMenuItemView(MenuItem item) {
		try {
			Class<?> c = item.getClass(); // itemはMenuItemImplのインスタンス
			Class<?>[] paramTypesGetItemView = { int.class, ViewGroup.class };
			Method method = c.getDeclaredMethod("getItemView", paramTypesGetItemView);

			// getItemViewはprivateメソッドなのでアクセス可能に変更する
			method.setAccessible(true);

			// IconMenuItemViewを取得できる
			TextView view = (TextView) method.invoke(item, new Object[] { 0, null });

			return view;
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// メニューアイテムの追加
		MenuItem menuItemCoffee = menu.add(Menu.NONE, MENU_ID_COFFEE,
				Menu.NONE, this.getText(R.string.menu_coffee)).setIcon(
				R.drawable.icon_coffee);
		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				this.getText(R.string.menu_love)).setIcon(R.drawable.icon_love);
		MenuItem menuItemRecycle = menu.add(Menu.NONE, MENU_ID_RECYCLE,
				Menu.NONE, this.getText(R.string.menu_recycle)).setIcon(
				R.drawable.icon_recycle);

		try {
			TextView viewCoffee = getMenuItemView(menuItemCoffee);
			TextView viewLove = getMenuItemView(menuItemLove);
			TextView viewRecycle = getMenuItemView(menuItemRecycle);

			// テキストの色を変える
			viewCoffee.setTextColor(0xFFB5985A);
			viewCoffee.setTextSize(14);
			viewCoffee.setTextScaleX(0.8f);

			viewLove.setTextColor(0xFFCF7D5B);
			viewLove.setTextSize(22);
			viewLove.setTypeface(Typeface.DEFAULT_BOLD);

			viewRecycle.setTextColor(0xFF8A6381);
			viewRecycle.setTextSize(16);
			viewRecycle.setTypeface(Typeface.create(Typeface.SERIF,
					Typeface.BOLD_ITALIC));

		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		}

		return super.onCreateOptionsMenu(menu);
	}
}

MenuItemの実体はMenuItemImplクラスで、getItemViewメソッドを使ってIconMenuItemViewクラスを取り出しています。

IconMenuItemViewクラスはTextViewを継承しているので、キャストすればテキストカラーやフォントサイズなどの変更ができます。

ちなみに、このあと、メニューのセパレータ(divider)画像の変更とアイコンと文字の配置の変更もやりました。それは次以降のエントリで。

オプションメニューのアイコンと文字を横並びにしたかった : blog.loadlimit – digital matter –