使用AppBarLayout实现二次吸顶功能

简介: 使用AppBarLayout实现二次吸顶功能

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


image.png

分析如下:


看到吸顶的效果,首先想到使用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;
    }
}



相关文章
|
11月前
|
Java Android开发 开发者
Android使用zxing生成二维码
这是一篇关于如何在Android应用中生成二维码的教程。首先,需要导入zxing库的jar包。布局文件中包含一个按钮、一个图片控件和一个输入框。用户可以在输入框中输入想要转换为二维码的内容,点击按钮后,程序会通过实例化QRCodeWriter类并使用for循环绘制二维码图像,最后将生成的二维码显示在ImageView上。源码展示了具体的实现细节,包括布局定义与Java逻辑代码,便于开发者理解和实践。
276 2
|
XML Android开发 UED
|
Python
变量名能用中文吗_汉语拼音变量名_蛇形命名法_驼峰命名法
本文探讨了变量命名规范,包括汉语拼音、中文和英文变量名的使用。主要内容如下: 1. **回顾上次内容**:介绍了命名法(如大驼峰、小驼峰、蛇形命名法)。 2. **Python命名规范**:常量用全大写加下划线(如`MATH_PI`),类名和类型名用大驼峰(如`MyClass`),异常名也用大驼峰(如`NameError`)。 3. **拼音变量名**:虽然可以使用拼音缩写或全拼,但易读性较差,建议避免。 4. **中文变量名**:Python 3 支持中文作为变量名,但不推荐广泛使用,因其不利于国际合作。
1002 5
|
XML Java API
Android原生TabLayout使用全解析,看这篇就够了
Android原生TabLayout使用全解析,看这篇就够了
2759 0
Android原生TabLayout使用全解析,看这篇就够了
|
Android开发
DialogFragment 使用指南:几个小问题的解法
DialogFragment是Android中用于创建弹窗的特殊Fragment,继承自Fragment。使用步骤包括:1. 创建子类,2. 在onCreateView加载布局,3. onViewCreated初始化控件,4. 通过show方法显示。示例代码展示了一个基本的DialogFragment及其布局。此外,文中还解答了三个常见问题:如何设置弹窗宽度为match_parent,如何使弹窗位于屏幕底部,以及如何去除弹窗四周的默认padding。每个问题都提供了相应的解决方案,涉及在onStart中调整窗口参数和设置自定义样式。
1814 2
DialogFragment 使用指南:几个小问题的解法
|
Cloud Native
云原生架构之X无限延伸:跨AZ、跨Region、跨Cloud,一文让你彻底解锁!
【8月更文挑战第25天】在云原生架构中,可扩展性至关重要,它确保了应用能按需高效调整资源。本文聚焦于三种扩展策略:跨AZ、跨Region及跨云扩展。跨AZ扩展通过在同一云内部不同可用区间部署应用副本增强容错性;跨Region扩展则通过不同地理区域的应用副本部署提升全球访问性能与可靠性;而跨云扩展则利用多云环境进一步加强应用的弹性和覆盖范围。文中提供了基于AWS CloudFormation的具体实践示例,帮助读者深入理解这些扩展机制的实际应用。
932 2
|
XML Java Android开发
14. 【Android教程】文本输入框 EditText
14. 【Android教程】文本输入框 EditText
1771 2
|
存储 设计模式 ARouter
组件化框架 ARouter 完全解析(一)
组件化框架 ARouter 完全解析(一)
804 2
|
Android开发
Android仿高德首页三段式滑动
Android仿高德首页三段式滑动
500 0
|
Android开发
Android RecyclerView实现吸顶动态效果,详细分析
Android RecyclerView实现吸顶动态效果,详细分析