2015年6月23日火曜日

AppCompat を継承したテーマで EditText のデフォルトスタイルを上書きするときは android:editTextStyle ではなく editTextStyle を使う

注意: appcompat-v7:22.2.0 での話です。将来 fix される可能性もあります。

AppCompat を継承したテーマで EditText のデフォルトスタイルを上書きしようとして android:editTextStyle を使うと、5系以降しか適用されないという落とし穴があります。

結論

android: をつけずに editTextStyle で指定すると4系にも適用されます。ただし、parent が Widget.AppCompat.EditText の場合、5系でカーソルが白になります(解説参照のこと)。
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="editTextStyle">@style/EditTextStyle</item>  
  5.     </style>  
  6.   
  7.     <style name="EditTextStyle" parent="Widget.AppCompat.EditText">  
  8.         <item name="android:background">#ccccff</item>  
  9.     </style>  
  10.   
  11. </resources>  

背景を変えたいだけなら android:editTextBackground と editTextBackground 両方を指定するほうが、カーソルの色を維持できます。
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="android:editTextBackground">@drawable/blue</item>  
  5.         <item name="editTextBackground">@drawable/blue</item>  
  6.     </style>  
  7.   
  8.     <drawable name="blue">#ccccff</drawable>  
  9.   
  10. </resources>  



解説

1. デフォルトの状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.     </style>  
  5.   
  6. </resources>  



2. android:editTextStyle を指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="android:editTextStyle">@style/EditTextStyle</item>  
  5.     </style>  
  6.   
  7.     <style name="EditTextStyle" parent="android:Widget.EditText">  
  8.         <item name="android:background">#ffffff</item>  
  9.     </style>  
  10.   
  11. </resources>  



3. editTextStyle を指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="editTextStyle">@style/EditTextStyle</item>  
  5.     </style>  
  6.   
  7.     <style name="EditTextStyle" parent="android:Widget.EditText">  
  8.         <item name="android:background">#ffcccc</item>  
  9.     </style>  
  10.   
  11. </resources>  


EditTextStyle の parent が android:Widget.EditText なので、4.4.2 のカーソルの色が黒になってしまっています。


4. parent が Widget.AppCompat.EditText なスタイルを editTextStyle を指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="editTextStyle">@style/EditTextStyle</item>  
  5.     </style>  
  6.   
  7.     <style name="EditTextStyle" parent="Widget.AppCompat.EditText">  
  8.         <item name="android:background">#ccccff</item>  
  9.     </style>  
  10.   
  11. </resources>  


今度は 5.0.0 のカーソルの色が白になった...

この白が何かというと、v12/values-v12.xml で android:textCursorDrawable に指定されている @drawable/abc_text_cursor_mtrl_alpha です。 これに accentColor で tint するのが適用されず白くなっているようです。たぶん。
  1. <style name="Base.V12.Widget.AppCompat.EditText" parent="Base.V7.Widget.AppCompat.EditText">  
  2.     <item name="android:textCursorDrawable">@drawable/abc_text_cursor_mtrl_alpha</item>  
  3. </style>  
そこで、android:editTextBackground です。


android:editTextBackground

背景を変えるだけなら android:editTextBackground を指定するという方法もあります。 ただ、こちらも落とし穴があり、
- android:editTextBackground // 5系にしか適用されない
- editTextBackground // 4系にしか適用されない
という状態なので、両方指定する必要があります。


5. android:editTextBackground を指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="android:editTextBackground">@drawable/yellow</item>  
  5.     </style>  
  6.   
  7.     <drawable name="yellow">#ffff00</drawable>  
  8.   
  9. </resources>  



6. editTextBackground を指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="editTextBackground">@drawable/cyan</item>  
  5.     </style>  
  6.   
  7.     <drawable name="cyan">#00ffff</drawable>  
  8.   
  9. </resources>  



7. android:editTextBackground と editTextBackground を両方指定した状態
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">  
  4.         <item name="android:editTextBackground">@drawable/blue</item>  
  5.         <item name="editTextBackground">@drawable/blue</item>  
  6.     </style>  
  7.   
  8.     <drawable name="blue">#ccccff</drawable>  
  9.   
  10. </resources>  



2015年6月14日日曜日

android.support.annotation を活用する

利用するには dependencies に追加します。
  1. dependencies {  
  2.     compile "com.android.support:support-annotations:22.0.2"  
  3. }  
参考 上記のドキュメントに書いてありますが、例えば

@Nullable を指定している変数に対して NPE になるようなコードを書くと警告してくれます。



alt + enter で修正候補がでます。



2番目の Replace with 'd != null ?:' を選択するとこんな感じになります。



@NonNull が指定された変数に null を渡そうとすると警告してくれます。



リソースIDはどれも int なので、Drawable のリソースIDを意図しているところに別のリソースIDを渡すことができてしまいます。



そこで Drawable のリソースIDを意図しているところに @DrawableRes を付けると、別のリソースIDを渡そうとしたところがエラーになります。



@StringRes や @LayoutRes など主要なものはそろっています。なにがあるかは package summary で確認してください。

@IntDef や @StringDef では、int や String に対して取りうる値のセットが定義されたアノテーションを作ることができます。
  1. public static final int SIZE_L = 0;  
  2. public static final int SIZE_M = 1;  
  3. public static final int SIZE_S = 2;  
  4.   
  5. @Retention(RetentionPolicy.SOURCE)  
  6. @IntDef({SIZE_L, SIZE_M, SIZE_S})  
  7. public @interface Size {  
  8. }  
  9.   
  10. @Size  
  11. public abstract int getSize();  
  12.   
  13. public abstract void setSize(@Size int size);  
  1. public static final String FORMAT_XML = "xml";  
  2. public static final String FORMAT_JSON = "json";  
  3.   
  4. @Retention(RetentionPolicy.SOURCE)  
  5. @StringDef({FORMAT_XML, FORMAT_JSON})  
  6. public @interface FORMAT {  
  7. }  
  8.   
  9. public abstract void fetchData(@FORMAT String format);  
@IntDef は enum だけでなく flag としても使うことができます。
  1. public static final int SHOW_TITLE = 0x1;  
  2. public static final int SHOW_SUB_TITLE = 0x2;  
  3. public static final int SHOW_ICON = 0x4;  
  4.   
  5. @Retention(RetentionPolicy.SOURCE)  
  6. @IntDef(flag = true,  
  7.         value = {  
  8.                 SHOW_TITLE,  
  9.                 SHOW_SUB_TITLE,  
  10.                 SHOW_ICON,  
  11.         })  
  12. public @interface TitleOptions {  
  13. }  
詳しくは Creating Enumerated Annotations を見るとよいです。



22.2.0で追加されたアノテーション

22.2.0 で新しく 13個のアノテーションが追加されました。上記のドキュメント及び android.support.annotation のパッケージサマリーにはまだないですが、そのうち更新されるでしょう。
Android Studio を 1.3 にしないと警告やエラーがでてくれないので注意です。
  • BinderThread
  • CallSuper
  • CheckResult
  • ColorInt
  • FloatRange
  • IntRange
  • Keep
  • MainThread
  • RequiresPermission
  • Size
  • TransitionRes
  • UiThread
  • WorkerThread
MainThread, WorkerThread, (BinderThread, UiThread), FloatRange, IntRange, Size, CallSuper, CheckResult, ColorInt, RequiresPermission は Google I/O 2015 のセッション「What's New in Android Development Tools」で紹介されていました。



実行スレッド系
- BinderThread
- MainThread
- UiThread
- WorkerThread

メソッドやクラスの実行スレッドを明記する。クラスにつけた場合はそのクラスのメソッド全てが対象になります。
MainThread と UiThread の違いがわからない...

↓バックグラウンドで @MainThread のメソッドを呼ぶと怒られます。



↓@MainThread がついてるメソッドで @WorkerThread のメソッドを呼ぶと怒られます。





値指定系
- FloatRange
- IntRange
- Size

@FloatRange

↓@FloatRangeの範囲外の値を指定すると怒られます。



Float とついているが double にも使えます。



@IntRange

↓@IntRangeの範囲外の値を指定すると怒られます。



Int とついているが long にも使えます。



@Size

@Size は size や length の指定ができます。文字列の長さや array, collection のサイズに使います。





multiple で約数を指定することもできます。



その他
- CallSuper
- CheckResult
- ColorInt
- Keep
- RequiresPermission

@CallSuper

↓@CallSuper を指定したメソッドをOverrideしてsuperを呼ばないと怒られます。





@CheckResult

↓@CheckResult を指定したメソッドを呼び出して戻り値を利用しないと怒られます。





@ColorInt

↓@ColorInt に color リソースID を渡すと怒られます。





@Keep

@Keep はビルド時に minified されたコードから外さないでほしいことを明示するためのアノテーションです。



@RequiresPermission

@RequiresPermission は必要なパーミッションを明示するためのアノテーションです。
  1. @RequiresPermission(Manifest.permission.NFC)  
  2. public abstract void scanNfc();  
↓パーミッションが無い状態で、それを必要とするAPIを呼ぶと怒ってくれるようになりました。









2015年6月13日土曜日

ScrimInsetsFrameLayout を使うときは android:background を指定する

Android Design Support Library で NavigationView が用意されましたね。
ただ、すぐには移行できなかったり、NavigationView では今のものを置き換えられれない場合などもあるでしょう。

そうは言っても StatusBar 部分の処理だけでも取り込みたい、という場合 ScrimInsetsFrameLayout で包むという方法が使えます。

参考: http://stackoverflow.com/questions/26745300/navigation-drawer-semi-transparent-over-status-bar-not-working

ScrimInsetsFrameLayout はもともと Google I/O アプリで使われていたクラスで、android:fitsSystemWindows="true" で計算される領域と View の領域との差分領域を app:insetForeground で指定された色で塗るという処理をしています。

ScrimInsetsFrameLayout は FrameLayout を継承しているので ViewGroup です。ViewGroup は常に draw() が呼ばれるわけではなく、描画の必要があるときしか呼ばれません。そのため、ScrimInsetsFrameLayout に直接背景を指定しないと draw() が呼ばれず StatusBar 部分に app:insetForeground で指定した色が塗られません。

ScrimInsetsFrameLayout を継承している NavigationView ではコンストラクタで setBackgroundDrawable() を呼んでいます。

Design Support Library の ScrimInsetsFrameLayout は @hi de ですが、こんな感じでXMLで指定して使えます。
  1. <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:app="http://schemas.android.com/apk/res-auto"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:id="@+id/drawer_layout"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent"  
  7.     android:fitsSystemWindows="true">  
  8.   
  9.     <LinearLayout  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="match_parent"  
  12.         android:orientation="vertical">  
  13.   
  14.         <android.support.v7.widget.Toolbar  
  15.             android:id="@+id/action_bar"  
  16.             android:layout_width="match_parent"  
  17.             android:layout_height="?attr/actionBarSize"  
  18.             android:background="?attr/colorPrimary"  
  19.             android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"  
  20.             app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />  
  21.   
  22.         <FrameLayout  
  23.             android:id="@+id/container"  
  24.             android:layout_width="match_parent"  
  25.             android:layout_height="match_parent" />  
  26.   
  27.     </LinearLayout>  
  28.   
  29.     <!--  android:background を忘れずに -->  
  30.     <android.support.design.internal.ScrimInsetsFrameLayout  
  31.         android:id="@+id/navigation_container"  
  32.         android:layout_width="wrap_content"  
  33.         android:layout_height="match_parent"  
  34.         android:layout_gravity="start"  
  35.         android:background="#ccc"  
  36.         android:fitsSystemWindows="true"  
  37.         app:insetForeground="#4000">  
  38.   
  39.         <fragment  
  40.             android:id="@+id/navigation_drawer"  
  41.             android:name="net.yanzm.navigationdrawersample.NavigationDrawerFragment"  
  42.             android:layout_width="@dimen/navigation_drawer_width"  
  43.             android:layout_height="match_parent"  
  44.             tools:layout="@layout/fragment_navigation_drawer" />  
  45.   
  46.     </android.support.design.internal.ScrimInsetsFrameLayout>  
  47.   
  48. </android.support.v4.widget.DrawerLayout>  
values-v21/styles.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <style name="AppTheme" parent="BaseTheme">  
  4.         <item name="android:windowDrawsSystemBarBackgrounds">true</item>  
  5.         <item name="android:statusBarColor">@android:color/transparent</item>  
  6.     </style>  
  7. </resources>  



2015年6月12日金曜日

SharedPrefenreces の値変更 + RxAndroid で状態の変更を
バックグラウンドのActivityに伝える

前回のエントリ「状態の変更をバックグラウンドのActivityに伝える方法はどれがいいんだろう?」の 3) を RxAndroid を使って実装してみました。

onStart() で値をチェックするのではなく、SharedPreferences の値が true になったイベントと起動時の処理を同じストリームになるようにしてみました。
  1. public class MainActivity extends Activity {  
  2.   
  3.     @InjectView(R.id.list)  
  4.     ListView listView;  
  5.   
  6.     private Subscription subscription = Subscriptions.empty();  
  7.   
  8.     private static final String FAVORITE_PREF_KEY = "favorite_changed";  
  9.   
  10.     @Override  
  11.     protected void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         setContentView(R.layout.activity_main);  
  14.         ButterKnife.inject(this);  
  15.   
  16.         final SharedPreferences pref = getSharedPreferences("reload_flag", MODE_PRIVATE);  
  17.   
  18.         final Observable<List<String>> observable = ContentObservable.fromSharedPreferencesChanges(pref)  
  19.                 .filter(new Func1<String, Boolean>() {  
  20.                     @Override  
  21.                     public Boolean call(String prefKey) {  
  22.                         return FAVORITE_PREF_KEY.equals(prefKey) && pref.getBoolean(prefKey, false);  
  23.                     }  
  24.                 })  
  25.                 .startWith(FAVORITE_PREF_KEY)  
  26.                 .flatMap(new Func1<String, Observable<List<String>>>() {  
  27.                     @Override  
  28.                     public Observable<List<String>> call(String s) {  
  29.                         return Observable.create(new Observable.OnSubscribe<List<String>>() {  
  30.                             @Override  
  31.                             public void call(Subscriber<? super List<String>> subscriber) {  
  32.                                 try {  
  33.                                     subscriber.onNext(getDataFromServer());  
  34.                                 } catch (Exception e) {  
  35.                                     subscriber.onError(e);  
  36.                                 }  
  37.                             }  
  38.                         }).subscribeOn(Schedulers.newThread());  
  39.                     }  
  40.                 });  
  41.   
  42.         subscription = AppObservable.bindActivity(this, observable)  
  43.                 .subscribe(new Subscriber<List<String>>() {  
  44.                     @Override  
  45.                     public void onCompleted() {  
  46.                     }  
  47.   
  48.                     @Override  
  49.                     public void onError(Throwable e) {  
  50.                         e.printStackTrace();  
  51.                         pref.edit().putBoolean(FAVORITE_PREF_KEY, false).apply();  
  52.                     }  
  53.   
  54.                     @Override  
  55.                     public void onNext(List<String> data) {  
  56.                         listView.setAdapter(new ArrayAdapter<>(MainActivity.this,  
  57.                                 android.R.layout.simple_list_item_1, data));  
  58.                         pref.edit().putBoolean(FAVORITE_PREF_KEY, false).apply();  
  59.                     }  
  60.                 });  
  61.     }  
  62.   
  63.     private List<String> getDataFromServer() throws Exception {  
  64.         // mock  
  65.         Thread.sleep(1000);  
  66.   
  67.         final Calendar c = Calendar.getInstance();  
  68.   
  69.         List<String> data = new ArrayList<>();  
  70.         for (int i = 0; i < 20; i++) {  
  71.             data.add(i + " : " + DateFormat.format("hh時mm分ss秒", c));  
  72.         }  
  73.         return data;  
  74.     }  
  75.   
  76.     @Override  
  77.     protected void onDestroy() {  
  78.         subscription.unsubscribe();  
  79.         super.onDestroy();  
  80.     }  
  81.   
  82.     @OnItemClick(R.id.list)  
  83.     void onItemClick(int position) {  
  84.         Intent intent = new Intent(this, SubActivity.class);  
  85.         startActivity(intent);  
  86.     }  
  87. }  
  1. public class SubActivity extends Activity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_sub);  
  7.         ButterKnife.inject(this);  
  8.     }  
  9.   
  10.     @OnClick(R.id.favorite_button)  
  11.     void onFavoriteButtonClicked() {  
  12.         // サーバー通信とかして、状態を変更できたとする  
  13.   
  14.         final SharedPreferences pref = getSharedPreferences("reload_flag", MODE_PRIVATE);  
  15.         pref.edit().putBoolean("favorite_changed"true).apply();  
  16.     }  
  17. }  


SubActivity でボタンをクリックすると、MainActivity でリロードが走ります。



状態の変更をバックグラウンドのActivityに伝える方法はどれがいいんだろう?

Activity A
- リストを持つ、リストの項目には favorite ボタンがある
- リストの項目をタップすると Activity B に遷移する

Activity B
- favorite ボタンがある

このとき、Activity B で favorite ボタンの状態が変わったことを Activity A に伝えて Activity A の見た目を変えたい

Activity A に伝える方法は何が最適なんだろう?

1) otto/EventBus 系
2) BroadcastReceiver
3) フラグ(SharedPreferenceとか)を書き換えて、onStartでリロードをかける
4) onActivityResult でリロードをかける
5) Service で Binding(Serviceにキャッシュさせて onStart で毎回 Service から取得)
6) CursorLoader(cursor.registerContentObserver() を使って状態を監視している)
7) その他


悩ましい...

1)

otto/EventBus 系のエントリって、onResume() や onStart() で register して onPause() や onStop() で unregister しているものが多い、この場合 Activity A はバックグラウンドに回ってしまうので、使用例として適切ではないのかもしれない?
ちなみに EventBus の HOWTO では onStart() で register して onStop() で unregister している。
otto のサンプル では onResume() で register して onStop() で unregister している。

2)

コードが見づらくなりそう?
(バックスタックの Activity を全部消したいときとかに使ってるらしい)
これも onResume() で register して onPause() で unregister するのがセオリーなんだろうなと思うんだけど、https://developer.android.com/training/run-background-service/report-status.html のサンプルは onCreate() で register して onDestroy() で unregister してた。ただわりと前のサンプルなのでなんとも言えない。

3)

わりと素直にかけそうだけど、ルールが見えづらいかも

4)

これのために startActivityForResult にするのもどうなんだろう

5)

これだけのために Service かー

6)

ContentProvider のデータを表示するときはこれが楽。ただし、ローカルデータだけならいいけど、サーバーが絡むと同期問題が...