digital matter
Internet Explorer 6さようなら運動はじめました!

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

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

前回
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 -

関連する投稿

Cannot connect to VMと表示されてデバッグができないとき

EclipseでAndroidの開発をしていて、上記ダイアログが出て唐突にデバッグで起動できなくなったら、慌てずにそのF11を押す手を離して、パッケージマネージャのプロジェクトを選択して、Debug As Android Applicationだ。

OK?

関連する投稿

ライブ壁紙開発時にUSBケーブルを接続しないと起動しない問題

問題というほどでもなかった。Androidのライブ壁紙作成のお話。

ライブ壁紙をステップ実行してデバッグするためにwaitForDebuggerを書いたのはいいのだけど、それを忘れてデバッグ用じゃない端末にインストールすると、一向にライブ壁紙を選択した後の「ライブ壁紙を読み込み中…」から進まない。

開発用端末でUSBケーブルを繋いでいると先に進めてしまうので、isDebuggerConnectedメソッドで分岐するようにしました。

public class MyWallpaperService extends WallpaperService {

	private final Handler handler = new Handler();

	@Override
	public Engine onCreateEngine() {
		if (android.os.Debug.isDebuggerConnected() // ここでデバッガが繋がっているか判定
				&& MyApplication.isDebuggable()) { // こっちはmanifestのdebuggableを読む実装
			android.os.Debug.waitForDebugger();    // デバッガ待機
		}
		return new MyEngine();
	}

	// do something ...

}

関連する投稿

Androidで地磁気センサーの値が取れない問題

いや、取れないわけじゃないですけどね。SensorManager.SENSOR_STATUS_UNRELIABLEにハマった…

駄猫の備忘録: 久々にセンサーを使ってみた

XPERIA rayで方位が取れない問題があったので、調べていたところaccuracyのチェックでメソッドを抜けていたことが判明…完全に見落としてた。

	@Override
	public void onSensorChanged(SensorEvent event) {

		if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
			return;
		}

		switch (event.sensor.getType()) {
		case Sensor.TYPE_MAGNETIC_FIELD:
			magneticValues = event.values.clone(); // ■ここに来ない
			break;
		case Sensor.TYPE_ACCELEROMETER:
			accelerometerValues = event.values.clone();
			break;
		}

		// do something ...

	}

GALAXY Sでも方向が取れないと報告があったので多分これでしょう。

キャリブレーションすればいいのかな…

関連する投稿

Heap updates are NOT ENABLED for this client

How to enable Heap updates on my android client – Stack Overflow

EclipseのDDMSでAndroid実機端末のヒープを見る方法のメモ。

heap1

heap2

デバイスビューの緑の筒みたいなアイコンをクリックします。

heap3

これでヒープが見られるようになります。

heap4

関連する投稿

CoreException: Could not calculate build plan: Plugin org.apache.maven.plugins:maven-resource-plugin:2.4.3

Eclipse 3.7(Indigo)で、m2eをインストールしてTwitter4jをインポートしたあと、pom.xmlで

CoreException: Could not calculate build plan: Plugin org.apache.maven.plugins:maven-resource-plugin:2.4.3

というエラーが表示されてビルドできないという現象がありました。

m2eの再インストールとか色々試したのですが、以下の情報で解決できました。

De GIS, Programación y Otros Demonios: Maven Error: Could not calculate build plan

Windowsのユーザーフォルダにある.m2\repositoryフォルダの中から、「.lastUpdated」という拡張子がついたファイルを検索します。

ファイルが見つかったら、それを削除します。

Eclipseのプロジェクトエクスプローラー上から、プロジェクトを選択して、右クリック、Maven→Update Project Configuration…を選択、プロジェクトを選択してOKを押すだけです。

関連する投稿

MavenのプロジェクトをEclipseにインポートする

環境はWindows7+Eclipse 3.7(Indigo)。

以前、m2eclipseと呼ばれていたプロジェクトは正式にEclipseのプロジェクトに取り込まれて、今はm2eとしてEclipseのレポジトリからインストールできます。

Webで検索すると、sonatypeのサイトにリンクしている、情報が古いエントリが多く出てくるので注意。

メニューのHelp→Install New Software…で、Work withに「Indigo – http://download.eclipse.org/releases/indigo」を選択。

「type filter text」の欄に「maven」と入力すれば「m2e」が表示されます。「m2e – Maven Integration for Eclipse    1.0.0.20110607-2117」にチェックを入れて、先に進めばインストールできます。

実際にMavenのプロジェクトを取り込むときは、メニューのFile→Import→Maven→Existing Maven Projectsを選択して、「Root Directory」の欄にpom.xmlのあるディレクトリを指定すればProjectsのリストが表示されます。

関連する投稿

Symfonyの機能テストでclickを使わずにmultipartのファイルPOSTをする方法

Symfony 1.4で実装したAPIのFunctionalテストをする際に、ファイルをPOSTする方法に迷ったので調べました。

ファイルのアップロードテストは、通常のWebサイトであれば、

$browser->
    get('/upload')->
    click('Upload', array('form' => array(
        'file1' => sfConfig::get('sf_test_dir').'/datas/test01.jpg',
    )))
;

みたいに書くのですが、テストしたいアプリケーションはAPIなので、GETするページはありません。

sfBrowserBase::clickを参考にして、こんな感じで実装しました。

まずはテスト本体。test/functional/api/hogeActionsTest.php

<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

$browser = new MyTestFunctional(new myBrowser());
$browser->loadData();

$browser->
    info('JPEGファイルをアップロードできる')->
    setUploadFile('file1', sfConfig::get('sf_test_dir').'/datas/test01.jpg')->
    post('/upload', array('title' => 'hoge'))->

    with('response')->begin()->
        isStatusCode(200)->
    end()->
;

次にmyBrowserを定義します。lib/test/myBrowser.class.php

<?php

class myBrowser extends sfBrowser
{

    public function setUploadFile($key, $filename)
    {
        if (is_readable($filename))
        {
          $fileError = UPLOAD_ERR_OK;
          $fileSize = filesize($filename);
        }
        else
        {
          $fileError = UPLOAD_ERR_NO_FILE;
          $fileSize = 0;
        }

        $this->parseArgumentAsArray($key, array(
            'name' => basename($filename),
            'type' => '',
            'tmp_name' => $filename,
            'error' => $fileError,
            'size' => $fileSize,
        ),
        $this->files);
    }

}

sfBrowserBaseクラスのfilesに対して配列でファイルの情報を書き込むと、POST直前に$_FILESに書きこんでくれます。

で、あとはMyでmyBrowser::setUploadFileを呼び出すだけです。lib/test/MyTestFunctional.class.php

<?php

class MyTestFunctional extends sfTestFunctional
{
    public function loadData()
    {
        $doctrine = Doctrine_Manager::getInstance()->getCurrentConnection()->getDbh();
        $doctrine->query('SET FOREIGN_KEY_CHECKS = 0');
        $doctrine->query('TRUNCATE TABLE uploads');
        $doctrine->query('SET FOREIGN_KEY_CHECKS = 1');
        unset($doctrine);

        Doctrine::loadData(sfConfig::get('sf_test_dir').'/fixtures/');
        return $this;
    }

    public function setUploadFile($key, $filename)
    {
        $this->browser->setUploadFile($key, $filename);

        return $this;
    }
}

こうやってsetUploadFile(…)->post(…)と呼び出せばOKです。

sfBrowserBase::callされるたびにfilesの中身はクリアされます。

関連する投稿

SSDのデータを完全消去する

SSDを廃棄したり、人に譲るときなど、データを完全に削除したい場合があります。

HDDで使われるディスク消去ツールは、HDDの特性に合わせて作られているので、SSDで実行した場合に、限られた書き込み回数を無駄に消費することになるので使えません。

Intelから提供されているSSD Toolboxを使うと、簡単にデータを削除できます。

ダウンロード・センター Intel® Solid State Drive Toolbox

Windows 7,Windows Vista,Windows XPで使えます。

関連する投稿