Android如何实现超级棒的沉浸式体验

做APP开发的过程中,有很多时候,我们需要实现类似于下面这种沉浸式的体验。

沉浸式体验

一开始接触的时候,似乎大家都会觉这种体验实现起来,会比较困难。难点在于:

  1. 头部的背景图在推上去的过程中,慢慢的变得不可见了,整个区域的颜色变成的暗黑色,然后标题出现了。
  2. StatusBar变的透明,且空间可以被利用起来,看我们的图片就顶到了顶 了。
  3. 我们的viewpager推到actionbar的下方的时候,就固定在了actionbar的下方,不能在往上面推了。
  4. 底部有一个控件,随着列表的向上滑动,它退出视角范围,以便于给出更多的空间来展示列表,其实整个沉浸式体验都是为了给列表留出更多的空间来展示。

好,总结起来以上就是我们的问题,也是需要解决的,一个一个解决了,这种需求也就实现了,那么,我们如何去一步一步来解决以上的问题呢?

1、头部背景和标题的渐隐渐现

首先,我们来分析第一个问题,头部的背景图在推上去的过程中,慢慢的变得不可见了,这种听起来好像是某种collapse,因此,很容易让人想到CollapsingToolbarLayout,如果你想要比较容易的了解CollapsingToolbarLayout

应用,建议看这位兄台的文章,他给也给了一个动画,比较详细的介绍了这个的应用,例如:

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>
效果就是这样

当然,这时候,标题是需要你自己设置渐隐渐现的。那么,我们依据什么呢?

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的套路代码。

按照这个框架来,相信你很容易写出这个样子的布局。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

1 条评论
登录 后参与评论

相关文章

来自专栏向治洪

仿uc下部弹出菜单

先说说我怎么会无聊到这种地步去弄这个代码呢,在今年2月份的时候公司本来要做个这种弹出的菜单的,有5个按钮每个都有一个菜单,记得网上有仿UC菜单的源码,就下下来看...

1828
来自专栏青玉伏案

Android开发之基本控件和详解四种布局方式

Android中的控件的使用方式和iOS中控件的使用方式基本相同,都是事件驱动。给控件添加事件也有接口回调和委托代理的方式。今天这篇博客就总结一下Android...

1845
来自专栏学海无涯

Android开发之Scroller

什么是Scroller? 翻译为弹性滑动对象,可以实现View的弹性滑动动画,与Scroller相关的就是大家比较熟悉的scrollTo和scrollBy方法,...

3365
来自专栏非著名程序员

基础篇章:React Native 之 View 和 Text 的讲解

(友情提示:RN学习,从最基础的开始,大家不要嫌弃太基础,会的同学请自行略过,希望不要耽误已经会的同学的宝贵时间) 从今天开始我们进入基础组件也就是一些简单控件...

2165
来自专栏Android常用基础

自定义View(七)-View的工作原理- Activity的布局加载

前面几篇对动画可以说是做了非常全面的总结了(上篇文章最后的4种ViewGroup相关动画相信在了解基础后看些文章也不会太难理解)。在View的工作原理 这一部分...

1133
来自专栏我就是马云飞

仿bilibili刷新按钮的实现

简述 最近跟小伙伴一起讨论了一下,决定一起仿一个BiliBili的app(包括android端和iOS端),我们并没有打算把这个项目完全做完,毕竟我们的重点是掌...

1878
来自专栏三流程序员的挣扎

Android 动画总结(7) - ViewGroup 子元素间的动画

然后对有子 View 的 ViewGroup 添加这个属性,比如 RecyclerView:

601
来自专栏jianhuicode

Android的FixScrollView自定义控件

需求模仿腾讯课堂视频播放详情页面,效果如图: 1外层滚动控件到顶部,内层控制滚动 2内层滚动到顶部,外层控制滚动 835108-201703311117091...

1928
来自专栏Android干货园

Android带你解析ScrollView--仿QQ空间标题栏渐变

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/52...

1061
来自专栏前端说吧

CSS-垂直|水平居中问题的解决方法总结

3376

扫码关注云+社区