• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

YukiMatsumura/ImageryHeader: Gap fill Header bar. few Material Design.

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

YukiMatsumura/ImageryHeader

开源软件地址(OpenSource Url):

https://github.com/YukiMatsumura/ImageryHeader

开源编程语言(OpenSource Language):

Java 89.6%

开源软件介绍(OpenSource Introduction):

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 prope 

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap