多重结构嵌套实现可伸缩头部界面

  最近做项目的时候遇到了一个问题,就是做顶部是固定的而下面的内容是RecyclerView构成的列表的界面,在自己使用的时候发现因为头部太大导致下面显示的范围会很小,用户体验比较差。于是就想到了现在很多app都有向下滑动的时候头部会缩小成一个只包含最主要功能的工具栏,于是就想着来找找如何实现。

效果展示

示例.gif
因为某些原因,图像经过特殊处理QWQ
修改自2022/7/5 我永远丢失了这张示例图

参考链接

@aqi00 CSDN 感谢大佬的方法!!

准备工作

  在build.gradle中导入库

implementation 'com.google.android.material:material:1.2.0'

布局文件XML编写

基本原理

  XML文件主要利用CoordinatorLayout嵌套AppBarLayout再嵌套CollapsingToolbarLayout再嵌套Toolbar的布局,听起来十分复杂,但是了解了每一个布局是什么作用理解起来就不是特别困难。

  • CoordinatorLayout:这个布局在这次demo的作用我理解的是将界面头部和下面包含的RecyclerView的滑动操作绑定起来,可以使得滑动后对头部做出相关操作。其中RecyclerView也可以替换为NestedScrollView
  • AppBarLayout:这个布局就是头部所在的布局,通过和RecyclerView一起嵌套在CoordinatorLayout中实现跟随内容视图下拉而展开,跟随内容视图上拉而收缩的效果
  • CollapsingToolbarLayout:这个布局用来包含头部所使用到的布局,以及定义跟随滑动产生的效果
  • Toolbar:定义了收缩前以及收缩后顶部工具栏的布局

实现代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/detail_top_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
            app:contentScrim="#F5F5F5">

            <include
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"
                layout="@layout/detail_expand_information"/>

            <androidx.appcompat.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="50dp"
                app:layout_collapseMode="pin"
                app:contentInsetLeft="0dp"
                app:contentInsetStart="0dp">

                <include
                    android:id="@+id/detail_close_bar"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:visibility="gone"
                    layout="@layout/detail_close_bar" />

            </androidx.appcompat.widget.Toolbar>

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id_detail_post_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="25dp"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

  其中include所包含的布局要自己实现在其他的xml文件中。并且ToolBar布局中的android:layout_height参数是工具栏的高度。

  值得注意的是在CollapsingToolbarLayout布局中的app:contentScrim="#F5F5F5"语句是定义了工具栏位置的背景色,这个背景色必须设置且不能为透明,否则被收缩起来的头部会被透过来。这里的#F5F5F5是安卓默认的背景颜色。

  还有一点就是,因为我的demo在头部展开的情况下不需要工具栏,所以ToolBar布局中只有一个收缩后的布局include,如果需要展开情况下也有工具栏的话则需在include一个布局文件,且不用设置android:visibility参数,且若在展开情况下也显示工具栏的话,需要手动在CollapsingToolbarLayout布局中设置android:layout_marginTop参数,且参数值为ToolBar的高度,以避免布局的重叠。

Java逻辑代码实现

  即使我们是通过include将我们的布局整合到一个布局文件中,但是我们依然可以用老办法来绑定布局组件,例如:

    icon = findViewById(R.id.detail_icon);
    icon_close = findViewById(R.id.detail_close_icon);
    id = findViewById(R.id.detail_id);
    id_close = findViewById(R.id.detail_close_id);
    introduce = findViewById(R.id.detail_introduce);
    fans = findViewById(R.id.detail_fans);
    blog = findViewById(R.id.detail_blog);
    more = findViewById(R.id.detail_more_pic);
    follow = findViewById(R.id.detail_follow_text);
    follow_close = findViewById(R.id.detail_close_follow_text);

  大部分的功能通过布局文件中RecyclerView布局的app:layout_behavior参数可以自动实现,我们这里只要实现ToolBar在不同状态下的切换即可,示例代码如下:

    private AppBarLayout detail_card;
    private View detail_close_bar;

    detail_card = findViewById(R.id.detail_top_bar);
    detail_close_bar = findViewById(R.id.detail_close_bar);

    detail_card.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(AppBarLayout appBarLayout, int i) {
            int offset = Math.abs(i);
            int total = appBarLayout.getTotalScrollRange();

            if (offset <= total / 2) {
                detail_close_bar.setVisibility(View.GONE);
            } else {
                detail_close_bar.setVisibility(View.VISIBLE);
            }
        }
    });

至此就可以实现图上的功能了

美化

  这一部分没有弄明白,似乎是为了双ToolBar时切换达到一个渐变的效果,在我的demo中实现后没有明显的差别,不知道是因为我是单ToolBar的原因,还是因为代码实现有问题。这里直接引用 @aqi00 大佬的代码:

public class AlipayActivity extends AppCompatActivity implements OnOffsetChangedListener {
    private final static String TAG = "AlipayActivity";
    private AppBarLayout abl_bar;
    private View tl_expand, tl_collapse;
    private View v_expand_mask, v_collapse_mask, v_pay_mask;
    private int mMaskColor;
    private RecyclerView rv_content;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_alipay);
        mMaskColor = getResources().getColor(R.color.blue_dark);
        rv_content = (RecyclerView) findViewById(R.id.rv_content);
        rv_content.setLayoutManager(new GridLayoutManager(this, 4));
        rv_content.setAdapter(new LifeAdapter(this, LifeItem.getDefault()));

        abl_bar = (AppBarLayout) findViewById(R.id.abl_bar);
        tl_expand = (View) findViewById(R.id.tl_expand);
        tl_collapse = (View) findViewById(R.id.tl_collapse);
        v_expand_mask = (View) findViewById(R.id.v_expand_mask);
        v_collapse_mask = (View) findViewById(R.id.v_collapse_mask);
        v_pay_mask = (View) findViewById(R.id.v_pay_mask);
        abl_bar.addOnOffsetChangedListener(this);
    }

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        Log.d(TAG, "verticalOffset="+verticalOffset);
        int offset = Math.abs(verticalOffset);
        int total = appBarLayout.getTotalScrollRange();
        int alphaIn = offset;
        int alphaOut = (200-offset)<0?0:200-offset;
        int maskColorIn = Color.argb(alphaIn, Color.red(mMaskColor),
                Color.green(mMaskColor), Color.blue(mMaskColor));
        int maskColorInDouble = Color.argb(alphaIn*2, Color.red(mMaskColor),
                Color.green(mMaskColor), Color.blue(mMaskColor));
        int maskColorOut = Color.argb(alphaOut*2, Color.red(mMaskColor),
                Color.green(mMaskColor), Color.blue(mMaskColor));
        if (offset <= total / 2) {
            tl_expand.setVisibility(View.VISIBLE);
            tl_collapse.setVisibility(View.GONE);
            v_expand_mask.setBackgroundColor(maskColorInDouble);
        } else {
            tl_expand.setVisibility(View.GONE);
            tl_collapse.setVisibility(View.VISIBLE);
            v_collapse_mask.setBackgroundColor(maskColorOut);
        }
        v_pay_mask.setBackgroundColor(maskColorIn);
    }

}

杂谈

  这四个布局其实还没有搞清楚具体的作用,以及使用方法,仅仅只是研究了如何实现本文提到的功能。并且也只是跟着大佬的教程一步一步的实现了而已。如果以后有时间再会去研究每一个布局的详细功能


初めて会ったの日から 僕の心の全てを奪った