2013年12月19日木曜日

Sublime Text 2 のプラグインで Toggle Comment を実装する

1. AAAPackageDev を Sublime Text に入れる
  • https://bitbucket.org/guillermooo/aaapackagedev/downloads から AAAPackageDev.sublime-package をダウンロードする
  • AAAPackageDev.sublime-package を Sublime Text の Installed Packages ([Preferences] - [Browse Packages...] から開くフォルダの一つ上の階層にある)に入れる
  • Sublime Text を再起動する


2. Package を作成する

作成する Syntax Definition に対応するパッケージがない場合は

[Tools] - [Packages] - [Package Development] - [New Packages...]

を選択し、パッケージ名を入力して Enter を押す



3. Comments Definition を作成する
  • <lang_name>.JSON-tmPreferences というファイル名で Packages/User フォルダーか、対応するパッケージフォルダに保存する { "name": "Comments", "scope": "source.ts", "settings": { "shellVariables" : [ { "name": "TM_COMMENT_START", "value": "// " }, { "name": "TM_COMMENT_START_2", "value": "/*" }, { "name": "TM_COMMENT_END_2", "value": "*/" } ] }, "uuid": "38232be9-44f1-49fd-91d4-85f5884fb298" }
  • "name" : "Comments" にする(別でもいいような気もするが)
  • "scope" : 対応する .tmLanguage の scopeName の値を使う
  • TM_COMMENT_START はシングルラインコメント、[Edit] - [Comment] - [Toggle Comment] に対応する
  • TM_COMMENT_START_2 はブロックコメントの開始、TM_COMMENT_END_2 はブロックコメントの終わり、[Edit] - [Comment] - [Toggle Block Comment] に対応する
  • ブロックコメントがない場合、TM_COMMENT_START_2とTM_COMMENT_END_2を両方省略することができる


4. .tmPreferences ファイルに変換する
  • [Tools] - [Build System] - [JSON to Property List] を選択
  • F7(または [Tools] - [Build])を押すと .JSON-tmPreferences ファイルと同じディレクトリに .tmPreferences ファイルができる
  • Sublime Text を再起動する




Sublime Text 2 用 ReVIEW プラグインでは [Edit] - [Comment] - [Toggle Comment] で先頭に #@# が挿入されるようにしました!



2013年12月9日月曜日

Android Javaコードで dp 単位を px 単位に変換する

1. DisplayMetrics を使う // 8dp に相当する px 値を取得 DisplayMetrics metrics = getResources().getDisplayMetrics(); int padding = (int) (metrics.density * 8);

2. TypedValue.applyDimension() を使う // 8dp に相当する px 値を取得 int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());

2013年12月2日月曜日

ActionBar の Overflow Menu の selector の色を変える

Android UI Cookbook for 4.0 で紹介した方法だと、適用する selector が不透明の場合はいいのですが、半透明の場合問題が起こります。

以下は、Android UI Cookbook for 4.0 で紹介した方法(android:itemBackground を指定する方法)で、押したとき背景が半透明の赤になるようにした場合です。



赤とデフォルトの水色が混ざって、紫っぽくなってしまっています。
これは、半透明の赤の下にデフォルトの水色の selector も表示されているからです。

これを修正する方法は2つあります。
  • 1. android:itemBackground には何も指定せず、下の水色の selector を変更する
  • 2. android:itemBackground を指定して、下の水色の selector に透明を指定する
いずれにしろ、この水色の selector を変更しないといけません。
結論としては、android:listChoiceBackgroundIndicator を利用します。この属性は API Level 11 からですが、Support Library v7 の appcompat でも対応しています。

res/values/styles.xml

ただし!なぜかベースのテーマを Theme.Holo.Light.DarkActionBar にすると、この設定が効きません!(Theme.Holo、Theme.Holo.Light は効く)
原因はまだ見つけてません。ぐぬぬ


2013.12.3 追記

原因&解決方法を見つけました!
Theme.Holo.Light.DarkActionBar でセットされている属性を一つずつ追加していったところ、 @android:style/Theme.Holo が原因だということがわかりました。Theme.Holo および Theme.Holo.Light ではこの属性には @null が指定されています。

この属性は ActionBarImpl クラスで利用されています。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/com/android/internal/app/ActionBarImpl.java#800 800 public Context getThemedContext() { 801 if (mThemedContext == null) { 802 TypedValue outValue = new TypedValue(); 803 Resources.Theme currentTheme = mContext.getTheme(); 804 currentTheme.resolveAttribute(com.android.internal.R.attr.actionBarWidgetTheme, 805 outValue, true); 806 final int targetThemeRes = outValue.resourceId; 807 808 if (targetThemeRes != 0 && mContext.getThemeResId() != targetThemeRes) { 809 mThemedContext = new ContextThemeWrapper(mContext, targetThemeRes); 810 } else { 811 mThemedContext = mContext; 812 } 813 } 814 return mThemedContext; 815 } そこで、Theme.Holo を継承し android:listChoiceBackgroundIndicator をセットしたテーマを別途用意し、android:actionBarWidgetTheme に指定するようにしたところうまくいきました。




■ 詳細解説

Overflow Menu は ListPopupWindow です。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/com/android/internal/view/menu/MenuPopupHelper.java#122 122 public boolean tryShow() { 123 mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle); 124 mPopup.setOnDismissListener(this); 125 mPopup.setOnItemClickListener(this); 126 mPopup.setAdapter(mAdapter); 127 mPopup.setModal(true); 128 129 View anchor = mAnchorView; 130 if (anchor != null) { 131 final boolean addGlobalListener = mTreeObserver == null; 132 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest 133 if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this); 134 anchor.addOnAttachStateChangeListener(this); 135 mPopup.setAnchorView(anchor); 136 mPopup.setDropDownGravity(mDropDownGravity); 137 } else { 138 return false; 139 } 140 141 if (!mHasContentWidth) { 142 mContentWidth = measureContentWidth(); 143 mHasContentWidth = true; 144 } 145 146 mPopup.setContentWidth(mContentWidth); 147 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 148 mPopup.show(); 149 mPopup.getListView().setOnKeyListener(this); 150 return true; 151 } ListPopupWindow には setListSelector() というメソッドが用意されています。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/android/widget/ListPopupWindow.java 350 public void setListSelector(Drawable selector) { 351 mDropDownListHighlight = selector; 352 } ... 971 private int buildDropDown() { 1015 private int buildDropDown() { 1016 ViewGroup dropDownView; 1017 int otherHeights = 0; 1018 1019 if (mDropDownList == null) { 1020 Context context = mContext; 1021 1022 /** 1023 * This Runnable exists for the sole purpose of checking if the view layout has got 1024 * completed and if so call showDropDown to display the drop down. This is used to show 1025 * the drop down as soon as possible after user opens up the search dialog, without 1026 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1027 */ 1028 mShowDropDownRunnable = new Runnable() { 1029 public void run() { 1030 // View layout should be all done before displaying the drop down. 1031 View view = getAnchorView(); 1032 if (view != null && view.getWindowToken() != null) { 1033 show(); 1034 } 1035 } 1036 }; 1037 1038 mDropDownList = new DropDownListView(context, !mModal); 1039 if (mDropDownListHighlight != null) { 1040 mDropDownList.setSelector(mDropDownListHighlight); 1041 } ... しかし、MenuPopupHelper では、setListSelector() を呼んでくれていません。

ListPopupWindow 内のリストは、DropDownListView です。 このクラスは ListPopupWindow の内部クラスです。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/android/widget/ListPopupWindow.java#1445 1445 public DropDownListView(Context context, boolean hijackFocus) { 1446 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1447 mHijackFocus = hijackFocus; 1448 // TODO: Add an API to control this 1449 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 1450 } ここでは com.android.internal.R.attr.dropDownListViewStyle を defStyleAttr として渡しています。

よって、android:dropDownListViewStyle を指定すればいいということです。Holo テーマでどのようなスタイルになっているか見てみましょう。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/res/res/values/themes.xml 906 1221 Theme.Holo も Theme.Holo.Light も @android:style/Widget.Holo.ListView.DropDown がセットされています。

Widget.Holo.ListView.DropDown は Widget.Holo.ListView と同じで、Widget.Holo.ListView では android:divider として ?android:attr/listDivider を、android:listSelector として ?android:attr/listChoiceBackgroundIndicator をセットしています。 1649 1715 よって、android:listChoiceBackgroundIndicator に変更したい selector を指定すればいいということになります。



2013年12月1日日曜日

Espresso で Navigation Drawer を開閉する(DIを使わない編)

Navigation Drawer を開閉するには android.R.id.home ボタンをタップすればいいので、 public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> { public MainActivityTest() { super(MainActivity.class); } @Override public void setUp() throws Exception { super.setUp(); // Espresso will not launch our activity for us, we must launch it via // getActivity(). getActivity(); } public void testDrawerOpen() { // Drawer open onView(withId(android.R.id.home)).perform(click()); // Click Settings onView(withId(R.id.settings)).perform(click()); } } で OK そうです(R.id.settings は Navigation Drawer 内のボタン)。

ところが、これはテストに失敗します。
onView(withId(R.id.settings)).perform(click());
のところで、そんな Id の View は見つからないと言われてしまいます。

原因は、Navigation Drawer が開き終わる前に View を探そうとするからです。

そこで、Navigation Drawer が開き(または閉じ)はじめてから、閉じる(または開く)まで、 Espresso に今は Idle 状態じゃないと伝えるようにします。

まず、IdlingResource インタフェースを実装したクラスのインスタンスを用意し、Espresso.registerIdlingResources()で登録します。

IdlingResource を実装したクラスとして、CountingIdlingResource が用意されています。
このクラスは内部でカウンターを持っていて、increment() と decrement() でカウンターの値を変え、カウンターが 0 のときが Idle 状態として Espresso に伝えられます。

Navigation Drawer を実現している DrawerLayout クラスの DrawerListener.onDrawerStateChanged() を利用して、カウンターの値を変えるようにします。

そのため、テスト対象の Activity に口を用意しないといけません。 public class MainActivity extends ActionBarActivity implements DrawerListener { private DrawerLayout mDrawerLayout; private View mDrawerContainer; private ActionBarDrawerToggle mDrawerToggle; private DrawerFragment mDrawerFragment; /** * Espresso で Drawer を開くため */ public interface DrawerStateListener { public void onDrawerStateChanged(int newState); } private DrawerStateListener mDrawerStateListener; public void setDrawerListener(DrawerStateListener l) { mDrawerStateListener = l; } public DrawerStateListener getDrawerListener() { return mDrawerStateListener; } /** * */ @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); setContentView(R.layout.activity_main); mDrawerFragment = (DrawerFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_drawer); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerContainer = findViewById(R.id.left_drawer); mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) { @Override public void onDrawerClosed(View drawerView) { supportInvalidateOptionsMenu(); super.onDrawerClosed(drawerView); } @Override public void onDrawerOpened(View drawerView) { supportInvalidateOptionsMenu(); super.onDrawerOpened(drawerView); } @Override public void onDrawerStateChanged(int newState) { if (mDrawerStateListener != null) { mDrawerStateListener.onDrawerStateChanged(newState); } super.onDrawerStateChanged(newState); } }; mDrawerLayout.setDrawerListener(mDrawerToggle); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); } ... } Activity に DrawerStateListener という口を用意しました。
この DrawerStateListener を実装した DrawerStateListenerImpl クラスを用意し、onDrawerStateChanged(int newState) で newState の値に応じて increment(), decrement() します。 このテストでは Navigation Drawer 部分をドラッグしないので DrawerLayout.STATE_DRAGGING は無いと思って簡略化しています。

setUp() の中で getActivity() で取得した Activity に対して DrawerStateListenerImpl のインスタンスを差し込み、registerIdlingResources() で DrawerStateListenerImpl で参照している CountingIdlingResource のインスタンスを登録しています。 public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> { public MainActivityTest() { super(MainActivity.class); } private class DrawerStateListenerImpl implements DrawerStateListener { private final DrawerStateListener realDrawerListener; private final CountingIdlingResource mIdlingResource; private DrawerStateListenerImpl(DrawerStateListener l, CountingIdlingResource idlingResource) { this.realDrawerListener = l; this.mIdlingResource = checkNotNull(idlingResource); } @Override public void onDrawerStateChanged(int newState) { // ドラッグしないので if (newState != DrawerLayout.STATE_IDLE) { mIdlingResource.increment(); } else { mIdlingResource.decrement(); } if (realDrawerListener != null) { realDrawerListener.onDrawerStateChanged(newState); } } } @Override public void setUp() throws Exception { super.setUp(); // Espresso will not launch our activity for us, we must launch it via // getActivity(). MainActivity activity = getActivity(); CountingIdlingResource countingResource = new CountingIdlingResource("DrawerCalls"); activity.setDrawerListener(new DrawerStateListenerImpl(activity.getDrawerListener(), countingResource)); registerIdlingResources(countingResource); } public void testDrawerOpen() { // Drawer open onView(withId(android.R.id.home)).perform(click()); // Click Settings onView(withId(R.id.settings)).perform(click()); } } このテストでは、ちゃんと R.id.settings ボタンがクリックされます。


Unable to execute dex: java.nio.BufferOverflowException.

[2013-12-01 12:14:18 - Dex Loader] Unable to execute dex: java.nio.BufferOverflowException. Check the Eclipse log for stack trace.

とか言われて調べていると、次の Issue に行き当たりました。

Issue 61710 - android - java.nio.BufferOverflowException When Building with 19 Build Tools - Android Open Source Project - Issue Tracker - Google Project Hosting:

Build Tool を 19.0.0 から 18.1.1 にしたらいいらしので、

Android SDK Manager を開いて
  • Android SDK Build-tools Rev. 19 を削除
  • Android SDK Build-tools Rev. 18.1.1 をインストール


エラーがでなくなりました!