实现二次吸顶效果

简介: 实现二次吸顶效果

1. 效果图


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


image.png


2. 实现思路


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


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

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

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

640.png

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

640.png

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

640.png

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

640.png

640.png

「在本文中,我把向上滑动区域3吸顶,向下滑动区域4吸顶做了一个定义叫二次吸顶。二次吸顶的功能会有一个矛盾点,我们想让区域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、无需手写嵌套滑动逻辑的情况下,不辱使命,完成了二次吸顶效果的开发。效果图如下:


image.png

代码如下


布局文件


<?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;
    }
}
相关文章
|
6月前
|
移动开发 前端开发
H5长页面禁止垂直滚动
H5长页面禁止垂直滚动
53 0
|
6月前
|
前端开发
动态水滴页面
动态水滴页面
39 0
|
3月前
|
前端开发 UED
解决margin重叠问题,切页面更加丝滑
解决margin重叠问题,切页面更加丝滑
|
前端开发 JavaScript 容器
JavaScrpit如何实现弹出遮罩层后将页面固定到当前位置且无法继续滚动
JavaScrpit如何实现弹出遮罩层后将页面固定到当前位置且无法继续滚动
72 0
uniapp滚动条置顶效果、自定义页面滚动条的位置(整理)
uniapp滚动条置顶效果、自定义页面滚动条的位置(整理)
|
XML 存储 缓存
底部导航栏的几种实现方式
底部导航栏的几种实现方式
328 0
|
iOS开发
iOS开发 - 渐变导航条升级版(判断滚动的方向和改变方向时的位置)
iOS开发 - 渐变导航条升级版(判断滚动的方向和改变方向时的位置)
135 0
iOS开发 - 渐变导航条升级版(判断滚动的方向和改变方向时的位置)
|
移动开发 前端开发
【笔记】H5长页面禁止垂直滚动
H5长页面禁止垂直滚动
295 0
|
JavaScript 前端开发 开发者
分类页 -iscroll 区域滚动|学习笔记
快速学习 分类页 -iscroll 区域滚动
100 0
|
移动开发 iOS开发
移动端阻止弹窗下层页面被滑动方法介绍
移动端阻止弹窗下层页面被滑动方法介绍