techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Android-ObservableScrollViewでパララックスなスクロールを実装する

Android-ObservableScrollViewはScrollViewやListViewなどのスクロール可能なウィジェットのスクロール状態を取得して、スクロールに関するイベントをハンドリングできるようにするためのライブラリです。

このAndroid-ObservableScrollViewを使用することで、Google Play Storeのような、スクロール量に応じてToolbarの表示/非表示や各アプリ詳細画面のヘッダイメージの表示/非表示の切り替えを簡単に実装することができます。今回はプレイストアのアプリ詳細画面のような、パララックスなスクロールを実装してみます。

このライブラリはサンプルが充実しており、パララックスの実装もgithub上で詳しい解説があります。ただし若干内容が古い箇所もあるため、サンプルを参考に一部変更しています。

公式のサンプルは以下を参照してください。ちなみに作者の方がQiitaで記事を公開されていますが、そちらも内容が充実しているので一度目を通しておくのがよいと思います。

Android-ObservableScrollView/parallax-image.md at master · ksoichiro/Android-ObservableScrollView · GitHub

Google Playストアアプリのようにスクロール時にActionBarやToolbarの表示を切り替える - Qiita

導入

現在の最新バージョンは1.6.0のようなので、例のごとく以下の記述をbuild.gradledependenciesに追加します。

compile 'com.github.ksoichiro:android-observablescrollview:1.6.0'

レイアウト定義

基本的なレイアウト構成は以下のとおりです。以下は公式サンプルからの引用です*1

<FrameLayout>
  <ObservableScrollView>
    <RelativeLayout>
      <ImageView/>
      <View/>
      <TextView/>
    </RelativeLayout>
  </ObservableScrollView>
  <Toolbar/>
</FrameLayout>

ルート要素のFrameLayoutの下にObservableScrollViewToolbarを配置する構成です。ObservableScrollView以下には1つのウィジェットしか置けないため、メインコンテンツとなるImageViewTextView(TextViewじゃなくても任意の要素で良い)を配置するためにRelativeLayoutでラップします。

ImageViewTextViewの間にあるViewが今回のポイントで、このViewをアンカーとすることで画像とテキスト部分のスクロール量を独立してセットできるようになります。

スクロール部分を抜粋すると以下のようになります。

    <com.github.ksoichiro.android.observablescrollview.ObservableScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/image_header"
                android:layout_width="match_parent"
                android:layout_height="@dimen/parallax_image_height"
                android:scaleType="centerCrop"
                android:src="@drawable/sample" />

            <View
                android:id="@+id/anchor"
                android:layout_width="match_parent"
                android:layout_height="@dimen/parallax_image_height"
                android:minHeight="@dimen/parallax_image_height" />

            <TextView
                android:id="@+id/tv_main"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/anchor"
                android:background="@android:color/white"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:text="@string/sample_text" />

        </RelativeLayout>
    </com.github.ksoichiro.android.observablescrollview.ObservableScrollView>

</FrameLayout>

ポイントはImageViewViewの高さを同じにすることと、TextViewlayout_below=@id/anchorを指定することです。これにより、テキストのスクロールが画像ではなく画像と全く同じサイズの透明なViewに追従するようになります。

後ほど説明しますが、実際にスクロールが発生した際に画像部分のスクロール量を小さくすることで、テキスト部分が画像に覆いかぶさる様にスクロールし、パララックス効果を生み出します。

Toolbar部分の定義は以下のようにします。公式サンプル作成時はビューの重なりを表現するためのelevationが使えなかったらしいためグラデーション用のビューが追加されていたようですが、現在は問題なく使用できるため直接指定しています。

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:elevation="8dp"
        android:minHeight="?attr/actionBarSize"
        app:titleTextColor="@android:color/white" />

パララックスの実装

Activity側で必要なのはObservableScrollViewCallbacksを実装して、スクロール状態の変化時に画像部分のスクロール量を小さくする処理と、スクロール量に合わせてToolbarのalpha値を変更する処理です。

実際のActivityの実装は以下のようになります。

public class MainActivity extends AppCompatActivity implements ObservableScrollViewCallbacks {
    @InjectView(R.id.image_header)
    ImageView mHeaderImage;

    @InjectView(R.id.toolbar)
    Toolbar mToolbar;

    @InjectView(R.id.scrollView)
    ObservableScrollView mScrollView;

    private int mParallaxHeight;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.inject(this);
        setSupportActionBar(mToolbar);

        mScrollView.setScrollViewCallbacks(this);

        // Toolbarの背景色をalpha=0でセットする
        mToolbar.setBackgroundColor(
                ScrollUtils.getColorWithAlpha(0, getResources().getColor(R.color.colorPrimary)));

        // 画像領域の高さを取得
        mParallaxHeight = getResources().getDimensionPixelSize(R.dimen.parallax_image_height);
    }

    @Override
    public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) {
        int baseColor = getResources().getColor(R.color.colorPrimary);

        // 現在のスクロール量からalpha値を計算する(0~1.0)
        float alpha = Math.min(1, (float) scrollY / mParallaxHeight);
        mToolbar.setBackgroundColor(ScrollUtils.getColorWithAlpha(alpha, baseColor));

        mHeaderImage.setTranslationY(scrollY / 2);
    }

    @Override
    public void onDownMotionEvent() {

    }

    @Override
    public void onUpOrCancelMotionEvent(ScrollState scrollState) {

    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        onScrollChanged(mScrollView.getCurrentScrollY(), false, false);
    }
}

全くスクロールしていない状態ではToolbarの背景は透明で、下方向にスクロールすることで、画像の移動とともに徐々にToolbarの背景色が表示されるようになっています。

onScrollChangedはスクロールイベントが発生するごとに実行されます。ここでToolbarの背景色のalpha値を0~1.0の間で計算してセットすることで、スクロール位置に応じたToolbarの表示/非表示を実現しています。パララックス効果は単純に同メソッド内でスクロール量の半分だけ画像部分を動かす(scrollY / 2)ことで実現しています。

Toolbarの表示切替と画像のスクロール量の変更を行うことで、Play Store風のパララックススクロールが比較的簡単に実装できました。この状態でアプリを起動すると以下のようになります。これが起動直後。

f:id:snishimura0926:20160508051710p:plain

下方向にスクロールすると、画像部分が少しずつスクロールされていき、それに応じて徐々にToolbarが表示されるのが分かります。

f:id:snishimura0926:20160508051857p:plain

サンプルコード

今回のサンプルプロジェクトは以下で公開しています。細かい部分は実際のサンプルをダウンロードして確認してください。

なおToolbarを使用する場合はActionBarを使用しないテーマになっていないとToolbarが2つ表示されるような状態になるため注意してください*2

*1:引用元でも触れられているが、ルート要素は別にFrameLayoutではなくRelativeLayoutでももちろんよい

*2:ちなみにToolbarとActionBarの違いについてはこのページが分かりやすかったです: MaterialDesignことはじめ ActionBar編 - Qiita