使用AppBarLayout实现二次吸顶功能

我开源了一个方便RecyclerView吸顶的Android库,欢迎您访问https://github.com/lizijin/StickyHeaderForRecyclerView,如果您使用本库,请提出您的宝贵意见。

它目前支持以下功能:

  1. 支持单类型吸顶功能
  2. 支持多类型吸顶功能
  3. 支持开启和关闭吸顶功能
  4. 支持指定位置吸顶功能
  5. 支持设置吸顶偏移量
  6. 支持自定义RecyclerView上Item吸顶边界自定义
  7. 可以无缝配合AppBarLayout

开门见山,最近需要实现一个二次吸顶的效果,UE给出的效果图如下

分析如下:

看到吸顶的效果,首先想到使用CoordinatorLayout和AppBarLayout来实现。界面主要分为2个部分:

  1. 底部的可滑动区域,可以用ScrollView或者RecyclerView实现。
  2. 顶部的吸顶区域用AppBarLayout实现。

我们把顶部的吸顶区域拆分一下,可以分为以下4个部分:

  1. 品牌区域,当往下滑的时候,RecyclerView显示第一个Item的时候,该区域才开始往下滑。

  1. 品类图片区域,当下滑的时候,该区域会跟随往下滑动,当该区域到达顶部时会一直吸顶,直到RecyclerView第一个Item出现的时候,才往下滑动。

  1. 品类标题区域,当上滑的时候,该区域会先跟随往上滑动,当该区域到达顶部时会一直吸顶

  1. 排序和筛选区域,当下滑的时候,无论RecyclerView当前的位置在哪里,该区域会首先跟随向下滑出


在本文中,我把向上滑动区域3吸顶,向下滑动区域2吸顶做了一个定义叫二次吸顶,二次吸顶的功能会有一个矛盾点,我们想让区域3在上滑的时候吸顶,那么势必造成区域4无法作为AppBarLayout的一部分(熟悉AppBarLayout的scroll标志的同学应该能理解,如果不能理解,本文后半部分原理篇会讲解到),那么区域4只能做为RecyclerView的头部,作为头部,只有当RecyclerView下滑到顶,区域4才会向下跟随滑出,无法满足上面区域4的要求。

当分析出这样一个矛盾点的时候。我的第一反应是,使用AppBarLayout无法满足现有的要求,我必须得自己重写一个Behavior来实现该功能,而且该Behavior必须满足下面3个条件:

  1. Behavior本身需要处理滑动事件,在吸顶区域滑动时,RecyclerView需要联动
  2. Behavior需要处理RecylerView和吸顶区域之间的嵌套滑动,在RecyclerView区域滑动时,吸顶区域需要联动
  3. 上滑时区域3要吸顶,下滑时区域4要吸顶

一想到只需要满足这么三个条件便能实现效果,迫不及待地跃跃欲试,但是冷静了几秒钟会发现,但是处理滑动事件,不仅需要考虑各种事件类型的处理,嵌套滑动的处理,以及CoordiatorLayout复杂的事件分发逻辑,还有Fling等操作的处理,突然感觉是一个超大的工程量,真的有必要自己手工写一个Behavior吗?难道Google爸爸的AppBarLayout真的就那么肤浅,只能实现一个单一的吸顶功能吗?在手写Behavior之前看来很有必要,看看AppBarLayout内部的实现原理呀,所谓知己知彼方能百战百胜嘛,又所谓知其然,知其所以然。如果AppBarLayout不能实现二次吸顶功能,我们要知道其中的原因,如果能实现二次吸顶的功能,那么我们应该怎么做呢?

经过了三天三夜,不断对AppBarLayout源码的深入研究以及不断的Demo验证,终于发现,原来Google爸爸心思缜密,二次吸顶的功能,早给我们想好了,就等待着我们去发掘。终于在无需手写一行自定义Behavior、无需手写嵌套滑动逻辑的情况下,不辱使命,完成了二次吸顶效果的开发。效果图如下:

代码如下

布局文件

<?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"
    tools:context=".coordinatorlayout.CollapsingToolbarLayoutActivity">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/long_hate_song"></TextView>
    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/pic1"
            android:contentDescription="吸顶区域一"
            android:gravity="center"
            android:scaleType="fitXY"
            app:layout_scrollFlags="scroll" />

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/pic2"
            android:contentDescription="吸顶区域二"
            android:gravity="center"
            app:layout_scrollFlags="enterAlways|scroll"></ImageView>

        <com.peter.viewgrouptutorial.coordinatorlayout.FloatLinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_scrollFlags="enterAlways|scroll|exitUntilCollapsed">


            <ImageView
                android:id="@+id/text.view3"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/pic3"
                android:contentDescription="吸顶区域三"
                android:gravity="center"
                android:text="HEAD3"
                app:layout_pin="true" />

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/pic4"
                android:contentDescription="吸顶区域四"
                android:gravity="center"
                app:layout_pin="false" />

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/pic5"
                android:contentDescription="吸顶区域四"
                app:layout_pin="false" />
        </com.peter.viewgrouptutorial.coordinatorlayout.FloatLinearLayout>
        >
    </com.google.android.material.appbar.AppBarLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

自定义FloatLinearLayout

public class FloatLinearLayout extends LinearLayout {
    int currentOffset;
    private AppBarLayout.OnOffsetChangedListener onOffsetChangedListener;

    public FloatLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setChildrenDrawingOrderEnabled(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (((LayoutParams) child.getLayoutParams()).pin) {
                setMinimumHeight(child.getMeasuredHeight());
                break;
            }
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Add an OnOffsetChangedListener if possible
        final ViewParent parent = getParent();
        if (parent instanceof AppBarLayout) {
            // Copy over from the ABL whether we should fit system windows
            ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));

            if (onOffsetChangedListener == null) {
                onOffsetChangedListener = new FloatLinearLayout.OffsetUpdateListener();
            }
            ((AppBarLayout) parent).addOnOffsetChangedListener(onOffsetChangedListener);

            // We're attached, so lets request an inset dispatch
            ViewCompat.requestApplyInsets(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        // Remove our OnOffsetChangedListener if possible and it exists
        final ViewParent parent = getParent();
        if (onOffsetChangedListener != null && parent instanceof AppBarLayout) {
            ((AppBarLayout) parent).removeOnOffsetChangedListener(onOffsetChangedListener);
        }

        super.onDetachedFromWindow();
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        return childCount - i - 1;
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    public static class LayoutParams extends LinearLayout.LayoutParams {
        boolean pin = false;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FloatLinearLayout_Layout);
            pin =
                    a.getBoolean(
                            R.styleable.FloatLinearLayout_Layout_layout_pin, false);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(int width, int height, float weight) {
            super(width, height, weight);
        }

        public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(LinearLayout.LayoutParams source) {
            super(source);
        }
    }

    private class OffsetUpdateListener implements com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener {
        OffsetUpdateListener() {
        }

        @Override
        public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
            currentOffset = verticalOffset;
            for (int i = 0, z = getChildCount(); i < z; i++) {
                final View child = getChildAt(i);
                final FloatLinearLayout.LayoutParams lp = (FloatLinearLayout.LayoutParams) child.getLayoutParams();
                final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
                if (lp.pin) {
                    int offset = -currentOffset - getTop();
                    offsetHelper.setTopAndBottomOffset(MathUtils.clamp(offset, 0, offset));
                }
            }
        }
    }

    @NonNull
    static ViewOffsetHelper getViewOffsetHelper(@NonNull View view) {
        ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(com.google.android.material.R.id.view_offset_helper);
        if (offsetHelper == null) {
            offsetHelper = new ViewOffsetHelper(view);
            view.setTag(com.google.android.material.R.id.view_offset_helper, offsetHelper);
        }
        return offsetHelper;
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值