2014/10/16

Android:知っておきたい標準API part.1

Intro

Android標準APIの中で知っておくと便利なものがいくつかある.
3rdParty製ライブラリを使うのもいいけれど, 標準で事足りるならそうしたい.
紹介するAPIについてはSupportLibraryを含んでおり, Javaパッケージは対象外.

Useful

LocalBroadcastManager

android.support.v4.content.LocalBroadcastManager
URL: http://developer.android.com/reference/android/support/v4/content/LocalBroadcastManager.html

Helper to register for and send broadcasts of Intents to local objects within your process. This is has a number of advantages over sending global broadcasts with sendBroadcast(Intent):

  • You know that the data you are broadcasting won’t leave your app, so don’t need to worry about leaking private data.
  • It is not possible for other applications to send these broadcasts to your app, so you don’t need to worry about having security holes they can exploit.
  • It is more efficient than sending a global broadcast through the system.

自プロセス内に閉じた, ローカルなBroadcastIntentを投げたい場合に使える. LocalBroadcastManagerを使うことの利点は次の通り.

  • 自プロセスに閉じたBroadcastIntentなのでPrivateデータを送信しても安心
  • 他のアプリケーションがBroadcastIntentを偽ることができないのでエクスプロイト対策にもなる
  • SystemにBroadcastIntentを投げさせるよりも効率的である

StickyなBroadcastIntentが投げられない等, いくつかの制約はある.
(LocalBroadcastManager自体のコードは極めてシンプルなので自分で拡張したほうが早いかも)

Usage

// Receiver登録
LocalBroadcastManager.getInstance(this).registerReceiver(receiver,
      new IntentFilter("custom-action-name"));

// Receiver解除
LocalBroadcastManager.getInstance(this).unregisterReceiver(mMessageReceiver);

// LocalBroadcast送信
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

LruCache

android.support.v4.util.LruCache<K, V>
URL: http://developer.android.com/reference/android/support/v4/util/LruCache.html

Static library version of LruCache. Used to write apps that run on API levels prior to 12. When running on API level 12 or above, this implementation is still used; it does not try to switch to the framework’s implementation. See the framework SDK documentation for a class overview.

LruCache本体がAPI Level12から追加された. API Level12より過去のバージョンでLruCacheを使用したい場合にこのAPIを使用する. 本クラスを使用する場合, API Level12以降でもこちらが参照される(FWでスイッチされるというようなことはない)

Usage

TechBoosterで詳しく紹介されています. TechBooster - LruCacheを使ってメモリキャッシュを実装する

メモリを圧迫しやすいBlobなデータをいい感じ(Least Recently Used)に管理してくれる.
参照されなくなったデータからメモリ解放したい場合に適している.

TimingLogger

android.util.TimingLogger
Added in API level 1
URL: http://developer.android.com/reference/android/util/TimingLogger.html

A utility class to help log timings splits throughout a method call. Typical usage is:

A区間からB区間までのタイミング(処理時間)を計測するのを支援するクラス.

TimingLogger timings = new TimingLogger(TAG, "methodA");
// ... do some work A ...
timings.addSplit("work A");
// ... do some work B ...
timings.addSplit("work B");
// ... do some work C ...
timings.addSplit("work C");
timings.dumpToLog();

The dumpToLog call would add the following to the log:

このコードを実行すると次のようなログを吐く.

D/TAG     ( 3459): methodA: begin     
D/TAG     ( 3459): methodA:      9 ms, work A     
D/TAG     ( 3459): methodA:      1 ms, work B     
D/TAG     ( 3459): methodA:      6 ms, work C     
D/TAG     ( 3459): methodA: end, 16 ms

参考:Android:TimingLoggerで処理間隔をログ出力する

AtomicFile

android.support.v4.util.AtomicFile
URL: http://developer.android.com/reference/android/support/v4/util/AtomicFile.html

Static library support version of the framework’s AtomicFile, a helper class for performing atomic operations on a file by creating a backup file until a write has successfully completed.

ファイル書き込み前にバックアップファイルを作成し, 書き込みに失敗した場合はロールバックすることで, ファイル操作をアトミックなものにするヘルパクラス.

Atomic file guarantees file integrity by ensuring that a file has been completely written and sync’d to disk before removing its backup. As long as the backup file exists, the original file is considered to be invalid (left over from a previous attempt to write the file).

AtomicFileは整合性を保証するためにファイルの書き込みが完了し, ディスクと同期するまでバックアップファイルを削除しない. バックアップファイルが残っている限り, オリジナルファイルは無効と判断する(前回のファイル書き込み処理から残っている状態)

  • オリジナルファイル名:作業対象ファイル
  • バックアップファイル名:オリジナルのバックアップ(オリジナルファイル名 + “.bak”で管理)

Atomic file does not confer any file locking semantics. Do not use this class when the file may be accessed or modified concurrently by multiple threads or processes. The caller is responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.

AtomicFileは単純なR/Wを想定している. ロックセマンティクスを提供していないため, マルチにアクセスされる可能性がある場合は自前でアクセス制御すること. また, AtomicFileによる操作中に(バックアップファイルも含めて)割り込まないように注意すること.

Usage

AtomicFile f = new AtomicFile(new File(...));
FileOutputStream o = null;
try {
    o = file.startWrite();  // バックアップファイル作成
    // TODO: ファイル出力処理
    f.finishWrite(o);       // コミット
} catch (IOException e) {
    f.failWrite(o);         // ロールバック
}

IntentService

android.app.IntentService
Added in API level 3
URL: http://developer.android.com/reference/android/app/IntentService.html

IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) on demand. Clients send requests through startService(Intent) calls; the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work.

IntentServiceはIntentによるリクエストを要求に応じて非同期処理するServiceクラス. クライアントはstartService(Intent)でリクエストを投げる. IntentServiceはworker threadでこれを受けて, 処理が全て完了すると自身で停止する.

This “work queue processor” pattern is commonly used to offload tasks from an application’s main thread. The IntentService class exists to simplify this pattern and take care of the mechanics. To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, and stop the service as appropriate.

work queue processorパターンはアプリケーションのメインスレッドからタスクをオフロードするために使用されるが, これを簡略化したのがIntentServiceである. IntentServiceを使用する場合はonHandleIntent(Intent)を実装する.

All requests are handled on a single worker thread – they may take as long as necessary (and will not block the application’s main loop), but only one request will be processed at a time.

全てのリクエストは single worker threadで処理される. 処理には時間がかかるかもしれない(が, アプリケーションのメインスレッドはブロックされない), しかしリクエストは1つずつ処理される.

Developer Guides
For a detailed discussion about how to create services, read the Services developer guide.

public class MyIntentService extends IntentService{
    public MyIntentService(){
        super("MyIntentService");  // 文字列はworker threadの名前に使われる
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "Intentの処理内容を記載");
    }
}

Messenger

android.os.Messenger
Added in API level 1
URL: http://developer.android.com/reference/android/os/Messenger.html

プロセス間の双方向通信をサポートするService形態にはAIDLとMessengerが存在する.
AIDLとMessengerの主な違いは下記.

AIDL

  • サービスホスト(以降ホスト)は.aidlを作成し, サービスクライアント(以降クライアント)はこれを取り込む必要がある.
  • クライアントからのリクエストはBinderThread経由で, 全てのリクエストが非同期通信となる.
  • ホストは必要に応じてクライアントからのリクエストをスレッドセーフに扱う必要がある.
  • .aidlに変更があった場合, クライアントも合わせてこれを更新する必要がある.

Messenger

  • ホストとクライアント間の通信はHandler-Messageの仕組みで実現されているため.aidlが不要.
  • クライアントからのリクエストはHandler経由で通知され, 全てのリクエストが同期通信となる.
  • ホストとクライアント間のメッセージはMessageオブジェクトで表現される.

Messengerはクライアントからのリクエストをシングルスレッドで処理するため, 基本的にスレッドセーフで動作する. また通信メッセージがMessageオブジェクトで表現されるため.aidlファイルも不要.
外部公開APIに変更があっても, .aidlファイルをクライアントに配布する必要がないため, Service側でMessageの規約(プロトコル)を更新するだけで済む(互換性に注意)

AIDLでは外部公開APIはメソッドとして定義されるが, Messengerでは”Messageオブジェクトの組み立て方”でこれを表現する必要がある.
クライアントはMessengerと通信するためにメッセージの組み立て方を理解する必要がある. (アプリローカルで使用されるHandler-Messageのそれと同じ)

参考: Android:Messengerの基本

// クライアントサイド
public class MessengerClient extends Activity {
    private Messenger mServiceMessenger;
    private Messenger mSelfMessenger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mSelfMessenger = new Messenger(new ResponseHandler());
        mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mServiceMessenger = new Messenger(service);
            }
        };
        bindService(new Intent("yuki.test.messenger.START"),
                mConnection, Service.BIND_AUTO_CREATE);

        ((Button)findViewById(R.id.button1)).setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mServiceMessenger != null) {
                            try {
                                Message msg = Message.obtain(null, 1);
                                msg.replyTo = mSelfMessenger;
                                mServiceMessenger.send(msg);
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
    }

    private class ResponseHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.e("yuki", "handle response=" + msg);
        }
    }
}

// ホストサイド
public class MessengerService extends Service {
    private Messenger mServiceMessenger;

    @Override
    public void onCreate() {
        super.onCreate();
        mServiceMessenger = new Messenger(new RequestHandler());
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mServiceMessenger.getBinder();
    }

    private class RequestHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.e("yuki", "handle request=" + msg);

            if (msg.replyTo != null) {
                try {
                    msg.replyTo.send(Message.obtain()); // send response.
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

DatabaseUtils

android.database.DatabaseUtils
Added in API level 1
URL: http://developer.android.com/reference/android/database/DatabaseUtils.html

データベースとCursorに関するユーティリティメソッドを提供する.

DatabaseUtils.dumpCursor

Cursorの内容をダンプするユーティリティ. dumpCursor後にCursorのpositionは変化しない.

Cursor c = this.getContentResolver().query(
        Browser.BOOKMARKS_URI, new String[]{"_id", "title"},
        null, null, null);
DatabaseUtils.dumpCursor(c);
結果:
I/System.out(2944): >>>>> Dumping cursor 
I/System.out(2944): 0 {
I/System.out(2944):    _id=1dumpCurrentRow (Cursor cursor)
I/System.out(2944):    title=Bookmarks
I/System.out(2944): }
I/System.out(2944): 1 {
I/System.out(2944):    _id=2
I/System.out(2944):    title=Android Developers
I/System.out(2944): }
I/System.out(2944): 2 {
I/System.out(2944):    _id=3
I/System.out(2944):    title=Google
I/System.out(2944): }
I/System.out(2944): <<<<<

DatabaseUtils.concatenateWhere

WHERE句を構築するユーティリティ. 半角スペースや括弧の実装負担を減らせる.

DatabaseUtils.concatenateWhere("criteria1", "criteria2");
結果:
(criteria1) AND (criteria2)

引数1と2は, それぞれ括弧で囲まれた上でAND条件結合される. 片方がnullあるいは空白である場合はもう片方の引数がそのまま返却される.

DatabaseUtils.queryNumEntries

指定されたテーブルに含まれる行数を調べる.

DatabaseUtils.queryNumEntries(db, "test");
結果:
 4 // testテーブルには4レコード存在しているとする

内部ではSELECT count(*)でカラムをカウントしている.

DatabaseUtils.sqlEscapeString

手動でクエリを構築する際にはエスケープ処理に注意する必要がある.
指定される文字列のエスケープ処理にはsqlEscapeStringメソッドが便利.

DatabaseUtils.sqlEscapeString("'");
結果:
 ''''

指定される文字列中にシングルクォートがあるとこれをエスケープする. エスケープ文字列はシングルクォートで囲まれる.

DatabaseUtils.getSqlStatementType

getSqlStatementTypeはSQL文字列のステートメント種別を返却する. 返却される種別は次の定数で定義されている.

  • STATEMENT_SELECT
  • STATEMENT_UPDATE
  • STATEMENT_ATTACH
  • STATEMENT_BEGIN
  • STATEMENT_COMMIT
  • STATEMENT_ABORT
  • STATEMENT_PRAGMA
  • STATEMENT_DDL
  • STATEMENT_UNPREPARED
  • STATEMENT_OTHER
DatabaseUtils.getSqlStatementType("SELECT * FROM bookmarks;");
DatabaseUtils.getSqlStatementType("PRAGMA user_version;");
DatabaseUtils.getSqlStatementType(";");
DatabaseUtils.getSqlStatementType("INSERT SELECT;");
結果:
 1(STATEMENT_SELECT)
 7(STATEMENT_PRAGMA)
 99(STATEMENT_OTHER)
 2(STATEMENT_UPDATE)

4番目の結果を見るとわかるように厳密な解析結果ではない. 単純な解析(頭3文字で判定)しかしてくれないので注意.

以上.

2014/09/21

Android:ImageryHeaderFragment

Intro

Android Developers Blogで紹介されているioschedアプリの機能を一部実装する.
Blog: http://android-developers.blogspot.jp/2014/08/material-design-in-2014-google-io-app.html

実現したい機能のイメージは次の通り. スクロールに追従し, 一定のY座標に到達すると画面上部との隙間を埋める(GapFill)ヘッダバーを実装する.
Imagery Header

First Releaseのデザインではスクロール量によってバナーエリアのα値が変化する. Android Developers Blogの著者はこれについて次のコメントを残している.

Our concern was that this design bent the physics of material design too far. It’s as if the text was sliding along a piece of paper whose transparency changed throughout the animation.

Material Designの重要なファクタである paper と ink の観点から, バナーエリアに描画されたテキストを残す形で透過アニメーションされる点が, Material Designの物理学から外れていると感じたようだ.
これを改善したのがUpdate version. Material Designのpaper と inkの物理学をより推進した形だ. 画像ではわかり辛いが, ヘッダバーエリアの下にコンテンツ詳細が潜り込むため, Z軸の存在を考慮してshadow効果も適用されている.

レイアウト構造は次のようになっている.
Layout structore

実装を始める前に, 今回参考とさせて頂いたサイト, およびリポジトリを紹介する.

また, 今回作成したコードは次のリポジトリで公開している.

ImageryHeaderFragment

Material Designではページのヘッダや背景等にイメージを配置し, コンテキストを表現することを推奨している. 今回作成するImageryHeaderFragmentはこれに倣って画像を配置できる機能を継承する(画像を必ず表示する必要はない). FABについては本コンポーネントの責務から外れるため実装されない. また, “L Preview”のAPIは本Fragmentでは使用しない.

Step by step

実装の手順を紹介する.

  1. Theme, Colorリソースを準備
  2. ImageryHeaderFragmentに必要なView拡張クラスを実装
  3. Layoutリソースを準備
  4. ImageryHeaderFragmentを実装

Step 1: Theme

ImageryHeaderFragment, ActionBarとActivityのためのThemeリソースを定義する.

  • /res/values/colors.xml
<resources>
    <color name="window_background">#eeeeee</color>

    <!-- API v21移行はandroid:colorPrimaryの値に使用 -->
    <color name="colorPrimary">#a00</color>
</resources>
  • /res/values/styles.xml
<!-- ImageryHeader Theme -->
<style name="Theme.ImageryHeader" parent="android:Theme.Holo.Light.DarkActionBar">
    <item name="android:actionBarStyle">@style/Theme.ImageryHeader.ActionBar</item>
    <item name="android:windowBackground">@color/window_background</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowActionBarOverlay">true</item>
</style>

<!-- ImageryHeader Theme Action Bar -->
<style name="Theme.ImageryHeader.ActionBar" parent="android:Widget.Holo.ActionBar">
    <item name="android:displayOptions">showHome</item>
    <item name="android:background">@null</item>
</style>
  • /AndroidManifest.xml
<activity
        android:name=".MainActivity"
        android:theme="@style/Theme.ImageryHeader">

ActionBarはApp iconとAction itemのために使用するため消すことはしない. かわりに背景を透過し, 不要な要素(shadowやApp title)を非表示にしておく.

Step 2: Custom View

ObservableScrollView

ImageryHeaderFragmentでは, 各Viewのポジションをスクロール量によって動的に変化させる. 実現するにはScrollViewのスクロールイベントのリスナーが必要であるが, Android純正のScrollViewにはコールバックリスナを受け付ける仕組みがない. これを実現するためにScrollViewを拡張したObservableScrollViewを実装する.

public class ObservableScrollView extends ScrollView {
    private ArrayList<Callbacks> mCallbacks = new ArrayList<Callbacks>();

    public ObservableScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        for (Callbacks c : mCallbacks) {
            c.onScrollChanged(l - oldl, t - oldt);
        }
    }

    @Override
    public int computeVerticalScrollRange() {
        return super.computeVerticalScrollRange();
    }

    public void addCallbacks(Callbacks listener) {
        if (!mCallbacks.contains(listener)) {
            mCallbacks.add(listener);
        }
    }

    public static interface Callbacks {
        public void onScrollChanged(int deltaX, int deltaY);
    }
}

Original source code:
GitHub - ObservableScrollView

Step 3: Layout

リソースとカスタムViewの準備ができたらlayoutリソースを作成する. 画面構成は大きく3つの要素から成る.

  • Header Image
  • Body
  • Header Bar

3つの要素はレイアウト上, (FragmentLayoutによって)重なるように定義される. 実際には画面のスクロールポジションに従って各ViewのY座標を動的に変えていく.
Header Barの定義位置を最後にしてあるのはz-indexを手前にするためである.

  • /res/layout/fragment_imageryheader.xml
<yuki312.android.imageryheaderfragment.ObservableScrollView
        android:id="@+id/scroll_view"
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true"
        android:overScrollMode="never">

    <FrameLayout
            android:id="@+id/scroll_view_child"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipChildren="false">

        <!-- Header Imagery -->
        <FrameLayout
                android:id="@+id/header_image_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

            <ImageView
                    android:id="@+id/header_image"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"/>
        </FrameLayout>

        <!-- Body -->
        <LinearLayout
                android:id="@+id/body_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/window_background"
                android:clipToPadding="false"
                android:orientation="vertical">

            <!-- TODO: Dummy View for scroll. remove this view.-->
            <TextView
                    android:layout_width="match_parent"
                    android:layout_height="777dp"
                    android:text="I`m Dummy"
                    />
        </LinearLayout>

        <!-- Header bar -->
        <FrameLayout
                android:id="@+id/header_bar_container"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:clipChildren="false"
                android:clipToPadding="false">

            <!-- background -->
            <!-- height assigned dynamically, and fill the ActionBar gaps. -->
            <View
                    android:id="@+id/header_bar_background"
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:background="@color/colorPrimary"/>

            <!-- contents -->
            <LinearLayout
                    android:id="@+id/header_bar_contents"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:paddingBottom="16dp"
                    android:paddingTop="16dp">

                <TextView
                        android:id="@+id/header_bar_title"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:text="Placeholder"/>

            </LinearLayout>

            <!-- shadow -->
            <View
                    android:id="@+id/header_bar_shadow"
                    android:layout_width="match_parent"
                    android:layout_height="6dp"
                    android:layout_gravity="bottom"
                    android:layout_marginBottom="-6dp"
                    android:background="@drawable/bottom_shadow"/>

        </FrameLayout>
    </FrameLayout>
</yuki312.android.imageryheaderfragment.ObservableScrollView>

@id/header_image_container

ヘッダイメージ画像のコンテナ. ioschedアプリではここにscrim効果を付与するアトリビュートも指定している. scrim効果を実装したい場合は次を追加し, scrim画像を用意する.

<FrameLayout
        android:id="@+id/header_image_container"
        ...
        android:foreground="@drawable/photo_banner_scrim">
  • /res/drawable/photo_banner_scrim
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
            android:angle="270"
            android:centerColor="#0000"
            android:centerY="0.3"
            android:endColor="#0000"
            android:startColor="#3000"/>
</shape>

Original source code:
GitHub - photo_banner_scrim.xml

@id/header_bar_background

今回のメインViewとなるHeaderBar. 一定量スクロールされるとGapFillアニメーションする. このViewはActionBarが担っていたブランディングカラーのためにcolorPrimaryを指定する.

@id/header_bar_shadow

一定量スクロールされるとHeaderBarに影を落とす効果を付与する. これはMaterial Designの物理学に従いBodyよりもHeaderBarのpaper要素が手前(Z depth)にあることを表現している.

android:clipChildren

このアトリビュートは, 子Viewの描画領域が自領域外であっても実行するものである. これはヘッダ
バーの画面上部へのGapFillアニメーションやshadow効果の描画に必要になる. このアトリビュートを使えば親Viewのサイズに影響することなくshadow等の効果が実現できる.
これはGoogleI/O 2014でも紹介(15:00~)されている(http://youtu.be/lSH9aKXjgt8). ただしhorrible hackの類いである点に留意する.
Android Lから導入されるlight sourceとshadowのサポートはこれらの問題を解決(構築されたView階層に対してZ軸を考慮した物理的にも正しいshadow効果を描画)する.

Fragment

準備は整った. 各ViewはFramelayoutの効果で全て0座標に位置している. 今の状態でアプリ実行すると次のレイアウトになる.

Layout define only

ここからはImageryHeaderFragmentのソースコードで各Viewの配置を調整していく.

public class ImageryHeaderFragment extends Fragment
        implements ObservableScrollView.Callbacks {
    ...
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ... findViewByID ...
        setupCustomScrolling(rootView);
        return rootView;
    }

    private void setupCustomScrolling(View rootView) {
        scrollView = (ObservableScrollView) rootView.findViewById(R.id.scroll_view);
        scrollView.addCallbacks(this);
        ...
    }

まずはスクロールイベントを検知するために, ObservableScrollView.Callbacksをimplementsする. Fragment.onCreateView()でObservableScrollViewへコールバック登録.

    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener
            = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            recomputeHeaderImageAndScrollingMetrics();
        }
    };

    private void setupCustomScrolling(View rootView) {
        ...
        ViewTreeObserver vto = scrollView.getViewTreeObserver();
        if (vto.isAlive()) {
            vto.addOnGlobalLayoutListener(globalLayoutListener);
        }
    }

    private void recomputeHeaderImageAndScrollingMetrics() {
        final int actionBarSize = calculateActionBarSize();
        headerBarTopClearance = actionBarSize - headerBarContents.getPaddingTop();
        headerBarContentsHeightPixels = headerBarContents.getHeight();

        headerImageHeightPixels = headerBarTopClearance;
        if (showHeaderImage) {
            headerImageHeightPixels =
                    (int) (headerImage.getWidth() / HEADER_IMAGE_ASPECT_RATIO);
            headerImageHeightPixels = Math.min(headerImageHeightPixels,
                    rootView.getHeight() * 2 / 3);
        }

        ViewGroup.LayoutParams lp;
        lp = headerImageContainer.getLayoutParams();
        if (lp.height != headerImageHeightPixels) {
            lp.height = headerImageHeightPixels;
            headerImageContainer.setLayoutParams(lp);
        }

        lp = headerBarBackground.getLayoutParams();
        if (lp.height != headerBarContentsHeightPixels) {
            lp.height = headerBarContentsHeightPixels;
            headerBarBackground.setLayoutParams(lp);
        }

        ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)
                bodyContainer.getLayoutParams();
        if (mlp.topMargin
                 != headerBarContentsHeightPixels + headerImageHeightPixels) {
            mlp.topMargin = headerBarContentsHeightPixels + headerImageHeightPixels;
            bodyContainer.setLayoutParams(mlp);
        }

        onScrollChanged(0, 0); // trigger scroll handling
    }

ObservableScrollViewからViewTreeObserverを取得してGlobalLayoutListenerを登録. ObservableScrollViewのレイアウト構築完了を待つ. レイアウト構築が完了したらheaderImageContainer, bodyContainer, headerBarBackgroundのレイアウトを調整する.

headerBarTopClearance
recomputeHeaderImageAndScrollingMetrics()では, headerBarTopClearance値を設定する. この値はHeaderBarと画面上部まで(あるいはActionBar部の)隙間間隔を表す.
コード上ではActionBarの高さからheaderBarContents.getPaddingTop()つまりHeaderBar領域の上部余白を除いた値を設定することで, HeaderBarのコンテンツ(“Placeholder”)部とActionBarの隙間を小さくしている.

次にImageryHeaderFragmentのコアとなる処理を見る.

    // GapFillアニメ開始位置の調整. 開始位置に"遊び"を持たせる.
    private static final float GAP_FILL_DISTANCE_MULTIPLIER = 1.5f;

    // ヘッダ画像スクロール時のパララックスエフェクト係数
    private static final float HEADER_IMAGE_BACKGROUND_PARALLAX_EFFECT_MULTIPLIER
            = 0.5f;

    @Override
    public void onScrollChanged(int deltaX, int deltaY) {
        final Activity activity = getActivity();
        if (activity == null || activity.isFinishing()) {
            return;
        }

        // Reposition the header bar -- it's normally anchored to the
        // top of the content, but locks to the top of the screen on scroll
        int scrollY = scrollView.getScrollY();

        float newTop = Math.max(headerImageHeightPixels,
                scrollY + headerBarTopClearance);
        headerBarContainer.setTranslationY(newTop);
        headerBarBackground.setPivotY(headerBarContentsHeightPixels);

        int gapFillDistance = 
                (int) (headerBarTopClearance * GAP_FILL_DISTANCE_MULTIPLIER);
        boolean showGapFill = !showHeaderImage || 
                (scrollY > (headerImageHeightPixels - gapFillDistance));
        float desiredHeaderScaleY = showGapFill ?
                ((headerBarContentsHeightPixels + gapFillDistance + 1) * 1f 
                        / headerBarContentsHeightPixels)
                : 1f;
        if (!showHeaderImage) {
            headerBarBackground.setScaleY(desiredHeaderScaleY);
        } else if (gapFillShown != showGapFill) {
            headerBarBackground.animate()
                    .scaleY(desiredHeaderScaleY)
                    .setInterpolator(new DecelerateInterpolator(2f))
                    .setDuration(250)
                    .start();
        }
        gapFillShown = showGapFill;

        // Make a shadow. TODO: Do not need if running on AndroidL
        headerBarShadow.setVisibility(View.VISIBLE);

        if (headerBarTopClearance != 0) {
            // Fill the gap between status bar and header bar with color
            float gapFillProgress = Math.min(Math.max(getProgress(scrollY,
                    headerImageHeightPixels - headerBarTopClearance * 2,
                    headerImageHeightPixels - headerBarTopClearance), 0), 1);
            // TODO: Set elevation properties if running on AndroidL
            headerBarShadow.setAlpha(gapFillProgress);
        }

        // Move background image (parallax effect)
        headerImageContainer.setTranslationY(scrollY *
                HEADER_IMAGE_BACKGROUND_PARALLAX_EFFECT_MULTIPLIER);
    }

onScrollChanged

newTop
HeaderBarの位置を調整する. headerImageHeightPixelsとHeaderBarの隙間(headerBarTopClearance)を考慮したスクロール量(scrollY)とのmaxをとるので, HeaderImageよりもHeaderBarが下に位置することはない.
GAP_FILL_DISTANCE_MULTIPLIER
HeaderBarと画面上部の隙間を埋めるGapFillアニメーションの開始位置に係る定数. ただし, GapFillアニメーション後もHeaderBarはheaderBarTopClearance(隙間間隔)の値に到達するまで移動する点に注意.
画面スクロールのY座標が, headerBarTopClearanceと本係数との乗を超えたときにGapFillアニメーションを開始する. GapFillアニメーションの開始を早めたい場合は本係数を変更する.
gapFillDistance
前述のGAP_FILL_DISTANCE_MULTIPLIERに関係する.

ここではAndroidLが正式リリースされた際の対応をTODOコメントとして残しておく(実際にはAndroidL前後で動作を変えるラッパを用意しておくのが望ましいが, 本件の主旨ではないので割愛.

Run & improvement

これで, MaterialDesignの物理学に従った対応ができた. ここまでの実装でアプリを動作させた場合のレイアウトが下記:

enter image description here

実際に動作させてアニメーションやshadowの具合を確認してみてほしい.
Material Designは他にもTypography(Roboto, Font size/style), Bold(color, FAB), Layout baselineといった項目があり, これだけではMaterial Designに則ったデザインであるとは到底言えないが, 本稿の目的は達成したのでこれ以上は実装しない. 必要であれば残るMaterial Designの要素を実装していく必要がある.

以上.


Copyright 2014 yuki312 All Right Reserved.

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.


License
Portions of this page are modifications based on work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.