2015年12月27日日曜日

Android 5.0未満では StateListDrawable に追加した Drawable の colorFilter は適用されない

5.0 未満では次のようなコードを実行しても赤色になりませんでしたが、5.0以降で実行すると赤色になります。 Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_launcher); drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{}, drawable); setBackground(stateListDrawable);
- 解説

原因は DrawableContainer の selectDrawable() での処理です。 5.0 から ColorFilter や tint を考慮するようになり、state に対応する Drawable に明示的に ColorFilter や tint をセットするようになりました。

4.4.0 の DrawableContainer.selectDrawable()
306 public boolean selectDrawable(int idx) { ... 336 if (d != null) { 337 d.mutate(); 338 if (mDrawableContainerState.mEnterFadeDuration > 0) { 339 mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; 340 } else { 341 d.setAlpha(mAlpha); 342 } 343 d.setVisible(isVisible(), true); 344 d.setDither(mDrawableContainerState.mDither); 345 d.setColorFilter(mColorFilter); 346 d.setState(getState()); 347 d.setLevel(getLevel()); 348 d.setBounds(getBounds()); 349 d.setLayoutDirection(getLayoutDirection()); 350 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored); 351 } 5.0.1 の DrawableContainer.selectDrawable()
412 public boolean selectDrawable(int idx) { ... 442 if (d != null) { 443 d.mutate(); 444 if (mDrawableContainerState.mEnterFadeDuration > 0) { 445 mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; 446 } else if (mHasAlpha) { 447 d.setAlpha(mAlpha); 448 } 449 if (mDrawableContainerState.mHasColorFilter) { 450 // Color filter always overrides tint. 451 d.setColorFilter(mDrawableContainerState.mColorFilter); 452 } else { 453 if (mDrawableContainerState.mHasTintList) { 454 d.setTintList(mDrawableContainerState.mTintList); 455 } 456 if (mDrawableContainerState.mHasTintMode) { 457 d.setTintMode(mDrawableContainerState.mTintMode); 458 } 459 } 460 d.setVisible(isVisible(), true); 461 d.setDither(mDrawableContainerState.mDither); 462 d.setState(getState()); 463 d.setLevel(getLevel()); 464 d.setBounds(getBounds()); 465 d.setLayoutDirection(getLayoutDirection()); 466 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);

- 5.0 未満でどうするか

以下のコードでは、デフォルト用の Drawable からタップしたとき用の ColorFilter がかかった Drawable を用意し、StateListDrawable を構成して背景にセットしています。 このコードを 5.0 以降で実行すると思ったように動きますが、5.0 未満だとタップしたときに色が変わりません。 Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_launcher); Drawable pressedDrawable = drawable.getConstantState().newDrawable().mutate(); pressedDrawable.setColorFilter(pressedColor, PorterDuff.Mode.SRC_ATOP); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable); stateListDrawable.addState(new int[]{}, drawable); setBackground(stateListDrawable); しょうがないので、http://stackoverflow.com/questions/7979440/android-cloning-a-drawable-in-order-to-make-a-statelistdrawable-with-filters にあるように次のような StateListDrawable の subclass を用意します。 private static class PressedStateListDrawable extends StateListDrawable { private final int pressedColor; public PressedStateListDrawable(Drawable drawable, int pressedColor) { super(); this.pressedColor = pressedColor; addState(new int[]{android.R.attr.state_pressed}, drawable); addState(new int[]{}, drawable); } @Override protected boolean onStateChange(int[] states) { boolean isPressed = false; for (int state : states) { if (state == android.R.attr.state_pressed) { isPressed = true; break; } } if (isPressed) { setColorFilter(pressedColor, PorterDuff.Mode.SRC_ATOP); } else { clearColorFilter(); } return super.onStateChange(states); } @Override public boolean isStateful() { return true; } } setBackground(new PressedStateListDrawable(drawable, pressedColor));


2015年11月3日火曜日

モジュール間の Product Flavor を同期させる

app モジュール('com.android.application')と api モジュール('com.android.library')があり、それぞれに staging と production flavor があるとする。

app モジュールの flavor が staging なら api モジュールの staging flavor を使い、
app モジュールの flavor が production なら api モジュールの production flavor を使いたい。

この場合、build.gradle に次のような設定を行う。

api モジュールの build.gradle apply plugin: 'com.android.library' configurations { stagingDebugCompile stagingReleaseCompile productionDebugCompile productionReleaseCompile } android { ... publishNonDefault true productFlavors { production { ... } staging { ... } } }

app モジュールの build.gradle apply plugin: 'com.android.application' configurations { stagingDebugCompile stagingReleaseCompile productionDebugCompile productionReleaseCompile } android { ... productFlavors { production { ... } staging { ... } } } dependencies { ... stagingDebugCompile project(path: ':api', configuration: 'stagingDebug') stagingReleaseCompile project(path: ':api', configuration: 'stagingRelease') productionDebugCompile project(path: ':api', configuration: 'productionDebug') productionReleaseCompile project(path: ':api', configuration: 'productionRelease') }

2015年10月29日木曜日

Android ビルド時に git のブランチ名を埋め込む方法

ざきさんに教えてもらった。ありがとうざきさん。 defaultConfig { ... buildConfigField "String", "GIT_BRANCH", "\"${'git rev-parse --abbrev-ref HEAD'.execute([], project.rootDir).text.trim()}\"" } とすると、コードから BuildConfig.GIT_BRANCH でブランチ名を取得できる。


2015年9月15日火曜日

AppCompat でカラーの Raised Button を設定する正しい方法

http://www.google.com/design/spec/components/buttons.html#buttons-flat-raised-buttons

まとめ
  • ボタンの文字は android:textAppearanceButton(文字サイズは 14sp、文字色は textColorPrimary)
  • ボタンの色は colorButtonNormal
    • 4以下にも適用される
    • テーマ直下の属性なので、全部のボタンに適用されてしまうのが難点

1. 背景が Light でボタンのテキストが Dark <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorButtonNormal">#2196F3</item> </style>

2. 背景が Light でボタンのテキストが Dark(Disabled 時はグレー) <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorButtonNormal">@color/button_color</item> </style> res/color/button_color.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#1E000000" android:state_enabled="false" /> <item android:color="#2196F3" /> </selector>

3. 背景が Dark でボタンのテキストが Light <style name="AppTheme" parent="Theme.AppCompat"> <item name="colorButtonNormal">#2196F3</item> </style>

4. 背景が Dark でボタンのテキストが Light(Disabled 時はグレー) <style name="AppTheme" parent="Theme.AppCompat"> <item name="colorButtonNormal">@color/button_color</item> </style> res/color/button_color.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#1EFFFFFF" android:state_enabled="false" /> <item android:color="#2196F3" /> </selector>

5. 背景が Light でボタンのテキストが Light <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorButtonNormal">#2196F3</item> <item name="android:textAppearanceButton">@style/TextAppearance.Button</item> </style> <style name="TextAppearance.Button" parent="Base.TextAppearance.AppCompat.Button"> <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> </style>

6. 背景が Light でボタンのテキストが Light(Disabled 時はグレー) <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorButtonNormal">@color/button_color</item> <item name="android:textAppearanceButton">@style/TextAppearance.Button</item> </style> <style name="TextAppearance.Button" parent="Base.TextAppearance.AppCompat.Button"> <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> </style> res/color/button_color.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#1E000000" android:state_enabled="false" /> <item android:color="#2196F3" /> </selector>

7. 背景が Light でボタンのテキストが Light(Disabled 時は文字もグレー) <style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorButtonNormal">@color/button_color</item> <item name="android:textAppearanceButton">@style/TextAppearance.Button</item> </style> <style name="TextAppearance.Button" parent="Base.TextAppearance.AppCompat.Button"> <item name="android:textColor">@color/button_text_color</item> </style> res/color/button_color.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#1E000000" android:state_enabled="false" /> <item android:color="#2196F3" /> </selector> res/color/button_text_color.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/primary_text_disabled_material_light" android:state_enabled="false" /> <item android:color="@color/primary_text_default_material_dark" /> </selector>


解説

Widget.AppCompat.Button

values.xml <style name="Widget.AppCompat.Button" parent="Base.Widget.AppCompat.Button"/>

Base.Widget.AppCompat.Button

values.xml <style name="Base.Widget.AppCompat.Button" parent="android:Widget"> <item name="android:background">@drawable/abc_btn_default_mtrl_shape</item> <item name="android:textAppearance">?android:attr/textAppearanceButton</item> <item name="android:minHeight">48dip</item> <item name="android:minWidth">88dip</item> <item name="android:focusable">true</item> <item name="android:clickable">true</item> <item name="android:gravity">center_vertical|center_horizontal</item> </style> values-v21.xml <style name="Base.Widget.AppCompat.Button" parent="android:Widget.Material.Button"/> <style name="Widget.Material.Button"> <item name="background">@drawable/btn_default_material</item> <item name="textAppearance">?attr/textAppearanceButton</item> <item name="minHeight">48dip</item> <item name="minWidth">88dip</item> <item name="stateListAnimator">@anim/button_state_list_anim_material</item> <item name="focusable">true</item> <item name="clickable">true</item> <item name="gravity">center_vertical|center_horizontal</item> </style> これを見ると、ボタンの文字は ?attr/textAppearanceButton を参照していることがわかる


textAppearanceButton

values.xml <style name="Theme.AppCompat" parent="Base.Theme.AppCompat"/> <style name="Base.Theme.AppCompat" parent="Base.V7.Theme.AppCompat"> <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat"> <item name="android:textAppearanceButton">@style/TextAppearance.AppCompat.Button</item> ... </style> <style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light"> <item name="android:textAppearanceButton">@style/TextAppearance.AppCompat.Button</item> </style> <style name="TextAppearance.AppCompat.Button" parent="Base.TextAppearance.AppCompat.Button"/> <style name="Base.TextAppearance.AppCompat.Button"> <item name="android:textSize">@dimen/abc_text_size_button_material</item> <item name="textAllCaps">true</item> <item name="android:textColor">?android:textColorPrimary</item> </style> values-v14.xml <style name="Base.TextAppearance.AppCompat.Button"> <item name="android:textSize">@dimen/abc_text_size_button_material</item> <item name="android:textAllCaps">true</item> <item name="android:textColor">?android:textColorPrimary</item> </style> 文字サイズは @dimen/abc_text_size_button_material (=14sp)、文字色は ?attr/textColorPrimary である。

values-v21.xml <style name="Base.Theme.AppCompat" parent="Base.V21.Theme.AppCompat"/> <style name="Base.V21.Theme.AppCompat" parent="Base.V7.Theme.AppCompat"> ... </style> <style name="Base.TextAppearance.AppCompat.Button" parent="android:TextAppearance.Material.Button"/> styles_material.xml <style name="TextAppearance.Material.Widget.Button" parent="TextAppearance.Material.Button" /> <style name="TextAppearance.Material.Button"> <item name="textSize">@dimen/text_size_button_material</item> <item name="fontFamily">@string/font_family_button_material</item> <item name="textAllCaps">true</item> <item name="textColor">?attr/textColorPrimary</item> </style> 文字サイズは @dimen/text_size_button_material (=14sp)、文字色は ?attr/textColorPrimary である。


btn_default_material <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?attr/colorControlHighlight"> <item android:drawable="@drawable/btn_default_mtrl_shape" /> </ripple>

btn_default_mtrl_shape <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- Used as the canonical button shape. --> <inset xmlns:android="http://schemas.android.com/apk/res/android" android:insetLeft="@dimen/button_inset_horizontal_material" android:insetTop="@dimen/button_inset_vertical_material" android:insetRight="@dimen/button_inset_horizontal_material" android:insetBottom="@dimen/button_inset_vertical_material"> <shape android:shape="rectangle"> <corners android:radius="@dimen/control_corner_material" /> <solid android:color="?attr/colorButtonNormal" /> <padding android:left="@dimen/button_padding_horizontal_material" android:top="@dimen/button_padding_vertical_material" android:right="@dimen/button_padding_horizontal_material" android:bottom="@dimen/button_padding_vertical_material" /> </shape> </inset> これを見ると ?attr/colorButtonNormal が solid の color に指定されていることがわかる


AndroidJUnitRunner で Toast や PopupWindow を表示するには

Toast、Dialog、PopupWindow など別の Window を使う操作をテストメソッド内で行うと、Handler が作れないと言われてエラーになります (PopupWindow を内包したユーティリティクラスのテストで困りました)。

例えば以下のテストを実行すると、Toast 部分で

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

になります。 @RunWith(AndroidJUnit4.class) @LargeTest public class SimpleMenuPopupTest { @Test public void showToastTest() { Context context = InstrumentationRegistry.getTargetContext(); Toast.makeText(context, "Hello Android.", Toast.LENGTH_SHORT).show(); } }
このような Handler の生成が必要な操作では Instrumentation.runOnMainSync() を利用します。 @RunWith(AndroidJUnit4.class) @LargeTest public class SimpleMenuPopupTest { @Test public void showToastTest() { InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { Context context = InstrumentationRegistry.getTargetContext(); Toast.makeText(context, "Hello Android.", Toast.LENGTH_SHORT).show(); } }); } } これでエラーにならなくなりました。


Dialog の場合、InstrumentationRegistry.getTargetContext() では Window Token が無いと言われてエラーになるので、ActivityTestRule から取得した Activity とかを使います。

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application


Espresso のコードはテストメソッドの方に書きます。 @RunWith(AndroidJUnit4.class) @LargeTest public class SimpleMenuPopupTest { @Test public void showPopupTest() { InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { Context context = activityRule.getActivity(); new AlertDialog.Builder(context) .setMessage("Hello Android.") .show(); } }); onView(withText("Hello Android.")).check(matches(isDisplayed())); } }


参考


2015年9月10日木曜日

Butter Knife のフィールドやメソッドが Lint に Unused declaration と言われないようにする方法

以下のように apt のプラグインを追加します。
https://bitbucket.org/hvisser/android-apt
このプラグインを追加すると、gradle のソースフォルダとして認識される場所に Butter Knife で生成されるコードが格納されます。
そのため、Butter Knife で生成されたクラスも Lint から見えるようになり、Lint が怒らなくなります。やったー。


ルートプロジェクトの build.gradle buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.3.1' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // これを追加 } }

モジュールの build.gradle apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' // これを追加 ...


minSdkVersion が自分のアプリより大きいライブラリを利用する

自分のアプリの minSdkVersion が 10 のときに、minSdkVersion が 14 のライブラリを利用しようとすると、以下のようなエラーがでてビルドに失敗します。
> Manifest merger failed : uses-sdk:minSdkVersion 10 cannot be smaller than version 14 declared in library [利用しているライブラリ] /.../AndroidManifest.xml
   Suggestion: use tools:overrideLibrary="利用しているライブラリ" to force usage

エラーメッセージの Suggestion にある通りに tools:overrideLibrary="利用しているライブラリ" を追加すればいいのですが、問題は追加する場所です。
uses-sdk タグに追加します。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="net.yanzm.sample"> <uses-sdk tools:overrideLibrary="利用しているライブラリ" /> ... </manifest> 複数ある場合は , で区切って記述します。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="net.yanzm.sample"> <uses-sdk tools:overrideLibrary="利用しているライブラリ1, 利用しているライブラリ2, 利用しているライブラリ3" /> ... </manifest>


ライブラリで指定された AndroidManifest の属性を置き換える

利用しているライブラリで android:allowBackup="true" が指定されているときに、自分のアプリで android:allowBackup="false" を指定するとマニフェストのマージに失敗してビルドエラーになります。
> Manifest merger failed : Attribute application@allowBackup value=(false) from AndroidManifest.xml:22:9-36
   is also present at [利用しているライブラリ] AndroidManifest.xml:9:18-44 value=(true)
   Suggestion: add 'tools:replace="android:allowBackup"' to  element at AndroidManifest.xml:21:5-83:19 to override

エラーメッセージの Suggestion にあるように tools:replace="android:allowBackup" をつけるとアプリ側の設定が利用されエラーが解消します。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="net.yanzm.sample"> <application android:allowBackup="false" tools:replace="android:allowBackup" ...> ... </application> </manifest> その他の tools 属性なども Attributes markers examples に載っています。


2015年9月9日水曜日

Google App Engine / Go で Google Cloud Storage に画像をアップロードする

How to upload image to Google Cloud Storage with Google App Engine / Go.

この情報は2015年9月9日時点のものです。appengine.Context, context.Context 問題は過渡期のようなので今後インタフェースが変わる可能性があります。

File API が終了になったので調べたけどあんまり情報がなく苦労したのでメモとして残しておく

storage#NewWriter に渡す Context は appengine#Context ではなく context#Context なので次のようにするとエラーになります。

import ( "appengine" "google.golang.org/cloud/storage" ... ) func uploadImage(w http.ResponseWriter, r *http.Request) { ... c := appengine.NewContext(r) wc := storage.NewWriter(c, bucketName, fileName) wc.ContentType = "image/jpg" if _, err := wc.Write(data); err != nil { ... } ... } google.golang.org/appengine の NewContext は context#Context を返すため、こちらを利用します。このワークアラウンドは https://github.com/golang/oauth2/#app-engine を参考にしています。 import ( "appengine" "fmt" "io/ioutil" "net/http" "strings" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" newappengine "google.golang.org/appengine" newurlfetch "google.golang.org/appengine/urlfetch" "google.golang.org/cloud" "google.golang.org/cloud/storage" ) func uploadImage(w http.ResponseWriter, r *http.Request) { file, fileHeader, err := r.FormFile("image_file") if err != nil { fmt.Fprint(w, "no image") // no image return } defer file.Close() data, err := ioutil.ReadAll(file) if err != nil { fmt.Fprint(w, "cloud not upload image") return } var mimeType string if strings.HasSuffix(filename, ".png") { mimeType = "image/png" } else if strings.HasSuffix(filename, ".jpeg") { mimeType = "image/jpg" } else { mimeType = "image/jpg" } bucketName := "mybucketname" fileName := fileHeader.Filename c := appengine.NewContext(r) ctx := newappengine.NewContext(r) hc := &http.Client{ Transport: &oauth2.Transport{ Source: google.AppEngineTokenSource(ctx, storage.ScopeFullControl), Base: &newurlfetch.Transport{Context: ctx}, }, } ctx2 := cloud.NewContext(appengine.AppID(c), hc) wc := storage.NewWriter(ctx2, bucketName, fileName) wc.ContentType = mimeType if _, err := wc.Write(data); err != nil { fmt.Fprint(w, "cloud not upload image") return } if err := wc.Close(); err != nil { return }
google.golang.org/cloud/storage などは GOPAH を設定して $ go get -u google.golang.org/cloud/storage でインストールしておきます


参考


2015年9月7日月曜日

FragmentArgs 使ってみた

FragmentArgs

Fragment 生成時に値を渡すには、引数付きのコンストラクタを用意するのではなく、Fragment.setArguments() で値を渡すのがベストプラクティスです。

FragmentArgs はアノテーションを付けたフィールドからFragmentのビルダークラスを生成してくれるライブラリです。

以下の内容は2015年9月7日時点の状態をもとにしています。



設定

Project の build.gradle buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.3.1' // apt を使うので以下が必要 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.7' } } allprojects { repositories { jcenter() } } app の build.gradle apply plugin: 'com.android.application' // apt を使うので以下が必要 apply plugin: 'com.neenbedankt.android-apt' android { ... } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // 以下が必要 compile 'com.hannesdorfmann.fragmentargs:annotation:2.1.2' apt 'com.hannesdorfmann.fragmentargs:processor:2.1.2' }

使い方 public class MainFragment extends Fragment { @Arg int userId; @Arg String name; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // @Arg を付けたフィールドに getArguments() から値が代入される FragmentArgs.inject(this); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Toast.makeText(getActivity(), "userId = " + userId + ", name = " + name, Toast.LENGTH_SHORT).show(); } } public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final MainFragment f = new MainFragmentBuilder("Android", 100) .build(); // 以下のメソッドも生成される // final MainFragment f = MainFragmentBuilder.newMainFragment("Android", 100); getSupportFragmentManager() .beginTransaction() .replace(android.R.id.content, f) .commit(); } }

Library Project での使い方

Library Project の build.gradle apply plugin: 'com.android.library' // apt を使うので以下が必要 apply plugin: 'com.neenbedankt.android-apt' // Library Project では以下を追加しないとコンパイルエラーになる apt { arguments { fragmentArgsLib true } } android { ... } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // 以下が必要 compile 'com.hannesdorfmann.fragmentargs:annotation:2.1.2' apt 'com.hannesdorfmann.fragmentargs:processor:2.1.2' } 2015年9月7日時点の FragmentArgs の README だと fragmentArgsLib = true のように = がついてますが、android-apt:1.7 ではメソッドになったので = が必要ないです。

public class MainLibraryFragment extends Fragment { @Arg int userId; @Arg String name; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Library Project のときは生成されたビルダークラスの injectArguments() を利用する必要がある MainLibraryFragmentBuilder.injectArguments(this); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Toast.makeText(getActivity(), "userId = " + userId + ", name = " + name, Toast.LENGTH_SHORT).show(); } }


2015年8月3日月曜日

Android JUnit 4 形式のテストにする

InstrumentationTestRunner は JUnit 3 しかサポートしていませんが、Android Testing Support Library に含まれる AndroidJUnitRunner を使うと JUnit 4-compatible なテストを実行することができます。
(JUnit 3 と JUnit 4.10 までの JUnit 4 互換)

Android 2.2 (API Level 8) 以上が必要です。

▪️ Setup dependencies { androidTestCompile 'com.android.support.test:runner:0.3' // to use JUnit 4 rules androidTestCompile 'com.android.support.test:rules:0.3' } android { defaultConfig { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } }

▪️ Usage

- JUnit 4 で実装されたテストクラスには @RunWith(AndroidJUnit4.class) をつける
- テスト対象メソッドには @Test をつける
- setUp() を Override し、public に変更し @Before をつける
- tearDown() を Override し、public に変更し @After をつける
- 必要に応じて AndroidTestCase.setContext(InstrumentationRegistry.getContext()) や InstrumentationTestCase.injectInstrumentation(InstrumentationRegistry.getInstrumentation()) を setUp() で行う

@RunWith(AndroidJUnit4.class) public class SimpleTest extends AndroidTestCase { @Before public void setUp() throws Exception { super.setUp(); setContext(InstrumentationRegistry.getContext()); } @Test public void testSample() { assertNotNull(getContext()); } @After public void tearDown() throws Exception { super.tearDown(); } } - InstrumentationRegistryでテスト実行時の情報を取得できる
- InstrumentationRegistry.getContext() : instrumentation のパッケージContext
- InstrumentationRegistry.getTargetContext() : テスト対象のアプリケーションのContext
- InstrumentationRegistry.getInstrumentation() : 実行中の instrumentation


▪️ Test filtering

- @RequiresDevice : 実機でのみ実行(エミュレータでは実行しない)
- @SdkSuppress : 指定された API Level より低い場合実行しない、@SdkSuppress(minSdkVersion=21) など
- @SmallTest, @MediumTest, @LargeTest : テストにどのくらいかかるかのクラス分け


▪️ 参考

- http://developer.android.com/tools/testing-support-library/index.html


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系でカーソルが白になります(解説参照のこと)。 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="editTextStyle">@style/EditTextStyle</item> </style> <style name="EditTextStyle" parent="Widget.AppCompat.EditText"> <item name="android:background">#ccccff</item> </style> </resources>
背景を変えたいだけなら android:editTextBackground と editTextBackground 両方を指定するほうが、カーソルの色を維持できます。 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:editTextBackground">@drawable/blue</item> <item name="editTextBackground">@drawable/blue</item> </style> <drawable name="blue">#ccccff</drawable> </resources>


解説

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


2. android:editTextStyle を指定した状態 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:editTextStyle">@style/EditTextStyle</item> </style> <style name="EditTextStyle" parent="android:Widget.EditText"> <item name="android:background">#ffffff</item> </style> </resources>


3. editTextStyle を指定した状態 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="editTextStyle">@style/EditTextStyle</item> </style> <style name="EditTextStyle" parent="android:Widget.EditText"> <item name="android:background">#ffcccc</item> </style> </resources>

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


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

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

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


android:editTextBackground

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


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


6. editTextBackground を指定した状態 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="editTextBackground">@drawable/cyan</item> </style> <drawable name="cyan">#00ffff</drawable> </resources>


7. android:editTextBackground と editTextBackground を両方指定した状態 <resources> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:editTextBackground">@drawable/blue</item> <item name="editTextBackground">@drawable/blue</item> </style> <drawable name="blue">#ccccff</drawable> </resources>


2015年6月14日日曜日

android.support.annotation を活用する

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

@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 に対して取りうる値のセットが定義されたアノテーションを作ることができます。
public static final int SIZE_L = 0; public static final int SIZE_M = 1; public static final int SIZE_S = 2; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface Size { } @Size public abstract int getSize(); public abstract void setSize(@Size int size); public static final String FORMAT_XML = "xml"; public static final String FORMAT_JSON = "json"; @Retention(RetentionPolicy.SOURCE) @StringDef({FORMAT_XML, FORMAT_JSON}) public @interface FORMAT { } public abstract void fetchData(@FORMAT String format); @IntDef は enum だけでなく flag としても使うことができます。 public static final int SHOW_TITLE = 0x1; public static final int SHOW_SUB_TITLE = 0x2; public static final int SHOW_ICON = 0x4; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { SHOW_TITLE, SHOW_SUB_TITLE, SHOW_ICON, }) public @interface TitleOptions { } 詳しくは 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 は必要なパーミッションを明示するためのアノテーションです。 @RequiresPermission(Manifest.permission.NFC) 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で指定して使えます。 <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/action_bar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> <android.support.design.internal.ScrimInsetsFrameLayout android:id="@+id/navigation_container" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:background="#ccc" android:fitsSystemWindows="true" app:insetForeground="#4000"> <fragment android:id="@+id/navigation_drawer" android:name="net.yanzm.navigationdrawersample.NavigationDrawerFragment" android:layout_width="@dimen/navigation_drawer_width" android:layout_height="match_parent" tools:layout="@layout/fragment_navigation_drawer" /> </android.support.design.internal.ScrimInsetsFrameLayout> </android.support.v4.widget.DrawerLayout> values-v21/styles.xml <?xml version="1.0" encoding="utf-8"?> <resources> <style name="AppTheme" parent="BaseTheme"> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:statusBarColor">@android:color/transparent</item> </style> </resources>


2015年6月12日金曜日

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

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

onStart() で値をチェックするのではなく、SharedPreferences の値が true になったイベントと起動時の処理を同じストリームになるようにしてみました。 public class MainActivity extends Activity { @InjectView(R.id.list) ListView listView; private Subscription subscription = Subscriptions.empty(); private static final String FAVORITE_PREF_KEY = "favorite_changed"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); final SharedPreferences pref = getSharedPreferences("reload_flag", MODE_PRIVATE); final Observable<List<String>> observable = ContentObservable.fromSharedPreferencesChanges(pref) .filter(new Func1<String, Boolean>() { @Override public Boolean call(String prefKey) { return FAVORITE_PREF_KEY.equals(prefKey) && pref.getBoolean(prefKey, false); } }) .startWith(FAVORITE_PREF_KEY) .flatMap(new Func1<String, Observable<List<String>>>() { @Override public Observable<List<String>> call(String s) { return Observable.create(new Observable.OnSubscribe<List<String>>() { @Override public void call(Subscriber<? super List<String>> subscriber) { try { subscriber.onNext(getDataFromServer()); } catch (Exception e) { subscriber.onError(e); } } }).subscribeOn(Schedulers.newThread()); } }); subscription = AppObservable.bindActivity(this, observable) .subscribe(new Subscriber<List<String>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { e.printStackTrace(); pref.edit().putBoolean(FAVORITE_PREF_KEY, false).apply(); } @Override public void onNext(List<String> data) { listView.setAdapter(new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data)); pref.edit().putBoolean(FAVORITE_PREF_KEY, false).apply(); } }); } private List<String> getDataFromServer() throws Exception { // mock Thread.sleep(1000); final Calendar c = Calendar.getInstance(); List<String> data = new ArrayList<>(); for (int i = 0; i < 20; i++) { data.add(i + " : " + DateFormat.format("hh時mm分ss秒", c)); } return data; } @Override protected void onDestroy() { subscription.unsubscribe(); super.onDestroy(); } @OnItemClick(R.id.list) void onItemClick(int position) { Intent intent = new Intent(this, SubActivity.class); startActivity(intent); } } public class SubActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sub); ButterKnife.inject(this); } @OnClick(R.id.favorite_button) void onFavoriteButtonClicked() { // サーバー通信とかして、状態を変更できたとする final SharedPreferences pref = getSharedPreferences("reload_flag", MODE_PRIVATE); pref.edit().putBoolean("favorite_changed", true).apply(); } }

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



2015年5月19日火曜日

未選択状態を持つデータをどう表現するか悩んだ話、その2 - SharedPreferences -

前回 の続きです。


1. enum 版で SharedPreferences

SharedPreferences.Editor には残念ながら putSerializable() 的なものがありません。そのため、putInt() なりを使わざるを得ません。

enum 版では
・enum から int に変換して SharedPreferences に保存
・SharedPreferences から取得した int の値を enum に変換
という処理が必要になります。

@zaki50 さんの gist を参考に前回のコードに合わせました。zaki50 さんいつもありがとう。 private static final String PREF_SIZE_KEY = "pref_size_key"; public static void saveSize(@NonNull Context context, @Nullable Size size) { final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences.Editor editor = pref.edit(); if (size == null) { editor.remove(PREF_SIZE_KEY); } else { editor.putInt(PREF_SIZE_KEY, size.getValue()); } editor.apply(); } @Nullable public static Size getSavedSize(@NonNull Context context) { final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); if (pref.contains(PREF_SIZE_KEY)) { int value = pref.getInt(PREF_SIZE_KEY, Size.SIZE_L); return Size.fromIntValue(value); } return null; } public enum Size { ... private volatile static SparseArray<Size> intToEnum; public static Size fromIntValue(int value) { if (intToEnum == null) { final SparseArray<Size> sizeSparseArray = new SparseArray<>(); final Size[] values = values(); for (Size size : values) { sizeSparseArray.append(size.getValue(), size); } // おまけ。値の重複チェックをしておく if (sizeSparseArray.size() != values.length) { throw new IllegalStateException("duplicate values in Size enum"); } intToEnum = sizeSparseArray; } final Size size = intToEnum.get(value); if (size == null) { throw new IllegalArgumentException("invalid value: " + value); } return size; } } SharedPreferences から取得した int の値を enum に変換するために、新しく fromIntValue() を追加しています。



2. class 版で SharedPreferences

class で実装した方はこんな感じになります。
変換用のメソッドを新しく用意することなく、int 値からそのまま Size に変換しています(valueOf()については前回のコード参照)。 private static final String PREF_SIZE_KEY = "pref_size_key"; public static void saveSize(@NonNull Context context, @Nullable Size size) { final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences.Editor editor = pref.edit(); if (size == null) { editor.remove(PREF_SIZE_KEY); } else { editor.putInt(PREF_SIZE_KEY, size.getValue()); } editor.apply(); } @Nullable public static Size getSavedSize(@NonNull Context context) { final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); if (pref.contains(PREF_SIZE_KEY)) { @ValidSize int value = pref.getInt(PREF_SIZE_KEY, Size.SIZE_L); return Size.valueOf(value); } return null; } SharedPreferences から読み出した値が @ValidSize である保証がないので、そこをチェックしたかったら enum 版と同じようなコンバーターか値のバリデータを介す必要があります。 @Nullable public static Size getSavedSize(@NonNull Context context) { final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); if (pref.contains(PREF_SIZE_KEY)) { int value = pref.getInt(PREF_SIZE_KEY, Size.SIZE_L); return Size.convertIntValue(value); } return null; } private static class Size { ... public static Size convertIntValue(int value) { if (value < SIZE_L || value > SIZE_S) { return null; } return new Size(value); } }


saveSize()経由でしか保存しないようにするなら、厳密にチェックしてなくてもいいのかなという気はしますが。


未選択状態を持つデータをどう表現するか悩んだ話

*追記1:enum の場合について最後に追記しました。

*追記2:まずは int 型じゃねーよ、enumだろって vvakameさんに怒られたけど、もともとは、とあるプロジェクトでサーバーに意図しない値がきてるんだけど、、、みたいなことがあって、サーバーに渡す値を静的に制限するにはどうするのがいいのかな、というのが出発点だったのです。なんで最初が int かっつーと、そのときのコードが int だったからだよっ(つまり初心者がやりがちってこと)



例えば T シャツのサイズをユーザーに選択してもらう画面があったとします。
Tシャツのサイズは L, M, S で、サーバーに投げるときはそれぞれ int値 の 1, 2, 3 として渡します。

まずは int 型で、ってなりますよね。 private int size; @Override public void onSizeSelected(int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { send(size); } @POST("/tshirt-size") boolean send(@Query("size") int size); これだと、ユーザーが選択していない状態で送信ボタンを押すと 0 が送られてしまうので、メッセージを表示して送信をブロックしましょう。
そのためには未選択状態の値を定義しないといけません。よくあるのは -1 で、こんな感じになるでしょう。 private int size = -1; @Override public void onSizeSelected(int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(context, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size); } } 悪くないんですが、-1 を弾くよりサーバーに送るデータ を 1, 2, 3 に制限したほうがよさそうです。
そこで @IntDef を使って次のようにしてみます。 public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } private int size = -1; @Override public void onSizeSelected(@ValidSize int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size); // ここでエラーになる } } @POST("/tshirt-size") boolean send(@ValidSize int size) { return true; } これだと send(size); のところでエラーがでます。size が @ValidSize ではないからですね。
次のように値をチェックすればエラーは出なくなりますが、int から int 変換ですしどうもいまいちです。 @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { switch (size) { case SIZE_L: send(SIZE_L); break; case SIZE_M: send(SIZE_M); break; case SIZE_S: send(SIZE_S); break; } } } そこで @ValidSize の int 値を保持するクラスを用意してみます。
未設定かどうかを保持する boolean 値も持たせます。 private static class Size { public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } @ValidSize private int size; private boolean isValid = false; public void setSize(@ValidSize int size) { this.size = size; this.isValid = true; } @ValidSize public int getSize() { return size; } public boolean isValid() { return isValid; } } private Size size = new Size(); @Override public void onSizeSelected(@Size.ValidSize int size) { this.size.setSize(size); } @OnClick(R.id.send_button) void onSendButtonClicked() { // ここのチェックを強制できない if (!size.isValid()) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getSize()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; } これで不細工なswitch文にさよならできましたが、問題があります。
size.isValid() でチェックすることを利用側に強制できません。

そこで、null を未設定状態として扱うようにしてみます。 private static class Size { public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } public static Size valueOf(@ValidSize int size) { return new Size(size); } @ValidSize private final int size; private Size(@ValidSize int size) { this.size = size; } @ValidSize public int getValue() { return size; } } @Nullable private Size size = null; @Override public void onSizeSelected(@Size.ValidSize int size) { this.size = Size.valueOf(size); } @Override public void onSizeCleared() { this.size = null; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == null) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getValue()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; } これで null じゃないときに取得できる値を @Size.ValidSize に制限できます。

size に @Nullable をつければ、null チェックをしないで size.getValue() を呼び出そうとしたところで Lint の警告が出てくれます。
size に null を代入することでサイズ選択のクリアもできます。


未選択状態に特定の値を割り当てる場合、その値が絶対使われないならいいのですが、使われる場合もありえます。 例えば、透明度を含む色を選択してもらいたい場合では #ffffffff が -1 なので、-1を未選択状態に割り当てるのは不適当になります。
null を未選択状態に割り当てる方法はこういう場合にも適用できます。



追記 :

@vvakame から enum 使えよおらーって言われたので、@zaki50 さんの提案をもとに enum版も載せておきます。

前提として、send()に渡される引数の値を制限したいというのが目的です。 retrofit で引数のパラメータに enum を渡すと toString() の値が利用されるようで、なにかしらコンバーターを仕込まないといけなさそうです。 なので、send() に渡す値に @ValidSize をつけるのはそのままにしたいと思います。

こんな感じになります。ほぼ同じですね。
違いは、onSizeSelected() の引数が @ValidSize int size から Size になったので、この部分の値の制限が Lint からコンパイラになったという点と、 enum なので == で比較できるという点くらいでしょうか。
ちなみにこの書き方だと SIZE_L などを static import する必要があります。Ctrl + space2回 で候補がでます。 public enum Size { L(SIZE_L), M(SIZE_M), S(SIZE_S); @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { int SIZE_L = 1; int SIZE_M = 2; int SIZE_S = 3; } @ValidSize private final int size; Size(@ValidSize int size) { this.size = size; } @ValidSize public int getValue() { return size; } } @Nullable private Size size = null; @Override public void onSizeSelected(Size size) { this.size = size; } @Override public void onSizeCleared() { this.size = null; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == null) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getValue()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; }



2015年5月18日月曜日

カスタムDrawableで複雑なプログレスを作る

ProgressBar の indeterminate にカスタムDrawableを指定すると、draw() と onLevelChange() が呼ばれ続けます。 final CustomDrawable customDrawable = new CustomDrawable(); ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setIndeterminate(true); progressBar.setIndeterminateDrawable(customDrawable); ProgressBar に RotateDrawable をセットすると回転するのは、このonLevelChange() を利用して角度を変えているからです。 public class CustomDrawable extends Drawable { @Override public void draw(Canvas canvas) { // 繰り返し呼ばれる } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter cf) { } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override protected boolean onLevelChange(int level) { // 繰り返し呼ばれる // level は 0 〜 10000 return super.onLevelChange(level); } } この振る舞いは、Animatable を実装すると変わります。 onLevelChange() は呼ばれなくなり、そのままだと stop(), start() の後に draw() が1回だけ呼ばれます。 public class CustomDrawable extends Drawable implements Animatable { @Override public void draw(Canvas canvas) { } ... @Override public void start() { } @Override public void stop() { } @Override public boolean isRunning() { return false; } } このままでは draw() が連続して呼ばれないのでアニメーションになりません。そのため、まず start() で内部のアニメーションを開始します。アニメーションを開始したら invalidateSelf() や scheduleSelf() を呼びます。
また、draw() 内でもアニメーション中なら invalidateSelf() や scheduleSelf() を呼ぶようにします。これにより、アニメーション中は draw() が連続して呼ばれるようになります。 public class CustomDrawable extends Drawable implements Animatable { ... private static final Interpolator cubicBezierInterpolator = PathInterpolatorCompat.create(0.66f, 0.22f, 0.21f, 1f); private final int width; private final int height; private final RectF rectF; private final ValueAnimator valueAnimator; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path path = new Path(); @Override public int getIntrinsicWidth() { return width; } @Override public int getIntrinsicHeight() { return height; } public CustomDrawable(Context context) { float density = context.getResources().getDisplayMetrics().density; width = (int) (density * 100); height = (int) (density * 100); rectF = new RectF(density * 6, density * 6, density * 94, density * 94); valueAnimator = ValueAnimator.ofFloat(0f, 1f); valueAnimator.setDuration(2000); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setInterpolator(cubicBezierInterpolator); paint.setColor(Color.parseColor("#0D47A1")); paint.setStyle(Paint.Style.FILL); } @Override public void draw(Canvas canvas) { float factor = (float) valueAnimator.getAnimatedValue(); paint.setAlpha((int) (255 * (1f - factor))); path.reset(); path.moveTo(width * 0.5f, height * 0.5f); path.arcTo(rectF, -90, 360 * factor); path.moveTo(width * 0.5f, height * 0.5f); path.close(); canvas.drawPath(path, paint); if (isStarted()) { invalidateSelf(); } } @Override public void start() { if (isStarted()) { return; } final Animator animator = valueAnimator; animator.start(); invalidateSelf(); } @Override public void stop() { final Animator animator = valueAnimator; animator.end(); } @Override public boolean isRunning() { final Animator animator = valueAnimator; if (animator.isRunning()) { return true; } return false; } private boolean isStarted() { final Animator animator = valueAnimator; if (animator.isStarted()) { return true; } return false; } }



Animatable を実装しているクラスとして、AnimatedVectorDrawableAnimationDrawable があります。



2015年5月11日月曜日

全画面の Toast を表示する

Gravity の FILL_HORIZONTALFILL_GRAVITY を指定します。
private void showFullscreenToast(Context context, String message) { TextView tv = new TextView(context); tv.setText(message); tv.setTextColor(Color.WHITE); tv.setBackgroundColor(Color.parseColor("#99000000")); tv.setGravity(Gravity.CENTER); Toast toast = new Toast(context); toast.setGravity(Gravity.FILL_HORIZONTAL | Gravity.FILL_VERTICAL, 0, 0); toast.setView(tv); toast.show(); }

対応するToastクラスのコードは以下になります。

http://tools.oesf.biz/android-5.0.1_r1.0/xref/frameworks/base/core/java/android/widget/Toast.java#400 400 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { 401 mParams.horizontalWeight = 1.0f; 402 } 403 if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { 404 mParams.verticalWeight = 1.0f; 405 }


2015年4月11日土曜日

DroidCon Montreal に行ってきました。



DriodCon Japan 開催をもくろむ mhidaka と共に視察(半分観光)として DroidCon Montreal に行ってきました。

DroidCon Montreal

カナダで最初のDroidConということで、Welcome talk では Toronto じゃなくて Montreal だぜ、いぇい、というのりでした。
朝食と昼食が出たのですが、ホテルの朝食よりおいしかったです。
9時スタートに来てもらうには朝食は必須ですね。



セッションメモ

Keynote - An Open Source Advantage
Jake Wharton & Jesse Wilson (Square Inc.)

セッションスライド
https://speakerdeck.com/swankjesse/an-open-source-advantage-droidcon-mtl-2015

二人が交互に話すスタイルだった
オープンソースの利点と運営(メンテナンス)についての話
say no (nicely) が大事



Building First Class SDKs
Ty Smith (Twitter)

セッションスライド
https://speakerdeck.com/tysmith/droidcon-montreal-building-first-class-sdks-a-fabric-case-study



A Few "OK" Libraries
Jake Wharton (Square Inc.)

セッションスライド
https://speakerdeck.com/jakewharton/a-few-ok-libraries-droidcon-mtl-2015

OkHttp, Retrofit, Moshi で利用すべく、dataのinput/ouputのライブラリである
Okio https://github.com/square/okio を作ったという話

java.io.*の使いにくさについて滔々と語る
複数のコンセプトが含まれていて振る舞いに一貫性がないAPIは使いにくいし、わかりにくい

Okio は java.io.*, java.nio.*を補完するもので、主な interface は Source と Sink
Source の read() メソッドは1種類だけでシンプル
Source と Sink を継承した BufferedSource と BufferedSink が用意されており、これらを実装したクラスとして Buffer が用意されている

例えば File file = // ... Sink fileSink = Okio.sink(file); Sink gzipSink = new GzipSink(fileSink); BfferedSink bufferedSink = Okio.buffer(gzipSink); bufferedSink.writeUtf8("Hello, hello, hello!"); bufferedSink.close(); とすると、"Hello, hello, hello!" を gzip ものがファイルに書き込まれる File file = // ... Sink fileSink = Okio.sink(file); Sink gzipSink = new GzipSink(fileSink); Sha1Sink hashingSink = new Sha1Sink(gzipSink); BfferedSink bufferedSink = Okio.buffer(gzipSink); bufferedSink.writeUtf8("Hello, hello, hello!"); bufferedSink.close(); のように Sha1Sink を GzipSink の前に挟むと、"Hello, hello, hello!" の Sha1 を gzip したものがファイルに書き込まれる

OkHttp の RequestBody には void writeTo(Buffered sink) というメソッドがあり ResponseBody には BufferedSource source() というメソッドがある

Moshi は Okio を利用した JSON のライブラリ
streaming 用には JsonReder / JsonWriter を使う
JsonWriter writer = new JsonWriter(sink); JsonReader reader = new JsonReader(source); オブジェクトへのマッピングは JsonAdapter を使う
ちなみに reflection ベース

Retrofit の Converter として MoshiConverter を指定できる



HTTP in a Hostile World
Jesse Wilson

セッションスライド
https://speakerdeck.com/swankjesse/http-in-a-hostile-world-droidcon-mtl-2015

https://bugsnag.com/



Debug Builds: A New Hope
Matt Precious (Square Inc.)

セッションスライド
https://speakerdeck.com/mattprecious/debug-builds-a-new-hope-droidcon-mtl-2015

Daggerとか使って、Endpoints とかパラメータをいろいろ変えられるデバッグ用画面を作る話
つまり U+2020 の説明
https://github.com/JakeWharton/u2020

アプリケーションプロセスを再スタートさせるtips
デバッグビルドで根本的な状態(staging から production に変えるとか)を変えるためにだけ利用すべし
ProcessPhoenix.triggerRebirth(getContext()); ProcessPhoenix.java
https://gist.github.com/JakeWharton/9404647aa6a2b2818d22

Charles
http://www.charlesproxy.com/

スクリーンショット付きのバグレポートを簡単に作れるライブラリ Telescope https://github.com/mattprecious/telescope

つまり、U+2020 を Fork すべし!



Transition Your Development
Kevin Grant(Tumblr)

セッションスライド
(まだ見つからなかった)

前半はわりとトランジション・アニメーションの概念的な話

後半はコードも出てきた
ViewTreeObserver.addOnPreDrawListener() 使って animation のトリガーにするのは ugly だからダメ絶対

Tumblr で、Likeボタンのアニメーションあり、なし、でどっちがより LikeしてくれるかABテストしたけど、50%、50%で違いがなかったそうだ

Lollipop の Dialer のアニメーションのサンプル作ったので参考にしてねとのこと
https://github.com/kevinthecity/DialerAnimation



getBounds(), the Drawable Story
Jamie Huson, Lisa Neigut (Etsy)

内容は
https://speakerdeck.com/niftynei/getbounds-the-story-of-drawables-and-their-view-masters
とだいたい同じ

CustomDrawable の作り方の話

Devoxx 2013 での Cyril Mottier のセッションが参考になる
https://speakerdeck.com/cyrilmottier/mastering-android-drawables



Mastering Recyclerview Layouts
Dave Smith (NewCircle, Inc.)

セッションスライド
http://www.slideshare.net/devunwired/mastering-recyclerview-layouts

RecyclerView の基本的な使い方
GirdLayoutManagerで各位置の span size を変えるとか

RecylerView Playground
https://github.com/devunwired/recyclerview-playground

Building a RecyclerView LayoutManager
http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-redux/

Android SDK Reference
http://developer.android.com/training/material/lists-cards.html



Recipes for a Solid Foundation
Eric Cochran (IFTTT)

セッションスライド
(まだ見つからなかった)

1ActivityでFragmentの管理よろしくしてくれるライブラリ作ったそうだ
https://github.com/nightlynexus/dire



Introduction To Functional Reactive Programming On Android
Juan Gomez (Netflix)

セッションスライド
https://speakerdeck.com/juandg/intro-to-functional-reactive-programming-droidcon-mtl-2015

Introductionな話
RxJava は Started at Netflix
図がわかりやすかった

https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables



Gotta Persist 'Em All: Realm As Replacement fo Sqlite
Siena Aguayo (Indiegogo)

セッションスライド
http://www.slideshare.net/SienaAguayo/gotta-persist-em-all-realm-as-replacement-for-sq-lite-46956020

Realm を使ってポケモンデータをあれこれする話
このお姉さんは Pokemon Master
スライドにも随所にポケモンがでてきた
(キレイハナ、ダストダス、ラティアス、ラティオス、フーディン、フシギダネ、ミロカロス)
フシギダネは bulbasaur と言うそうだ



DroidCon Japan?

日本でDroidConを開催したらJakewhartonが来てくれるかもしれない。がんばれ mhidaka





おまけ

モントリオールはフランス語圏(フランス語圏としてはパリについで2番目の経済圏らしい by roishi情報)なので 町中の表示がみんなフランス語でした。SORTIE は exit のことだということを覚えました。

ノートルダム大聖堂にも行きました(入場料5$)。昼の12時には鐘がなっていました。



マリー・レーヌ・デュ・モンド大聖堂にも行きました(入場料無料)。