注意! この記事はQiitaにて公開されていた内容をimportしたものです。
これらの内容は場合によっては陳腐化していて役に立たなくなっていたり、有害であったり、現在の著者の主張と異なることがあります。
皆様の判断の上でご利用いただけますと幸いです(度を超してヤバいものは著者に連絡して頂ければ対応します m(_ _)m)


はじめに

Overview Screen(またの名をrecents screenrecent task listrecent appsとも。 → Overview Screen | Android Developers )は、大抵の場合Recentsボタンを押すことで開くことが出来るタスク切り替え画面です。 Lollipopでは、この画面デザインが大きく変わると同時に、アプリのテーマ設定のcolorPrimary値などでアプリ名部分の背景色を定義することが出来るようになっています。

Screenshot_2014-12-04-13-43-23_2.png

ところで、この画面の文字色は、アプリのテーマ設定では指定できません。少なくとも「Overview Screenに表示される自分のアプリの文字色を指定するパラメータ」は見つかりませんでした。 この文字色を変更したいときがあります。そんなとき、自在に文字色を制御することが出来るなら、文字色を制御するための規則があるなら、それを知っておくことでより良くアプリの世界観を表現できることでしょう。

今回は、「Overview Screenのアプリ名などに使われる文字色はどうやって決まるのか?」を解説します。

ルールを探る

コードはあとで読むのですが、ふつうに使っている限り、なんとなく規則性は見えてきます。 (これより説明する「白」「黒」は、「白っぽい色」「黒っぽい色」という意味です。)

  • 背景色に濃い色を使うと文字色は白になる
  • 背景色に薄い色を使うと文字色は黒になる
  • 文字色は、白か黒しか存在しない
  • テーマ設定されていないものはグレーの背景色になり、文字色は黒になる

Activity#setTaskDescription(ActivityManager.TaskDescription)について

謎を解き明かす鍵になるのが、Activity#setTaskDescription(ActivityManager.TaskDescription)です。 このメソッドはJake Wharton神のツイートで知ったのですが:

ということで、Overview Screenのタスクリストのアイコンを独自に定義することができます。 使い方は以下のとおり。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        ActivityManager.TaskDescription taskDescription = new ActivityManager.TaskDescription(
                getString(R.string.app_name),
                ((BitmapDrawable) getDrawable(R.drawable.ic_recents)).getBitmap(),
                getResources().getColor(R.color.General_ThemeColor)
        );
        setTaskDescription(taskDescription);
    }
}

ActivityManager.TaskDescriptionクラスについて

setTaskDescription()に与えるオブジェクトなのですが、setterが@hide指定されているので、コンストラクタで値を付与するしかありません。 コンストラクタの値のそれぞれの意味は以下のとおり:

  • label : このタスクの現在の状況を示す説明
  • icon : このタスクの現在の状況を示すアイコン
  • colorPrimary : テーマのprimaryColorを上書きする色。Opaque(不透明)でなければならない。

拘りがなければ、ラベルはアプリ名で良いと思います。colorPrimaryを指定しない(2引数コンストラクタを使う)と値は0になるんですが、そうなるとテーマ未指定時のグレー色になってしまいました。 アイコンについて、別にic_launcherでもいいのですが、ここはあえてアイコンロゴのシルエットだけのものを用意して使う(先程の例では、これをic_recentsとして定義しました。)と良い感じです。

参考までに、現在のTwitterアプリではどのように使われているかを示します。ランチャーアイコンとは別のリソースが割り当てられていることがわかります。

Screenshot_2014-12-04-13-44-08_2.png

アイコンの話は今回の話とは関係ないですが、ついでに解説しました。

TaskDescription#getPrimaryColor()を追う

「ルールを探る」のとおりであれば、primaryColorの値が文字色を決める手がかりになります。 TaskDescription#getPrimaryColor()をCall Hierarchy機能を使い調べていくと:

packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java - platform/frameworks/base - Git at Google

    /** Returns the activity's primary color. */
    public int getActivityPrimaryColor(ActivityManager.TaskDescription td,
            RecentsConfiguration config) {
        if (td != null && td.getPrimaryColor() != 0) {
            return td.getPrimaryColor();
        }
        return config.taskBarViewDefaultBackgroundColor;
    }

RecentsTaskLoader.java#469

// Load the label, icon, and color
String activityLabel  = getAndUpdateActivityLabel(taskKey, t.taskDescription,
        ssp, infoHandle);
Drawable activityIcon = getAndUpdateActivityIcon(taskKey, t.taskDescription,
        ssp, res, infoHandle, preloadTask);
int activityColor = getActivityPrimaryColor(t.taskDescription, config);

// (略)

// Add the task to the stack
Task task = new Task(taskKey, (t.id > -1), t.affiliatedTaskId, t.affiliatedTaskColor,
        activityLabel, activityIcon, activityColor, (i == (taskCount - 1)),
        config.lockToAppEnabled, icon, iconFilename);

となります。このTaskというクラスは何をするものかよくわかりません。 Taskクラスのコンストラクタは以下。

packages/SystemUI/src/com/android/systemui/recents/model/Task.java - platform/frameworks/base - Git at Google

public Task(TaskKey key, boolean isActive, int taskAffiliation, int taskAffiliationColor,
            String activityTitle, Drawable activityIcon, int colorPrimary,
            boolean lockToThisTask, boolean lockToTaskEnabled, Bitmap icon,
            String iconFilename) {
    boolean isInAffiliationGroup = (taskAffiliation != key.id);
    boolean hasAffiliationGroupColor = isInAffiliationGroup && (taskAffiliationColor != 0);
    this.key = key;
    this.taskAffiliation = taskAffiliation;
    this.taskAffiliationColor = taskAffiliationColor;
    this.activityLabel = activityTitle;
    this.activityIcon = activityIcon;
    this.colorPrimary = hasAffiliationGroupColor ? taskAffiliationColor : colorPrimary;
    this.useLightOnPrimaryColor = Utilities.computeContrastBetweenColors(this.colorPrimary,
            Color.WHITE) > 3f;
    this.isActive = isActive;
    this.lockToThisTask = lockToTaskEnabled && lockToThisTask;
    this.lockToTaskEnabled = lockToTaskEnabled;
    this.icon = icon;
    this.iconFilename = iconFilename;
}

おや、this.colorPrimaryの内容を基に真偽値を出しているthis.useLightOnPrimaryColorがありますね? この値を決めるのに使用されているUtilities.computeContrastBetweenColors(int,int)の返り値が「3以上」だとtrueであることがわかります。

computeContrastBetweenColors()は後回しにして、useLightOnPrimaryColorが本当に文字の白黒に関わっているかどうかを調べます。 (余談:パブリックフィールドなので、Call Hierarchyのような手法が使えなくてつらかった。。。古典的なfind . -type f -print | xargs grep useLightOnPrimaryColor /dev/nullで解決。)

すると、その名もズバリなTaskViewHeader.javaがこの値を使用していました。

packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java - platform/frameworks/base - Git at Google

どうやらuseLightOnPrimaryColorは関係があると断言できますね。

Utilities.computeContrastBetweenColors(int,int) について

このスタティックメソッドは何をするかというと、メソッド名通りなのですが、「与えられた2値のコントラスト(x:1のx部分)を計算して返す」というものです。 つまり、先ほどのロジックでは、「colorPrimaryの値とColor.WHITE(#FFFFFF)のコントラスト比を計算し、3以上なら白、3未満なら黒で描画する」ということになります。

この算出に使用されているアルゴリズムは「CWAG v2」というものだ、と書かれています。 知らなかったのでググったところ、「ウェブコンテンツ・アクセシビリティ・ガイドライン (WCAG) 2.0」というページに辿り着きました。

つまり、由緒あるアルゴリズムなのだということがわかりますね(小学生並みの感想)

これでOverview Screenの文字色の謎はすべて解き明かしました。

文字色の白と黒を自在に決めるためには

上記のサイトで簡単にコントラスト比を導出することができます。 Foregroundに#FFFFFFを入力し、Backgroundに使いたい色を入力することで、コントラスト比がわかります。

まとめ

Overview Screenの文字色というニッチな部分ではありますが、自在に色をコントロールして、アプリの世界観を華麗に表現していきたいですね!