Android技能树点亮计划-仿天猫商详联合滑动效果实现

3,962 阅读4分钟

背景

实现一个类似天猫sku选择页的滑动效果
ijj2p-owuih.gif

实现

基于AppBarLayout+CollapsingToolbarLayout实现

相关基础能力

�CoordinatorLayout�

他是一个高级版的FrameLayout容器,他可以通过Behavior处理内部View之间的嵌套滑动

Behavior

他通过关联联动View和被联动View,在联动View发生滑动时,通知Behavior从而触发被联动View的响应,他主要包含以下几个方法:

  • onStartNestedScroll:嵌套滑动开始,一般通过parent view的类型确定behavior是否要监听滑动
  • onNestedPreScroll:嵌套滑动中,要监听的parentView 即将开始滑动,滑动事件还未消费
  • onNestedScroll:嵌套滑动中,要监听的parentView 即将开始滑动,滑动事件已消费
  • onStopNestedScroll:嵌套滑动结束

AppBarLayout

AppBarlayout默认实现了AppBarLayout.Behavior,当与其组合的滚动布局(RecycleView、NestedScrollView等)设置了app:layout_behavior="@string/appbar_scrolling_view_behavior",他就根据设置的滚动标识(app:layout_scrollFlags)响应滚动布局的滚动做出响应。
滚动标识有:

  • scroll:布局的滚动与滚动事件直接相关。需要设置此标志才能使下面的其他标志生效。
  • exitUntilCollapsed:当你给布局定义了一个minHeight,此布局将在滚动到达这个最小高度的时候折叠。
  • enterAlways:任何的向下滚动都会让view出现,这个标识通常用于“快速返回”模式
  • enterAlwaysCollapsed:当你给布局定义了一个minHeight并且设置了enterAlways,那么在向下滑动过程中view会展示为最小高度,直到划到顶部在展开
  • snap:当滚动结束的时候,如果布局是部分可见的,那么他会滚动到完全收缩或者展开,可以和exitUntilCollapsed共同使用收缩到一定高度

CollapsingToolbarLayout

CollapsingToolbarLayout 是 Toolbar 的一个包装器,它实现了一个折叠的应用栏。它旨在用作 AppBarLayout 的直接子级。
折叠方式有:

  • **off**:这个是默认属性,布局将正常显示,没有折叠的行为。
  • **pin**:CollapsingToolbarLayout折叠后,此布局将固定在顶部。
  • **parallax**:CollapsingToolbarLayout折叠时,此布局将会以视差方式移动
    • 结合app:layout_collapseParallaxMultiplier,可以设置视差滚动乘数。0表示完全没有移动,1表示正常滚动移动

布局

通过CoordinatorLayout实现顶部的AppbarLayout和底部的RecycleView联合滑动,滑动的过程中RecycleView通知AppbarLayout滑动的距离,appbarLayout操纵其中的ImageView进行缩放,大致页面结构如下

实现

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_light"
    android:fitsSystemWindows="true">


    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:elevation="0dp">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:minHeight="100dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <View
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@android:color/white"
                app:layout_collapseMode="pin" />

            <ImageView
                android:id="@+id/image"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitCenter"
                android:src="@drawable/xiana"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="1" />

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

    </androidx.recyclerview.widget.RecyclerView>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • 监听appbarLyaout的滑动偏移量,控制ImageView进行缩放
 private void initAppBarLayout() {
        AppBarLayout appBarLayout = findViewById(R.id.appbar);
        ImageView imageView = findViewById(R.id.image);
        // 把锚点放到左上角
        imageView.setPivotX(0);
        imageView.setPivotY(0);
        //显示的调用invalidate
        imageView.invalidate();
        imageView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "onClick: ");
            }
        });
        appBarLayout.addOnOffsetChangedListener(new OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                // 第一次滑动的时候记录图片的原始高度
                if (imageOriginHeight == 0) {
                    imageOriginHeight = imageView.getMeasuredHeight();

                }
                // 根据滑动的距离缩放图片
                float newHeight = imageOriginHeight + verticalOffset;
                float scale = newHeight / imageOriginHeight;
                ViewCompat.setScaleY(imageView, scale);
                ViewCompat.setScaleX(imageView, scale);
            }

        });
    }

基于BottomSheetBehavior实现

相关基础能力

BottomSheetBehavior

他是CoordinatorLayout子View使用的Behavior的一种,用作底部工作表,主要支持了多段的手势下滑关闭,在BottomSheetDialog和BottomSheetDialogFragment都有使用。
他比较重要的几个属性有

  • state:bottomSheet的状态,包含以下几种(默认初始状态为STATE_COLLAPSED)
    • STATE_COLLAPSED:折叠状态
    • STATE_HALF_EXPANDED:半展开状态
    • STATE_EXPANDED:展开状态
    • STATE_HIDDEN:隐藏状态
    • STATE_DRAGGING:滑动中状态
    • STATE_SETTLING:停止状态
  • peekHeight:当bottomSheet为STATE_COLLAPSED时的高度
  • hideable:bottomSheet是否从STATE_COLLAPSED状态滑动到完全隐藏
  • fitToContents
    • true:bottomSheet根据内容自适应展开和折叠状态的高度
    • false:bottomSheet根据用户自己来配置展开,半展开,折叠的高度
  • halfExpandedRatio:需要fitToContents为false才能使用,设置bottomSheet HALF_EXPANDED状态下的比例,默认为0.5
  • expandedOffset:需要fitToContents为false才能使用,设置bottomSheet EXPANDED状态下距离顶部的偏移量(注意:这个高度会包含Android的状态栏,所以设置时要手动加上状态栏高度才能正确)

参考文档

布局

实现

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_light"
    android:fitsSystemWindows="true">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="@dimen/image_max_height"
        android:scaleType="fitCenter"
        android:src="@drawable/xiana" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

    </androidx.recyclerview.widget.RecyclerView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • 设置bottomSheet的基础属性并且监听滑动回调实现图片缩放
 private void initBottomSheet() {
        ImageView imageView = findViewById(R.id.image);
        // 把锚点放到左上角
        imageView.setPivotX(0);
        imageView.setPivotY(0);
        //显示的调用invalidate
        imageView.invalidate();

        float imageMinHeight = getResources().getDimension(R.dimen.image_mini_height);
        float imageMaxHeight = getResources().getDimension(R.dimen.image_max_height);
        int screenHeight = ScreenTools.getScreenHeight(this);

        BottomSheetBehavior behavior = BottomSheetBehavior.from(findViewById(R.id.recyclerView));
        // 设置折叠状态下的高度
        behavior.setPeekHeight((int)(screenHeight - imageMaxHeight));
        // 设置展开状态下的高度
        behavior.setExpandedOffset((int)imageMinHeight + ScreenTools.getStatusBarHeight(this));
        behavior.setFitToContents(false);
        // 监听bottomSheet的滑动
        behavior.addBottomSheetCallback(new BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                ViewCompat.setScaleY(imageView, 1 - slideOffset * 0.5f);
                ViewCompat.setScaleX(imageView, 1 - slideOffset * 0.5f);
            }
        });
    }

踩坑时发现tips

  • 手动展开/收起appbarLayout
// 第一个参数true为展开,false为收起,第二个参数为是否需要有动画
appBarLayout.setExpanded(true, true);
  • 判断RecycleView是否滑动到顶部
// true为可滑动,fasle为滑动到顶部
view.canScrollVertically(-1)

其他

git仓库地址
Android技能树点亮计划Git库
Android技能树点亮计划-语雀文档库
稀土掘金:悠二
Github:PettyWing