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 フォルダーか、対応するパッケージフォルダに保存する
    1. "name""Comments",  
    2.   "scope""source.ts",  
    3.   "settings": {  
    4.    "shellVariables" : [  
    5.     {  
    6.      "name""TM_COMMENT_START",  
    7.      "value""// "  
    8.     },  
    9.     {  
    10.      "name""TM_COMMENT_START_2",  
    11.      "value""/*"  
    12.     },  
    13.     {  
    14.      "name""TM_COMMENT_END_2",  
    15.      "value""*/"  
    16.     }  
    17.    ]  
    18.   },  
    19.   "uuid""38232be9-44f1-49fd-91d4-85f5884fb298"  
    20. }  
  • "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 を使う
  1. // 8dp に相当する px 値を取得  
  2. DisplayMetrics metrics = getResources().getDisplayMetrics();  
  3. int padding = (int) (metrics.density * 8);  


2. TypedValue.applyDimension() を使う
  1. // 8dp に相当する px 値を取得  
  2. int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,   
  3.                                               8,   
  4.                                               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
  1. <resources xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   
  3.     <style name="AppTheme" parent="android:Theme.Holo.Light">  
  4.         <item name="android:listChoiceBackgroundIndicator">@drawable/selector_dropdownlist</item>  
  5.     </style>  
  6.   
  7. </resources>  


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


2013.12.3 追記

原因&解決方法を見つけました!
Theme.Holo.Light.DarkActionBar でセットされている属性を一つずつ追加していったところ、
  1. <item name="android:actionBarWidgetTheme">@android:style/Theme.Holo</item>  
が原因だということがわかりました。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
  1. 800     public Context getThemedContext() {  
  2. 801         if (mThemedContext == null) {  
  3. 802             TypedValue outValue = new TypedValue();  
  4. 803             Resources.Theme currentTheme = mContext.getTheme();  
  5. 804             currentTheme.resolveAttribute(com.android.internal.R.attr.actionBarWidgetTheme,  
  6. 805                     outValue, true);  
  7. 806             final int targetThemeRes = outValue.resourceId;  
  8. 807   
  9. 808             if (targetThemeRes != 0 && mContext.getThemeResId() != targetThemeRes) {  
  10. 809                 mThemedContext = new ContextThemeWrapper(mContext, targetThemeRes);  
  11. 810             } else {  
  12. 811                 mThemedContext = mContext;  
  13. 812             }  
  14. 813         }  
  15. 814         return mThemedContext;  
  16. 815     }  
そこで、Theme.Holo を継承し android:listChoiceBackgroundIndicator をセットしたテーマを別途用意し、android:actionBarWidgetTheme に指定するようにしたところうまくいきました。
  1. <resources xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   
  3.     <style name="AppThemeHolo" parent="android:Theme.Holo">  
  4.         <item name="android:listChoiceBackgroundIndicator">@drawable/selector_dropdownlist</item>  
  5.     </style>  
  6.       
  7.     <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">  
  8.         <item name="android:listChoiceBackgroundIndicator">@drawable/selector_dropdownlist</item>  
  9.         <item name="android:actionBarWidgetTheme">@style/AppThemeHolo</item>  
  10.     </style>  
  11.   
  12. </resources>  





■ 詳細解説

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
  1. 122     public boolean tryShow() {  
  2. 123         mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle);  
  3. 124         mPopup.setOnDismissListener(this);  
  4. 125         mPopup.setOnItemClickListener(this);  
  5. 126         mPopup.setAdapter(mAdapter);  
  6. 127         mPopup.setModal(true);  
  7. 128   
  8. 129         View anchor = mAnchorView;  
  9. 130         if (anchor != null) {  
  10. 131             final boolean addGlobalListener = mTreeObserver == null;  
  11. 132             mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest  
  12. 133             if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);  
  13. 134             anchor.addOnAttachStateChangeListener(this);  
  14. 135             mPopup.setAnchorView(anchor);  
  15. 136             mPopup.setDropDownGravity(mDropDownGravity);  
  16. 137         } else {  
  17. 138             return false;  
  18. 139         }  
  19. 140   
  20. 141         if (!mHasContentWidth) {  
  21. 142             mContentWidth = measureContentWidth();  
  22. 143             mHasContentWidth = true;  
  23. 144         }  
  24. 145   
  25. 146         mPopup.setContentWidth(mContentWidth);  
  26. 147         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);  
  27. 148         mPopup.show();  
  28. 149         mPopup.getListView().setOnKeyListener(this);  
  29. 150         return true;  
  30. 151     }  
ListPopupWindow には setListSelector() というメソッドが用意されています。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/android/widget/ListPopupWindow.java
  1.     350     public void setListSelector(Drawable selector) {  
  2.     351         mDropDownListHighlight = selector;  
  3.     352     }  
  4. ...  
  5.     971     private int buildDropDown() {  
  6.    1015     private int buildDropDown() {  
  7.    1016         ViewGroup dropDownView;  
  8.    1017         int otherHeights = 0;  
  9.    1018   
  10.    1019         if (mDropDownList == null) {  
  11.    1020             Context context = mContext;  
  12.    1021   
  13.    1022             /** 
  14.    1023              * This Runnable exists for the sole purpose of checking if the view layout has got 
  15.    1024              * completed and if so call showDropDown to display the drop down. This is used to show 
  16.    1025              * the drop down as soon as possible after user opens up the search dialog, without 
  17.    1026              * waiting for the normal UI pipeline to do it's job which is slower than this method. 
  18.    1027              */  
  19.    1028             mShowDropDownRunnable = new Runnable() {  
  20.    1029                 public void run() {  
  21.    1030                     // View layout should be all done before displaying the drop down.  
  22.    1031                     View view = getAnchorView();  
  23.    1032                     if (view != null && view.getWindowToken() != null) {  
  24.    1033                         show();  
  25.    1034                     }  
  26.    1035                 }  
  27.    1036             };  
  28.    1037   
  29.    1038             mDropDownList = new DropDownListView(context, !mModal);  
  30.    1039             if (mDropDownListHighlight != null) {  
  31.    1040                 mDropDownList.setSelector(mDropDownListHighlight);  
  32.    1041             }  
  33. ...  
しかし、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
  1. 1445         public DropDownListView(Context context, boolean hijackFocus) {  
  2. 1446             super(context, null, com.android.internal.R.attr.dropDownListViewStyle);  
  3. 1447             mHijackFocus = hijackFocus;  
  4. 1448             // TODO: Add an API to control this  
  5. 1449             setCacheColorHint(0); // Transparent, since the background drawable could be anything.  
  6. 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
  1.  906     <style name="Theme.Holo">  
  2. 1087         <item name="dropDownListViewStyle">@android:style/Widget.Holo.ListView.DropDown</item>  
  3. 1214     </style>  
  4. 1221     <style name="Theme.Holo.Light" parent="Theme.Light">  
  5. 1402         <item name="dropDownListViewStyle">@android:style/Widget.Holo.ListView.DropDown</item>  
  6. 1529     </style>  
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 をセットしています。
  1. 1649     <style name="Widget.Holo.ListView.DropDown">  
  2. 1650     </style>  
  3. 1715     <style name="Widget.Holo.ListView" parent="Widget.ListView">  
  4. 1716         <item name="android:divider">?android:attr/listDivider</item>  
  5. 1717         <item name="android:listSelector">?android:attr/listChoiceBackgroundIndicator</item>  
  6. 1718     </style>  
よって、android:listChoiceBackgroundIndicator に変更したい selector を指定すればいいということになります。



2013年12月1日日曜日

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

Navigation Drawer を開閉するには android.R.id.home ボタンをタップすればいいので、
  1. public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {  
  2.   
  3.     public MainActivityTest() {  
  4.         super(MainActivity.class);  
  5.     }  
  6.   
  7.     @Override  
  8.     public void setUp() throws Exception {  
  9.         super.setUp();  
  10.         // Espresso will not launch our activity for us, we must launch it via  
  11.         // getActivity().  
  12.         getActivity();  
  13.     }  
  14.   
  15.     public void testDrawerOpen() {  
  16.         // Drawer open  
  17.         onView(withId(android.R.id.home)).perform(click());  
  18.         // Click Settings  
  19.         onView(withId(R.id.settings)).perform(click());  
  20.     }  
  21. }  
で 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 に口を用意しないといけません。
  1. public class MainActivity extends ActionBarActivity implements DrawerListener {  
  2.   
  3.     private DrawerLayout mDrawerLayout;  
  4.     private View mDrawerContainer;  
  5.     private ActionBarDrawerToggle mDrawerToggle;  
  6.   
  7.     private DrawerFragment mDrawerFragment;  
  8.   
  9.     /** 
  10.      * Espresso で Drawer を開くため 
  11.      */  
  12.     public interface DrawerStateListener {  
  13.         public void onDrawerStateChanged(int newState);  
  14.     }  
  15.   
  16.     private DrawerStateListener mDrawerStateListener;  
  17.   
  18.     public void setDrawerListener(DrawerStateListener l) {  
  19.         mDrawerStateListener = l;  
  20.     }  
  21.   
  22.     public DrawerStateListener getDrawerListener() {  
  23.         return mDrawerStateListener;  
  24.     }  
  25.     /** 
  26.      *  
  27.      */  
  28.   
  29.     @Override  
  30.     protected void onCreate(Bundle savedInstance) {  
  31.         super.onCreate(savedInstance);  
  32.         setContentView(R.layout.activity_main);  
  33.   
  34.         mDrawerFragment = (DrawerFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_drawer);  
  35.   
  36.         mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);  
  37.         mDrawerContainer = findViewById(R.id.left_drawer);  
  38.   
  39.         mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.drawable.ic_drawer,  
  40.                 R.string.drawer_open, R.string.drawer_close) {  
  41.   
  42.             @Override  
  43.             public void onDrawerClosed(View drawerView) {  
  44.                 supportInvalidateOptionsMenu();  
  45.                 super.onDrawerClosed(drawerView);  
  46.             }  
  47.   
  48.             @Override  
  49.             public void onDrawerOpened(View drawerView) {  
  50.                 supportInvalidateOptionsMenu();  
  51.                 super.onDrawerOpened(drawerView);  
  52.             }  
  53.   
  54.             @Override  
  55.             public void onDrawerStateChanged(int newState) {  
  56.                 if (mDrawerStateListener != null) {  
  57.                     mDrawerStateListener.onDrawerStateChanged(newState);  
  58.                 }  
  59.                 super.onDrawerStateChanged(newState);  
  60.             }  
  61.         };  
  62.   
  63.         mDrawerLayout.setDrawerListener(mDrawerToggle);  
  64.   
  65.         getSupportActionBar().setDisplayHomeAsUpEnabled(true);  
  66.         getSupportActionBar().setHomeButtonEnabled(true);  
  67.     }  
  68.   
  69.     ...  
  70. }  
Activity に DrawerStateListener という口を用意しました。
この DrawerStateListener を実装した DrawerStateListenerImpl クラスを用意し、onDrawerStateChanged(int newState) で newState の値に応じて increment(), decrement() します。 このテストでは Navigation Drawer 部分をドラッグしないので DrawerLayout.STATE_DRAGGING は無いと思って簡略化しています。

setUp() の中で getActivity() で取得した Activity に対して DrawerStateListenerImpl のインスタンスを差し込み、registerIdlingResources() で DrawerStateListenerImpl で参照している CountingIdlingResource のインスタンスを登録しています。
  1. public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {  
  2.   
  3.     public MainActivityTest() {  
  4.         super(MainActivity.class);  
  5.     }  
  6.   
  7.     private class DrawerStateListenerImpl implements DrawerStateListener {  
  8.         private final DrawerStateListener realDrawerListener;  
  9.         private final CountingIdlingResource mIdlingResource;  
  10.   
  11.         private DrawerStateListenerImpl(DrawerStateListener l, CountingIdlingResource idlingResource) {  
  12.             this.realDrawerListener = l;  
  13.             this.mIdlingResource = checkNotNull(idlingResource);  
  14.         }  
  15.   
  16.         @Override  
  17.         public void onDrawerStateChanged(int newState) {  
  18.             // ドラッグしないので  
  19.             if (newState != DrawerLayout.STATE_IDLE) {  
  20.                 mIdlingResource.increment();  
  21.             } else {  
  22.                 mIdlingResource.decrement();  
  23.             }  
  24.   
  25.             if (realDrawerListener != null) {  
  26.                 realDrawerListener.onDrawerStateChanged(newState);  
  27.             }  
  28.         }  
  29.     }  
  30.   
  31.     @Override  
  32.     public void setUp() throws Exception {  
  33.         super.setUp();  
  34.         // Espresso will not launch our activity for us, we must launch it via  
  35.         // getActivity().  
  36.         MainActivity activity = getActivity();  
  37.         CountingIdlingResource countingResource = new CountingIdlingResource("DrawerCalls");  
  38.         activity.setDrawerListener(new DrawerStateListenerImpl(activity.getDrawerListener(), countingResource));  
  39.         registerIdlingResources(countingResource);  
  40.     }  
  41.   
  42.     public void testDrawerOpen() {  
  43.         // Drawer open  
  44.         onView(withId(android.R.id.home)).perform(click());  
  45.         // Click Settings  
  46.         onView(withId(R.id.settings)).perform(click());  
  47.     }  
  48. }  
このテストでは、ちゃんと 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 をインストール


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