この記事は Nick Rout による Android Developers Blog の記事 "MAD Skills Material Design Components: Wrap-Up" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Modern Android Development(最先端の Android 開発)について取り上げる連載シリーズ MAD Skills の動画と記事の 3 番目のトピックが完結しました。今回は、マテリアル デザイン コンポーネント(MDC)についてご説明しました。このライブラリは、マテリアル コンポーネントを Android ウィジェットとして提供します。これを使うと、マテリアル テーマ、ダークテーマ、モーションなど、material.io で使われているデザイン パターンを簡単に実装できます。
説明した内容については、下記にまとめた各エピソードからご確認ください。これらの動画では、MDC についての最新の記事や、既存のサンプルアプリ、Codelab を詳しく説明しています。また、MCD チームのエンジニアによる Q&Aセッションも含まれています 。
最初のエピソードは、Nick Butcher が、なぜ私たちが MDC の利用を推奨するのかなど、今回の MAD Skills シリーズ全体の概要を説明しています。その後、マテリアル テーマ、ダークテーマ、モーションについて詳しく掘り下げていきます。また、MDC を Jetpack Compose と合わせて使う方法、MDC やテーマ、スタイルのベスト プラクティスを含むようにアップデートされた Android Studio のテンプレートについてもお話ししています。
エピソード 2 では、Nick Rout がマテリアル テーマについて説明し、Android で MDC を使ってこれを実装するチュートリアルについて解説しています。主な内容は、Theme.MaterialComponents.* アプリのテーマを設定し、material.io のツールを使用して色や種類、形状の属性を選択し、最終的にそれらをテーマに追加して、ウィジェットがどのように自動的に反応して UI を適応させるかを確認します。また、テーマカラー属性を解決する、イメージに図形を適用するなど、MDC が特定のシナリオ向けに提供している便利なユーティリティ クラスについても説明します。
Theme.MaterialComponents.*
Chris Banes が、Android アプリで MDC を使ってダークテーマを実装する方法を紹介しています。説明する内容は、Force Dark を使って短時間で変換しビューを除外する方法、デザインを選んでダークテーマを手動で作成する方法、`.DayNight` MDC アプリテーマ、`.PrimarySurface` MDC ウィジェット スタイル、そしてシステム UI を扱う方法などです。
エピソード 4 では、Nick Rout がマテリアルのモーション システムについて解説しています。また、既存の「Android でマテリアル モーションを使って美しい画面遷移を構築する」Codelab の手順を詳しくフォローしています。この Codelab では、Reply サンプルアプリを使って、コンテナ変換、共有軸、フェードスルー、フェードという遷移パターンを活用してスムーズでわかりやすいユーザー エクスペリエンスを実現する方法を紹介しています。また、Fragment(Navigation コンポーネントを含む)、Activity、View を使うシナリオについて説明しています。
エピソード 5 は、Android コミュニティから Google Developer Expert (GDE) の Zarah Dominguez さんが、MDC カタログアプリをウィジェットの機能や API の例として参考にしながら紹介してくれました。そのほか、異なる画面やフローにまたがる一貫したデザイン言語を確保するために、彼女が取り組んでいるアプリに「テーマショーケース」ページを構築することが、どのように有益であるかを説明しています。
最後のまとめとして、Chet Haase が MDC エンジニアリング チームの Dan Nizri と Connie Shi と一緒に Q&A セッションを行い、Twitter や YouTube で寄せられた皆さんからの質問に回答しました。このセッションでは MDC の起源、AppCompat との関係、今までの改善点について解説したほか、テーマやリソースを整理するためのベストプラクティス、さまざまなフォントやタイポグラフィ スタイルの使用方法、シェイプ テーミングなどについてお話しました。また、私たちはお気に入りの Material コンポーネントをすべて公開し、最後に、将来的に MDCと Jetpack Compose という Androidの次世代 UI ツールキットでは、デフォルトで Material Design が組み込まれる新しいコンポーネントが登場することについて議論しました。
このシリーズでは、MDC のデモとして、次の 2 つのサンプルアプリを使いました。
これらは、もう 1 つの Material Studies のサンプルアプリである Owl とともに、GitHub リポジトリの MDC サンプルで確認できます。
Reviewed by Takeshi Hagikura - Developer Relations Team and Hidenori Fujii - Google Play Developer Marketing APAC
Google では、2018 年 9 月 27 日(木)に Material Design Day Tokyo を開催します。
Google では、Material Design にフォーカスしたイベント 「Material Design Day Tokyo」を 9 月 27 日 (木) 10:00 AM より開催します。 本イベントでは、今年の Google I/O で発表された、Material Theming やツールについての Google のスピーカーによるセッションに加えて、コードラボを通じて、いかに Material Design を貴社のアプリに利用するかのワークショップも合わせて行います。 Material Design に興味のあるデザイナーの方、開発者の方はぜひご参加ください。 イベント概要 イベント名: Material Design Day Tokyo 日程: 2018 年 9 月 27 日(木) 10:00 - 16:30 (開場: 9:30) 場所:グーグル合同会社 定員 :250 名 タイムテーブル: 9 : 30 受付開始 10:00 - 10:10 開会のご挨拶 10:10 - 11:10 Building a design system with Material 11:10 - 11:40 Material Tools and Eng 11:40 - 12:10 Material Examples 12:10 - 12:30 Q&A セッション 12:30 - 13:30 ランチ 13:30 - 16:30 コードラボ ※タイムテーブルは変更となる場合があります。 申込方法 本イベントへの申し込み、詳細につきましてはこちらのサイトをご覧ください。 ※ 参加可能な方には後日参加証を送付いたします。
A
B
ImageView
ViewPager
RecyclerView
GridFragment
ImageData
ImagePagerFragment
onClick
ImageFragments
MainActivity
transitionName
ImageFragment
setTransitionName
onCreateView
SharedElementCallbacks
onMapSharedElements
FragmentManager
fragment.getFragmentManager() .beginTransaction() .setReorderingAllowed(true) // setAllowOptimization before 26.1.0 .addSharedElement(imageView, imageView.getTransitionName()) .replace(R.id.fragment_container, new ImagePagerFragment(), ImagePagerFragment.class.getSimpleName()) .addToBackStack(null) .commit();
setReorderingAllowed
true
onDestroy()
onCreate(Bundle)
TransitionSet
<ImagePagerFragment.java>
Transition transition = TransitionInflater.from(getContext()) .inflateTransition(R.transition.image_shared_element_transition); setSharedElementEnterTransition(transition);
<image_shared_element_transition.xml>
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:transitionOrdering="together"> <changeClipBounds/> <changeTransform/> <changeBounds/> </transitionSet>
SharedElementCallback
setExitSharedElementCallback()
Fragment
<GridFragment.java>
setExitSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { // Locate the ViewHolder for the clicked position. RecyclerView.ViewHolder selectedViewHolder = recyclerView .findViewHolderForAdapterPosition(MainActivity.currentPosition); if (selectedViewHolder == null || selectedViewHolder.itemView == null) { return; } // Map the first shared element name to the child ImageView. sharedElements .put(names.get(0), selectedViewHolder.itemView.findViewById(R.id.card_image)); } });
setEnterSharedElementCallback()
setEnterSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { // Locate the image view at the primary fragment (the ImageFragment // that is currently visible). To locate the fragment, call // instantiateItem with the selection position. // At this stage, the method will simply return the fragment at the // position and will not create a new one. Fragment currentFragment = (Fragment) viewPager.getAdapter() .instantiateItem(viewPager, MainActivity.currentPosition); View view = currentFragment.getView(); if (view == null) { return; } // Map the first shared element name to the child ImageView. sharedElements.put(names.get(0), view.findViewById(R.id.image)); } });
onCreateView()
postponeEnterTransition()
startPostponedEnterTransition()
postponeEnterTransition
startPostponeEnterTransition
<ImageFragment.java>
Glide.with(this) .load(arguments.getInt(KEY_IMAGE_RES)) // Load the image resource .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } }) .into((ImageView) view.findViewById(R.id.image));
setExitTransition(TransitionInflater.from(getContext()) .inflateTransition(R.transition.grid_exit_transition));
<grid_exit_transition.xml>
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:startDelay="25"> <fade> <targets android:targetId="@id/card_view"/> </fade> </transitionSet>
GridAdapter
// The 'view' is the card view that was clicked to initiate the transition. ((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true);
onViewCreated
recyclerView.addOnLayoutChangeListener( new OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { recyclerView.removeOnLayoutChangeListener(this); final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View viewAtPosition = layoutManager.findViewByPosition(MainActivity.currentPosition); // Scroll to position if the view for the current position is null (not // currently part of layout manager children), or it's not completely // visible. if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){ recyclerView.post(() -> layoutManager.scrollToPosition(MainActivity.currentPosition)); } } });
TextRenderer
ComplicationDrawable
ComplicationProviderService
ComplicationData
<resources> <string name="transition_avatar">AvatarTransition</string> </resources>
<ImageView android:id="@+id/avatar" android:layout_width="@dimen/avatar_size" android:layout_height="@dimen/avatar_size" android:layout_marginEnd="@dimen/keyline_16" android:transitionName="@string/transition_avatar"/>
private void performSignInWithTransition(View v) { Activity activity = getActivity(); ActivityOptions activityOptions = ActivityOptions .makeSceneTransitionAnimation(activity, v, activity.getString(R.string.transition_avatar)); CategorySelectionActivity.start(activity, mPlayer, activityOptions); activity.finishAfterTransition(); }
performScoreAnimation
private void addFloatingActionButton() { final int fabSize = getResources().getDimensionPixelSize(R.dimen.fab_size); int bottomOfQuestionView = findViewById(R.id.question_view).getBottom(); final LayoutParams fabLayoutParams = new LayoutParams(fabSize, fabSize, Gravity.END | Gravity.TOP); final int fabPadding = getResources().getDimensionPixelSize(R.dimen.padding_fab); final int halfAFab = fabSize / 2; fabLayoutParams.setMargins(0, // left bottomOfQuestionView - halfAFab, //top 0, // right fabPadding); // bottom addView(mSubmitAnswer, fabLayoutParams); }
addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int l, int t, int r, int b, int oldLeft, int oldTop, int oldRight, int oldBottom) { removeOnLayoutChangeListener(this); addFloatingActionButton(); } });
@Override public final void getOutline(View view, Outline outline) { final int size = view.getResources(). getDimensionPixelSize(R.id.view_size); outline.setOval(0, 0, size, size); }
setClipToOutline(true)
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="48" android:viewportHeight="48"> <path android:pathData="M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z" android:fillColor="?android:attr/textColorPrimary" /> </vector>
// Property for animating the foreground public static final Property FOREGROUND_COLOR = new IntProperty("foregroundColor") { @Override public void setValue(FrameLayout layout, int value) { if (layout.getForeground() instanceof ColorDrawable) { ((ColorDrawable) layout.getForeground()).setColor(value); } else { layout.setForeground(new ColorDrawable(value)); } } @Override public Integer get(FrameLayout layout) { return ((ColorDrawable) layout.getForeground()).getColor(); } };
final ObjectAnimator foregroundAnimator = ObjectAnimator .ofArgb(this, FOREGROUND_COLOR, Color.WHITE, backgroundColor);