Android如何實現超級棒的沉浸式體驗

brzhang發表於2018-10-25

原文連結  cloud.tencent.com/developer/a…

做APP開發的過程中,有很多時候,我們需要實現類似於下面這種沉浸式的體驗。

Android如何實現超級棒的沉浸式體驗
沉浸式體驗

一開始接觸的時候,似乎大家都會覺這種體驗實現起來,會比較困難。難點在於:

  1. 頭部的背景圖在推上去的過程中,慢慢的變得不可見了,整個區域的顏色變成的暗黑色,然後標題出現了。
  2. StatusBar變的透明,且空間可以被利用起來,看我們的圖片就頂到了頂 了。
  3. 我們的viewpager推到actionbar的下方的時候,就固定在了actionbar的下方,不能在往上面推了。
  4. 底部有一個控制元件,隨著列表的向上滑動,它退出視角範圍,以便於給出更多的空間來展示列表,其實整個沉浸式體驗都是為了給列表留出更多的空間來展示。

好,總結起來以上就是我們的問題,也是需要解決的,一個一個解決了,這種需求也就實現了,那麼,我們如何去一步一步來解決以上的問題呢?

1、頭部背景和標題的漸隱漸現

首先,我們來分析第一個問題,頭部的背景圖在推上去的過程中,慢慢的變得不可見了,這種聽起來好像是某種collapse,因此,很容易讓人想到CollapsingToolbarLayout,如果你想要比較容易的瞭解CollapsingToolbarLayout

應用,建議看這位兄臺的文章,他給也給了一個動畫,比較詳細的介紹了這個的應用,例如:

Android如何實現超級棒的沉浸式體驗
CollapsingToolbarLayout

對於裡面的用法,我這裡不作講解了,但是如果你不瞭解這個佈局的應用,我強烈建議你好好了解一下,才能繼續下面走,只是想說明一下,走到這裡,你有一個坑需要去填,那就是我們的標題動畫可以不是這樣的,而且,還是標題還是居中的,注意,這裡的實現,標題不是居中的,是靠左的,這本來是Android設計規範,但是設計師偏偏不買Android規範的賬,因此,我們必須躺過這個坑,然後,從Stack Overflow上了解到一個issue

<android.support.v7.widget.Toolbar
    android:id="@+id/toolbar_top"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="?attr/actionBarSize"
    android:background="@color/action_bar_bkgnd"
    app:theme="@style/ToolBarTheme" >


     <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Toolbar Title"
        android:layout_gravity="center"
        android:id="@+id/toolbar_title" />


</android.support.v7.widget.Toolbar>複製程式碼

假設,這個方式是可行的,那麼要解決居中的問題後,把返回按鈕改為我們的按鈕樣式,然後,在耍點小詭計,讓title開始是透明的,並且改變返回按鈕的圖片:

collapsingToolbarLayout.setCollapsedTitleTextColor(Color.WHITE);
//collapsingToolbarLayout.setExpandedTitleColor(Color.WHITE);
collapsingToolbarLayout.setExpandedTitleColor(Color.TRANSPARENT);複製程式碼

然而,假設,始終只是一個假設,實際上,這個假設不成立,我在嘗試的時候,發現Toolbar中的TextView根本就不能使用android:layout_gravity="center"這種屬性好吧,即使強行加上,效果也是靠左的。

那麼,如何做,我的解決方式是這樣的

<android.support.design.widget.AppBarLayout
            android:id="@+id/appbarlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:elevation="0dp">

            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsing_tool_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:contentScrim="@color/b_G6"
                app:expandedTitleMarginEnd="10dp"
                app:expandedTitleMarginStart="10dp"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

                <android.support.constraint.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                    <ImageView
                        android:id="@+id/igame_arena_rank_class_header_bg"
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:scaleType="centerCrop"
                        android:src="@drawable/bg_arena_rank_class"
                        app:layout_constraintDimensionRatio="375:156" />
                        .........

                </android.support.constraint.ConstraintLayout>

                <android.support.v7.widget.Toolbar
                    android:id="@+id/common_index_activity_tb_title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:minHeight="?android:attr/actionBarSize"
                    android:visibility="visible"
                    app:contentInsetLeft="0dp"
                    app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin">

                    <include
                        layout="@layout/igame_common_tool_bar"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center" />
                </android.support.v7.widget.Toolbar>


            </android.support.design.widget.CollapsingToolbarLayout>

        </android.support.design.widget.AppBarLayout>複製程式碼

然後,include裡面的佈局是這樣的

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

//*****請注意這個View*******///
    <View
        android:id="@+id/common_index_activity_view_status_bar"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="50dp">

        <TextView
            android:id="@+id/tv_toolbar_bg"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_centerInParent="true"
            tools:background="@color/b_G6" />

        <TextView
            android:id="@+id/common_index_header_tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:textColor="@color/b_G99"
            android:textSize="@dimen/igame_textsize_xl"
            tools:text="這裡是標題" />


        <RelativeLayout
            android:id="@+id/common_index_header_rl_back"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_centerVertical="true"
            android:layout_gravity="center_vertical"
            android:visibility="visible">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerInParent="true"
                android:contentDescription="@string/image_desc"
                android:scaleType="centerInside"
                android:src="@drawable/igame_actionbar_arrow_left" />
        </RelativeLayout>

    </RelativeLayout>
</LinearLayout>
複製程式碼
Android如何實現超級棒的沉浸式體驗
效果就是這樣

當然,這時候,標題是需要你自己設定漸隱漸現的。那麼,我們依據什麼呢?

appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mTitle.setAlpha(-verticalOffset * 1.0f / appBarLayout.getTotalScrollRange());
            }
        });複製程式碼

依據的就是對appBarLayout的監聽。

2、將statusBar變為透明,且利用他的空間來放我們的佈局內容。

 /**
     * 使狀態列透明,並覆蓋狀態列,對API大於19的顯示正常,但小於的介面擴充到狀態列,但狀態列不為透明
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public static void transparentAndCoverStatusBar(Activity activity) {
        //FLAG_LAYOUT_NO_LIMITS這個千萬別用,帶虛擬按鍵的機型會有特別多問題

//        //FLAG_TRANSLUCENT_STATUS要求API大於19
//        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
//        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
//        //FLAG_LAYOUT_NO_LIMITS對API沒有要求
//        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
            window.setNavigationBarColor(Resources.getSystem().getColor(android.R.color.background_dark));
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Window window = activity.getWindow();
            window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }複製程式碼

這裡是在網上找的一個方法,直接呼叫即可,但是API需要大於19,相信目前基本上都滿足吧。請注意,我的AppBarLayout中並沒有這個屬性

android:fitsSystemWindows="true"複製程式碼

如果你加了這個屬性,嘿嘿,statusbar雖然空間可以利用,但是有一個你揮之不去的顏色覆蓋在上面,

然後,你還記得上面那個佈局中

//*****請注意這個View*******///
    <View
        android:id="@+id/common_index_activity_view_status_bar"
        android:layout_width="match_parent"
        android:layout_height="0dp" />複製程式碼

這個作用可大了,就是為了對status_bar原始空間做偏移的,在程式碼中,需要動態的改變這個View的高度為statusBar的高度,怎麼獲取:

/**
     * 獲取狀態列高度
     *
     * @param context context
     * @return 狀態列高度
     */
    public static int getStatusBarHeight(Context context) {
        // 獲得狀態列高度
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        return context.getResources().getDimensionPixelSize(resourceId);
    }
複製程式碼

完了之後,還需要設定我們自己塞進去的那個toolbar的高度為toolbar的高度加上StatusBar的高度。

3、ViewPager推到actionbar下面就不讓在推了

這個其實需要你CollapsingToolbarLayout裡面有一個子view是要使用pin模式的,那麼這個子view是誰,顯然就是那個toolbar了

<android.support.v7.widget.Toolbar
                    android:id="@+id/common_index_activity_tb_title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:minHeight="?android:attr/actionBarSize"
                    android:visibility="visible"
                    app:contentInsetLeft="0dp"
                    app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin">

                    <include
                        layout="@layout/igame_common_tool_bar"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center" />
                </android.support.v7.widget.Toolbar>
複製程式碼

4、底部控制元件隨著列表的滑動漸漸隱藏

可以看到,底部的控制元件是覆蓋在列表上的,列表向上滑動的時候,把他隱藏,就可以空出更多的控制元件看列表。那麼,如何做呢?

既然,我們是包裹在CoordinatorLayout中,那麼,顯然,最好的方式是使用layout_behavior了,我這裡實現了一個BottomBehavior:

public class BottomBehavior extends CoordinatorLayout.Behavior {
    private int id;
    private float bottomPadding;
    private int screenWidth;
    private float designWidth = 375.0f;//設計檢視的寬度,通常是375dp,

    public BottomBehavior() {
        super();
    }

    public BottomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        screenWidth = getScreenWidth(context);
        TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.BottomBehavior);
        id = typedArray.getResourceId(R.styleable.BottomBehavior_anchor_id, -1);
        bottomPadding = typedArray.getFloat(R.styleable.BottomBehavior_bottom_padding, 0f);
        typedArray.recycle();
    }

    @Override
    public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
        params.dodgeInsetEdges = Gravity.BOTTOM;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == id;
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        child.setTranslationY(-(dependency.getTop() - (screenWidth * bottomPadding / designWidth)));
        Log.e("BottomBehavior", "layoutDependsOn() called with: parent = [" + dependency.getTop());
        return true;
    }


    public static int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = null;
        if (wm != null) {
            display = wm.getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            int width = size.x;
//            int height = size.y;
            return width;
        }
        return 0;
    }
}複製程式碼

這個裡面有兩個自定義屬性,id,bottomPadding,id表示基於哪個控制元件的相對位置改變,我這打算基於viewpager

這個控制元件,看原始碼可以知道,只有當onDependentViewChanged返回ture時,layoutDependsOn才會被回撥。bottomPadding是表示一個初始的偏移,因為viewpager本身不是頂在螢幕頂端的(開始被圖片佔據了一部分控制元件),因此,需要扣除這部分佔有。

同理,加入讓你實現一個懸浮在左側,右側,滑動隱藏,停止顯示的,也都可以參考類似Behavior的方式,減少程式碼耦合。

總結

最後整個佈局是這樣子的

<?xml version="1.0" encoding="utf-8"?>
<com.tencent.igame.view.common.widget.IGameRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/igame_competition_detail_fragment_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:id="@+id/appbarlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:elevation="0dp">

            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsing_tool_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:contentScrim="@color/b_G6"
                app:expandedTitleMarginEnd="10dp"
                app:expandedTitleMarginStart="10dp"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

                <android.support.constraint.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                    <ImageView
                        android:id="@+id/igame_arena_rank_class_header_bg"
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:scaleType="centerCrop"
                        android:src="@drawable/bg_arena_rank_class"
                        app:layout_constraintDimensionRatio="375:156" />
                        ............

                </android.support.constraint.ConstraintLayout>

                <android.support.v7.widget.Toolbar
                    android:id="@+id/common_index_activity_tb_title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:minHeight="?android:attr/actionBarSize"
                    android:visibility="visible"
                    app:contentInsetLeft="0dp"
                    app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin">

                    <include
                        layout="@layout/igame_common_tool_bar"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center" />
                </android.support.v7.widget.Toolbar>


            </android.support.design.widget.CollapsingToolbarLayout>

        </android.support.design.widget.AppBarLayout>

        <com.tencent.igame.widget.viewpager.IgameViewPager
            android:id="@+id/igame_arena_rank_class_vp_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_gravity="bottom"
            android:background="@color/b_G6"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            app:anchor_id="@+id/igame_arena_rank_class_vp_content"
            app:bottom_padding="156.0"
            app:layout_behavior="com.tencent.igame.common.widget.BottomBehavior">
..........底部佈局

        </android.support.constraint.ConstraintLayout>

    </android.support.design.widget.CoordinatorLayout>

</com.tencent.igame.view.common.widget.IGameRefreshLayout>
複製程式碼

注:IGameRefreshLayout實際上就是封裝的PullToRefreshView,IgameViewPager是我們封裝的Viewpager,減少每次寫Viewpager的套路程式碼。

按照這個框架來,相信你很容易寫出這個樣子的佈局。


相關文章