class: center, middle, hero, chapter-1 #
サポート ライブラリの
できるまで
--- class: normal, chapter-1 # 自己紹介 .card[ ![近影](../images/yaraki.png) 荒木佑一 [@yuichi_araki](https://twitter.com/yuichi_araki) Developer Programs Engineer - Google I/O アプリ - Google Santa Tracker アプリ - [d.android.com/samples](http://d.android.com/samples) のサンプル - MIDI 関連 - Camera2 関連 - Android for Work 関連 - ScreenCapture - Transition 関連 ] --- class: center, middle, hero, chapter-1 #
Android Design
Support Library
--- class: normal, chapter-1 # それは何 .card[ ![cheesesquare](cheesesquare.png) Material Design のコンポーネント群 - FloatingActionButton - Snackbar - CoordinatorLayout - AppBar - CollapsingToolbarLayout - TextInputLayout - TabLayout - NavigationView サンプル: [github.com/chrisbanes/cheesesquare](https://github.com/chrisbanes/cheesesquare) ] --- class: normal, chapter-1 # 歴史 .card[ 2014/06: Google I/O 2014 [Material Design を発表](https://www.youtube.com/watch?v=Q8TXgCzxEnw) 2014/10: Android 5.0 Lollipop リリース 2014/12: 企画開始 2015/01: 開発開始 2015/05: Google I/O 2015 Design Support Library をリリース (バージョン 22.2.0) (略) 2015/11: バージョン 23.1.1 ] ??? - I/O アプリ - 2014 - 非マテリアル版とマテリアル版の 2 種類あった - Design ライブラリはありませんでした - 2015 - Design ライブラリをつかった最初のアプリ - バージョン番号 - 1 つ目はフレームワークの最新 API レベル - 2 つ目はライブラリの API が変更されたときにあげられる - 3 つ目は (基本的に) API 変更を伴わないバグフィックス、改善 - SDK マネージャーで出てくる Support Repository の リビジョン番号とは関係ない - ビルドツールのバージョンとも直接は関係ない --- class: normal, chapter-1 # 立ち位置 .card[ ## Support v4 - v4 以降の API のバックポート + プラットフォームにないもの - リソースなし ## AppCompat - v7 以降 UI 関係の API のバックポート - リソースあり - Support v4 に依存 ## Design - プラットフォームにはないものだけ - AppCompat v7 と RecyclerView に依存 ] ??? - よく聞かれる質問: なぜ既存のライブラリに機能を追加するのではなく、新規にライブラリを作成したか - フレームワークにあるもののバックポートは AppCompat に、フレームワークにないものは Design に - どこに何があるかわからなくなったときの参考に - RecyclerView には 23.1.0 から依存するようになった --- class: normal, chapter-1 # 使い方 .card[ Android Design Support Library について http://googledevjp.blogspot.jp/2015/07/android-design-support-library.html Android サポート ライブラリ 23.1 のご紹介 http://googledevjp.blogspot.jp/2015/11/android-231.html Techbooster Android Masters! https://techbooster.booth.pm/items/126263 chrisbanes/cheesesquare https://github.com/chrisbanes/cheesesquare ] ??? - 丸投げ - その他インターネットで検索するとたくさん見つかる - 今回は使い方の話は最低限にとどめて、この Design サポート ライブラリを作る上で役に立った知見を共有する - ただし、あくまでサポート ライブラリ以外でも役に立つものに限る --- class: normal, chapter-1 # これだけ .card[ ```ruby android { compileSdkVersion `23` } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:`23`.1.1' } ``` ] ??? - 必ずサポートライブラリのメジャー バージョンと compileSdkVersion を合わせる --- class: center, middle, hero, chapter-2 # 23.1.0, 23.1.1 の変更点 --- class: normal, chapter-2 # AppBar の layout_scrollFlags で snap .card[ AppBar 内の要素が部分的に表示されている状態を避ける ```xml <android.support.design.widget.CoordinatorLayout ... > <android.support.design.widget.AppBarLayout ... > <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:layout_scrollFlags="scroll|enterAlways|`snap`" /> ... ``` ] ??? - AppBarLayout はアップバー (アクションバー) を作るコンポーネント - CoordinatorLayout に入れて使う - Toolbar を入れたり、TabLayout でタブをつけたり、CollapsingToolbarLayout でパララックスな背景をつけたり - 例は cheesesquare から --- class: normal, chapter-2 # TextInputLayout の文字数カウンター .card[ ![TextInputLayout スクリーンショット](til-counter.png) 自動的に入力文字数をカウントする ```xml
``` - TextView の android:maxLength と違って、制限数を超えて入力できる ] ??? - TextInputLayout は EditText の機能を補強するコンポーネント - エラー表示、ヒントのフロート表示など - 新しく文字数カウンターが入った - Java から使うなら setCounterEnabled, setCounterMaxLength - エラー表示とも共存できる --- class: normal, chapter-2 # NavigationView の app:actionLayout .card[ ![NavigationView スクリーンショット](nv-actionlayout.png) NavigationView の項目の右端に任意のレイアウトを配置する ```xml <menu ...> <group android:checkableBehavior="single"> ... <item android:id="@+id/navigation_item_3" android:icon="@drawable/ic_android" android:title="@string/navigation_item_3" `app:actionLayout="@layout/action_layout"`/> ... ``` - MenuItem#getActionView() で参照を取得 ] ??? - NavigationView はナビゲーション ドローワーの中のメニューを作るコンポーネント - DrawerLayout に入れて使う。メニュー XML を使う。 - app:actionLayout を使うと、各メニュー項目に任意の View を配置することができる - Gmail の未読数など --- class: normal chapter-2 # その他 .card[ - NavigationView の getHeaderView (23.1.1) XML の app:headerLayout で指定したヘッダーを getHeaderView で取得できる - バグフィックス ] --- class: center, middle, hero, chapter-3 # カスタム View の基本 ??? - Design サポート ライブラリはカスタム View のかたまり - もっとみんなカスタム View を作るべき - リストの項目と同じものを詳細画面でも使いたい場合 - Activity や Fragment に UI ロジックが多くなって見通しが悪くなった場合 --- class: normal, chapter-3 # コンストラクター .card[ View を直接または間接的に継承し、1 引数のコンストラクターから 3 引数のコンストラクターまで delegate していく ```java public class NavigationView extends ScrimInsetsFrameLayout { public NavigationView(Context context) { * this(context, null); } public NavigationView(Context context, AttributeSet attrs) { * this(context, attrs, 0); } public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) { * super(context, attrs, defStyleAttr); // 初期化 } } ``` ] ??? - 例は NavigationView - ただし、LinearLayout の 3 引数コンストラクターは API Level 11 以降なので - 4 引数のコンストラクターを使えばデフォルトのスタイルを指定できるが、 v21 以降なのでいったん忘れよう --- class: normal, chapter-3 # カスタム属性の定義 - シンプルな型 .card[ values/attrs.xml にカスタム View と同名の declare-styleable を追加する ```xml
``` - すでに存在する属性 (Android フレームワークや AppCompat などに含まれる属性) には format を付けない - その View でのみ利用するものは format を付ける - 複数のカスタム View で共通のカスタム属性を利用する場合は、declare-styleable の外で format 付きの attr 定義をし、declare-styleable の中で format なしの attr 定義をする ] ??? - レイアウト XML に記述する属性名を自分で定義できる - 対応するメソッドも用意する - format の方は boolean, color, dimension, integer, reference などがある。 --- class: normal, chapter-3 # カスタム属性の定義 - enum, flag .card[ enum は選択肢の中から一つ選ばせるとき flag は選択肢の中から複数選ばせるとき ```xml
...
...
``` コードでも対応する値を int 定数として宣言する ] ??? - 例は FloatingActionButton - 仕様上の大きさが 56dp か 40dp の 2 通り - Android Studio での補完も効くようになる - コード側で対応する int 定数を宣言する方法は後ほど --- class: normal, chapter-3 # デフォルトの属性値 (スタイル) .card[ ユーザーが属性値を指定しなかった場合のデフォルト値を values/styles.xml で定義する。 名前は Widget.[プロジェクト名].[カスタム View 名] ```xml <style name="Widget.Design.NavigationView" parent="android:Widget"> <item name="elevation">@dimen/navigation_elevation</item> <item name="android:maxWidth">@dimen/navigation_max_width</item> ... </style> ``` ] ??? - ディレクトリのサフィックスで画面の縦横などによってスタイルを変えることもできる - 例: TabLayout など --- class: normal, chapter-3 # カスタム属性の値を取得 .card[ コンストラクターの中で属性の値を取得し、カスタム View を初期化する ```java // 属性リストを取得 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavigationView, // 属性の定義 defStyleAttr, R.style.Widget_Design_NavigationView); // デフォルトの属性値 // 属性値を取得 mMaxWidth = a.getDimensionPixelSize( R.styleable.NavigationView_android_maxWidth, 0); final Drawable itemBackground = a.getDrawable( R.styleable.NavigationView_itemBackground); // (略) // 忘れずに a.recycle(); ``` ] ??? - 先ほどの 4 引数コンストラクターが使えればこの記述は楽にできるが、v21 でしか使えないので忘れよう --- class: normal, chapter-3 # レイアウト XML の利用 .card[ ```java public class NavigationMenuItemView extends ForegroundLinearLayout implements MenuView.ItemView { ... public NavigationMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(HORIZONTAL); * inflate(context, R.layout.design_navigation_menu_item, this); ... ``` ```xml
...
``` ] ??? - android.view.View クラスを直接拡張して onDraw をオーバーライドしてカスタム View を作ることもできるが、あまりしない - 大抵は既存の View や ViewGroup を継承して作る - ViewGroup を拡張する場合、レイアウト XML を利用するべき - レイアウト XML は merge で始める。そうしないと LinearLayout が二重になる - merge タグの中では親クラスの LayoutParams を利用できる - RelativeLayout を継承しているなら、layout_alignParentTop などを利用できる --- class: normal, chapter-3 # SavedState の実装 .card[ View.BaseSavedState を継承したインナー クラス SavedState を作る ```java protected static class SavedState extends BaseSavedState { public Bundle menuState; public SavedState(Parcel in, ClassLoader loader) { super(in); * menuState = in.readBundle(loader); } public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); * dest.writeBundle(menuState); } public static final Parcelable.Creator<SavedState> CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { @Override public SavedState createFromParcel(Parcel parcel, ClassLoader loader) { return new SavedState(parcel, loader); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }); } ``` ] ??? - 画面回転時などに、カスタム View の状態を保持するオブジェクト - Parcel に複数の値を入れる場合は、書き込む順番と読み込む順番を合わせる --- class: normal, chapter-3 # SavedState の利用 .card[ ```java @Override protected Parcelable onSaveInstanceState() { SavedState state = new SavedState(super.onSaveInstanceState()); * state.menuState = new Bundle(); * mMenu.savePresenterStates(state.menuState); return state; } @Override protected void onRestoreInstanceState(Parcelable savedState) { SavedState state = (SavedState) savedState; super.onRestoreInstanceState(state.getSuperState()); * mMenu.restorePresenterStates(state.menuState); } ``` - View を利用するときは id を付けておくのを忘れずに ] ??? - 何を保存するべきかよく考える - 保存される例: EditText の入力内容, 保存されない例: padding - レイアウト XML で指定するもののうち、縦横などで切り替える可能性があるもの - EditText は画面回転時も入力内容を保持するが、id がついてないと保持されない - ここまでの内容を押さえておけば簡単なカスタム View は作れるはず --- class: middle, center, hero, chapter-4 # カスタム View の Tips --- class: normal, chapter-4 # AppCompat のコンポーネントを継承 .card[ AppCompat なんとか が存在する場合、そちらを継承する - AppCompatButton - AppCompatTextView - AppCompatEditText - AppCompatCheckBox - AppCompatRadioButton - AppCompatSpinner - etc ] ??? - Design サポート ライブラリでこのパターンはなかった - AppCompat なんとかには backgroundTint などの機能のバックポートが含まれる - AppCompat を適切に使っている限り、レイアウト XML で Button タグを使えば、AppCompat が自動的に AppCompatButton を inflate するので、AppCompatButton の存在を意識することはない - カスタム View に関しては明示的に継承する必要がある --- class: normal, chapter-4 # アノテーション .card[ パブリックな API には @NonNull/Nullable や @IdRes, @ColorInt などのアノテーションを適切に付ける - http://tools.android.com/tech-docs/support-annotations int を enum 代替として使う場合には独自のアノテーションを定義する ```java @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG}) @Retention(RetentionPolicy.SOURCE) public @interface Duration {} ``` ```java public static Snackbar make(@NonNull View view, @NonNull CharSequence text, `@Duration` int duration) { ... } ``` ] ??? - @ColorRes と @ColorInt に注意 - 例は Snackbar から --- class: normal, chapter-4 # 不用意にインターフェイスを実装しない .card[ ```java public class TextInputLayout extends LinearLayout `implements TextWatcher` { private void setEditText(EditText editText) { mEditText.addTextChangedListener(`this`); } } ``` .center[ ↓ ] ```java public class TextInputLayout extends LinearLayout { private void setEditText(EditText editText) { mEditText.addTextChangedListener(`mTextWatcher`); } private final TextWatcher mTextWatcher = new TextWatcher() { ... }; } ``` ] ??? - 例は TextInputLayout、EditText への入力を監視する必要があるので、TextWatcher を使う - 上の書き方だと、外部からこのクラスのインスタンスを TextWatcher として利用できてしまう --- class: normal, chapter-4 # 複数のメソッドを持つリスナー .card[ ## 従来のパターン インターフェイス と 空実装の抽象クラス をそれぞれ用意する 例: DrawerLayout.DrawerListener + DrawerLayout.SimpleDrawerListener AnimatorListener + AnimatorListenerAdapter ## 新しいパターン 抽象クラスのみ 例: FloatingActionButton.OnVisibilityChangedListener ] ??? - すべてのリスナーをこうするべきとは限らない --- class: normal, chapter-4 # API レベルによって処理を分ける .card[ ```java public class FloatingActionButton extends ImageButton { `private final FloatingActionButtonImpl mImpl`; public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { final int sdk = Build.VERSION.SDK_INT; if (sdk >= 21) { mImpl = new FloatingActionButtonLollipop(this, delegate); } else if (sdk >= 14) { mImpl = new FloatingActionButtonIcs(this, delegate); } else { mImpl = new FloatingActionButtonEclairMr1(this, delegate); } } public void show(@Nullable OnVisibilityChangedListener listener) { `mImpl.show(wrapOnVisibilityChangedListener(listener))`; } } ``` ] ??? - if 文の分岐が増えるとコードが煩雑になってしまう - FloatingActionButtonImpl は抽象クラスで、API レベルごとの処理が必要なメソッドの空定義が入っている - FloatingActionButtonEclairMr1 は FloatingActionButtonImpl を拡張していて、すべてのメソッドを EclarMr1 向けに実装している - FloatingActionButtonIcs は FloatingActionButtonEclairMr1 を拡張していて、Ics で使える機能に置き換えている - FloatingActionButtonLollipop は FloatingActionButtonIcs を拡張していて、Lollipop で使える機能に置き換えている - すべての API レベルで共通の処理は FloatingActionButton クラスに直接書く --- class: normal, chapter-4 # RTL .card[ ## マージン、パディングなど 左右対称のとき ```xml layout_marginLeft="@dimen/item_margin_horizontal" layout_marginRight="@dimen/item_margin_horizontal" ``` 左右非対称の時 ```xml layout_marginStart="@dimen/item_margin_start" layout_marginLeft="@dimen/item_margin_start" layout_marginEnd="@dimen/item_margin_end" layout_marginRight="@dimen/item_margin_end" ``` ## Gravity 系 ``` gravity="top|start" ``` ] ??? - Gravity の start/end は 14, margin/padding の start/end は API Level 17 (JB MR1)以降 - 左右対称のときは left/right だけでいい - Android Studio も警告を出さないようになっている - 左右対称でないときは start/end と left/right の両方書く - Gravity は start/end だけでいい - コードから指定するなら GravityCompat.START/END --- class: normal, chapter-4 # レイアウト変更を引き起こす操作を知る .card[ レイアウト変更を引き起こす操作は、外観上問題がなくても、必要なとき以外は呼び出さない ```java public void setCounterEnabled(boolean enabled) { if (mCounterEnabled != enabled) { if (enabled) { ... * mCounterView.setTextAppearance(getContext(), mCounterTextAppearance); ... } else { ... } mCounterEnabled = enabled; } } ``` ] ??? - TextView の setTextAppearance は、文字のサイズや色などをまとめて設定するメソッド - TextView 自体のサイズが変わる可能性がある - つまりその TextView を含む ViewGroup のサイズも変わる可能性がある - さらにその ViewGroup を含む ... - ツリー上のすべての View をレイアウトし直す必要がある - レイアウト変更は重い処理 - 文字の色を変更するだけなら大丈夫なように思うが、そんなことはない - まったく同じ TextAppearance を設定してもやはりレイアウト変更が発生する - 他にも TextView なら setText など --- class: normal, chapter-4 # テーマでカスタマイズできるようにする .card[ styles.xml などで既存の ?android:attr/... や ?attr/... を利用する ```xml android:background="`?attr/colorPrimary`" android:foreground="`?attr/selectableItemBackground`" android:textColor="`?android:attr/textColorPrimary`" ``` ] ??? - カスタム View がアプリのテーマに従った見た目になるようにする - AppCompat にあるものは ?attr - システム標準で昔からあるものは ?android:attr - 同じ名前で両方あるなら AppCompat のもの、つまり ?attr を使う --- class: center, middle, hero, chapter-5 # ライブラリに関する Tips --- class: normal, chapter-5 # リソースを隠す .card[ Public and Private Resources http://developer.android.com/intl/ja/tools/studio/studio-features.html#private-res values/public.xml に宣言したものは public、それ以外は private ```xml <resources> <public type="attr" name="elevation"/> <public type="string" name="appbar_scrolling_view_behavior"/> <public type="style" name="Widget.Design.FloatingActionButton"/> </resources> ``` ] ??? - 何もしなければライブラリ内のすべてのリソースが public になってしまう - カスタム View を作るための layout ファイルも外から見えるようになってしまう - Android Studio の補完にどうでもいいリソースが出てきてイラッとする - public リソースがあれば、宣言されていないものは private になる - 明示的に private なものを指定する方法はない --- class: normal, chapter-5 # ProGuard 設定 .card[ ライブラリの build.gradle で ProGuard 設定を指定 ```ruby android { buildTypes.all { * consumerProguardFiles 'proguard-rules.pro' } } ``` ```java -keep public class * extends android.support.design.widget.CoordinatorLayout$Behavior { public
(android.content.Context, android.util.AttributeSet); public
(); } ``` ProGuard Usage https://stuff.mit.edu/afs/sipb/project/android/sdk/android-sdk-linux/tools/proguard/docs/index.html#manual/usage.html ] ??? - 主にライブラリ内でリフレクションを使っている場合 - このライブラリを使っているアプリで ProGuard を使った時にこの設定が利用される --- class: center, middle, hero, chapter-6 #
Design ライブラリについての
ご意見・ご要望
## b.android.com ## @yuichi_araki