entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

pixiv Android アプリでのデザインシステムを活用したダークテーマ対応

フリーランスのソフトウェアエンジニアとして活動している djyugg です。ピクシブ株式会社では、業務委託として主に pixiv Android アプリの開発に携わっています。

play.google.com

多くの要望を頂いていた pixiv Android アプリのダークテーマ対応を2021年7月末にリリースしました。

このダークテーマ対応は、ピクシブのデザインシステムの思想を実装して実現しています。今回は、Android アプリでいかにデザインシステムの実装を実現し、ダークテーマに対応したかを紹介します。

なお、今回は主に Android View 向けの内容となっており、Jetpack Compose には触れていないです。

デザインシステムを利用した色の設定

ピクシブでは、デザインシステムを運用しています。

デザインシステムを実現する実装は、ダークテーマ対応開始時点では Android アプリ向けには存在していなかったため、対応開始と同時に実装をはじめました。

Web 向けの内容になりますが、以前公開された記事はこちらです。

前編:

inside.pixiv.blog

後編:

inside.pixiv.blog

デザインシステム内で定義している内容はいくつかありますが、ダークテーマ対応時に主に利用したのは、デザインシステム内でデザイントークンとして定義している色です。

後編の記事にも記載されていますが、用途ごとに利用するセマンティクスを最低限にした色の名前を用意しており、それらを利用することでライト・ダークテーマ毎に適切な色が当たるように設計されています。

この色のデザイントークンを各画面・コンポーネントで利用することでダークテーマ対応を実現しています。

色の実装について

具体的な実装としては、デザイントークンを独自の属性として定義し、ライト・ダークテーマそれぞれで利用する Theme に属性に対応する色を設定する形になります。

また、デザイントークンとは別に、デザインシステム内で利用する色をまとめているカラーパレットが存在しており、属性の内容はカラーパレットの色を設定しています。

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Brand -->
    <attr name="colorPixivBrand" format="color" />
    <attr name="colorPixivBrandDark" format="color" />

    <!-- Semantic -->
    <attr name="colorPixivSuccess" format="color" />
    <attr name="colorPixivAssertive" format="color" />
    ...

    <!-- Surface -->
    <attr name="colorPixivSurface1" format="color" />
    <attr name="colorPixivSurface2" format="color" />
    ...

    <!-- On Surface -->
    <attr name="colorPixivText1" format="color" />
    <attr name="colorPixivText2" format="color" />
    ...
</resources>

themes.xml

<resources>

    <style name="Theme.Pixiv.Light" parent="Theme.MaterialComponents.Light.Bridge">
        <item name="colorPixivBrand">@color/color_palette_xxx</item>
        <item name="colorPixivBrandDark">@color/color_palette_xxx</item>
        <item name="colorPixivSuccess">@color/color_palette_xxx</item>
        <item name="colorPixivAssertive">@color/color_palette_xxx</item>
        <item name="colorPixivSurface1">@color/color_palette_xxx</item>
        <item name="colorPixivSurface2">@color/color_palette_xxx</item>
        <item name="colorPixivText1">@color/color_palette_xxx</item>
        <item name="colorPixivText2">@color/color_palette_xxx</item>
        ...
    </style>

    <style name="Theme.Pixiv.Dark" parent="Theme.MaterialComponents.Bridge">
        <item name="colorPixivBrand">@color/color_palette_xxx</item>
        <item name="colorPixivBrandDark">@color/color_palette_xxx</item>
        <item name="colorPixivSuccess">@color/color_palette_xxx</item>
        <item name="colorPixivAssertive">@color/color_palette_xxx</item>
        <item name="colorPixivSurface1">@color/color_palette_xxx</item>
        <item name="colorPixivSurface2">@color/color_palette_xxx</item>
        <item name="colorPixivText1">@color/color_palette_xxx</item>
        <item name="colorPixivText2">@color/color_palette_xxx</item>
        ...
    </style>
</resources>

Theme切り替え

DayNight Theme を用意することでライト・ダークテーマ毎に参照する Theme を切り替えるようにします。

values/themes_daynight.xml

<resources>
    <style name="Theme.Pixiv.DayNight" parent="Theme.Pixiv.Light" />
</resources>

values-night/themes_daynight.xml

<resources>
    <style name="Theme.Pixiv.DayNight" parent="Theme.Pixiv.Dark" />
</resources>

依存関係

Theme の依存関係を図にすると以下のような形になります。

values-night ディレクトリは、アプリ内でのナイトモードが有効になっているときに利用される代替リソースのディレクトリです。ダークテーマが有効となっている場合は、このディレクトリのリソースが利用されるようになります。

上記のように定義した Theme.Pixiv.DayNight を利用すると、ダークテーマ無効時は Theme.Pixiv.Light 、ダークテーマ有効時は Theme.Pixiv.Dark を参照するようになります。 この代替リソースの仕組みを利用して Theme を切り替えることで、 属性に紐づける色リソースの切り替えを実現します。

AppCompat や Material Components にて用意されている DayNight Theme も同様の実装となっており、それらを参考に実装しました。

この DayNight Theme を各 Activity で利用するよう設定し、デザイントークンに紐づく属性をレイアウトやコード上で参照することで色を適用していきます。

デザインシステムと Material Components for Android

ここからは、Theme やコンポーネントの設定について書きます。

ピクシブのデザインシステムにて、現時点で Android 向けに定義されているコンポーネントのほとんどは Material Design に定義されているものをベースに配色を変えたものとなっております。そのため、 Material Components for Android を活用して実装しています。

現時点では、バージョン 1.4.0 を利用しているため、その前提で続きを書きます。

Color Theming

Material Components for Android に定義されているすべてのコンポーネントを利用するためには、上記ライブラリに定義されている Theme を利用する必要があります。

pixiv Android アプリでは、Theme 適用時のアプリへの影響を最小限に抑えるために Bridge Theme を採用しました。

また、Material Components for Android には、Material Theming というアプリの外観を体系的に表現する仕組みがあります。その中でも、ダークテーマに大きく関わる部分として Color Theming が挙げられます。

今回、Material Components for Android を利用するにあたって、Color Theming の属性には類似する役割のデザイントークンを割り当てています。以下、設定の一部です。

themes.xml

<item name="colorPrimary">?attr/colorPixivBrand</item>
<item name="colorPrimaryVariant">?attr/colorPixivBrandDark</item>
<item name="colorOnPrimary">?attr/colorPixivText5</item>
<item name="android:colorBackground">?attr/colorPixivBackground1</item>
<item name="colorOnBackground">?attr/colorPixivText2</item>
<item name="colorSurface">?attr/colorPixivSurface1</item>
<item name="colorOnSurface">?attr/colorPixivText2</item>
<item name="colorError">?attr/colorPixivAssertive</item>
<item name="colorOnError">?attr/colorPixivText5</item>

ピクシブのデザインシステムでは、デザイントークンが代替の役割を果たすため直接参照することはありませんが、Color Theminig を活用する場合はこれらの属性をレイアウトやコード上で適宜参照する形になります。

コンポーネント

ピクシブのデザインシステムで定義しているコンポーネントは、Material Components for Android のものをベースに定義されてはいますが、配色のルールはデザインシステム向けに変更しています。

上記の実現方法としては、Widget.MaterialComponents. から始まる Style をベースに、デザインシステム向けに変更した Style を定義し、各所でその Style を利用する形にしています。

Widget.MaterialComponents. から始まる Style では、Color Theminig の属性を参照して色の設定を行なっているため、その Style を利用するだけでも多少はデザインシステムの色が反映されます。

しかし、Material Design とピクシブのデザインシステムでは色の利用方法・種類が異なるため、Color Theminig の設定のみではピクシブのデザインシステムで定義されたコンポーネントを再現することができません。そのままでは意図した配色にならないため手を加える必要があります。

そのため、Color Theming 自体はコンポーネントに色を反映するために補助的に利用し、Color Theming のみで意図した配色にならない箇所については別途カスタマイズしていくことになります。

簡単な一例ですが、MaterialRadioButton をデザインシステム向けにカスタマイズする場合は以下のように Style を設定しています。

styles.xml

<style name="Widget.Pixiv.CompoundButton.RadioButton" parent="Widget.MaterialComponents.CompoundButton.RadioButton">
    <item name="useMaterialThemeColors">false</item>
    <item name="buttonTint">@color/pixiv_compound_button_button_tint_color_selector</item>
</style>

この例では、RadioButton が disabled かつ checked の状態が Material Components 標準の Style と異なります。 適用したときの UI の差分は以下の通りです。枠で囲っている箇所が変更点になります。

MaterialRadioButton などのコンポーネントは、Color Theminig で設定した色を元に色を生成・適用する実装が入っており、Widget.MaterialComponents. から始まる Style ではその実装を利用する設定である useMaterialThemeColors が有効になっています。

デザインシステムの実装を利用する限り、 useMaterialThemeColors の実装を利用する必要がないため無効化しています。RadioButton の例では、この設定を無効化した上で RadioButton に割り当てる色を独自に設定している形になります。

この Style を以下のようにデフォルトの Style として Theme に設定することで、必ず Style が当たるようにしています。

themes.xml

<item name="radioButtonStyle">@style/Widget.Pixiv.CompoundButton.RadioButton</item>

これをコンポーネント毎に実装していくことで、コンポーネント単位でデザインシステムの意図に沿ったダークテーマ対応を行いました。

また、Material Components for Android のダークテーマ向け Theme で有効となっている Elevation Overlay は、ピクシブのデザインシステムでは利用しないため設定を無効化しています。 Elevation Overlay の詳細については公式ドキュメント の Elevation を参照してください。

themes.xml

<item name="elevationOverlayEnabled">false</item>

ここまで書いた内容を、アプリ全体に適用していくことで、ダークテーマ対応を実現しました。

まとめと今後

pixiv Android アプリで、ピクシブのデザインシステムを用いたダークテーマ対応方法について書かせていただきました。

Android アプリ向けのデザインシステムの実装は、実質的に Material Componets for Android のラッパーのような形になったと感じています。 このライブラリに含まれるコンポーネントは、柔軟にカスタマイズすることが可能でかつドキュメンテーションもしっかりしていることもあり、今回書いた内容も苦戦することなく実装でき非常に助かりました。

ちなみに、開発期間としては2019年の9月頃から着手し、2021年の7月末にリリースしました。この期間内に、ダークテーマ対応のみ開発していたわけではないですが、おおよそ2年近く作業していた形になりました。 レビューを除き、設計・実装はほぼ私1人で対応しましたが、なかなかに大変でした。。。

開発が始まった当初から、長期的な開発タスクとなることはわかっていたため、少しずつ開発を進めていくやり方も試行錯誤して進めました。 その内容についても今回書く予定でしたが、思っていたよりも文量が多くなってしまったため、この記事内には載せないことにしました。今回の記事が好評であれば、そちらも公開することがあるかもしれないです。

また、今回は Android View 中心の話でしたが、現在 Jetpack Compose での実装も進めておりますので、機会があればそちらについても何かしら記事を書ければと思っています。

最後に、ピクシブさんのほうでエンジニアを募集しています。この記事を見て気になった方は、以下のリンクも是非ご参照ください。

https://hrmos.co/pages/pixiv/jobs/003hrmos.co

20220112111425
djyugg
フリーランスのソフトウェアエンジニア。ピクシブ株式会社では主に pixiv Android アプリの開発をお手伝いしています。