使用CollapsingToolbarLayout高仿稀土掘金个人中心页

4,632 阅读3分钟

前言

CollapsingToolbarLayout是android MaterialDeign提供的一个组件,通过搭配AppBarLayout可实现toolbar的折叠效果。下边就通过仿实现稀土掘金个人中心页来讲解它的具体用法。先上效果图:

demo.gif

实现

我们将toolbar分为两层:

toolbar分层.png

  • 红色圈中的大模块,暂名:A
  • 绿色圈中的模块,暂名:B 通过分层再分析下效果:
  • 当视图向上滚动时,A会逐渐折叠
  • 当A完全折叠后,B的图标会更新状态,并且用户的信息会向上浮动显示在B中

微信截图_20211202153506.png

  • 当A没有完全折叠后,用户信息会消失,并且B中的图标会恢复原来的状态

通过效果分析,我们可以这么设计:

  • 将A内容包含在CollapsingToolbarLayout中,再配合CoordinatorLayout+AppBarLayout,即可实现A内容的折叠
  • 监听A内容是否折叠,改变B的状态

CollapsingToolbarLayout折叠事件的监听

  1. 通过AppBarLayout.OnOffsetChangedListener的回调
Interface definition for a callback to be invoked when an AppBarLayout's vertical offset changes.
// TODO(b/76413401): update this interface after the widget migration
public interface OnOffsetChangedListener extends BaseOnOffsetChangedListener<AppBarLayout> {
  void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
}

查看官方api文档,该回调可以获取到AppBarLayout的垂直偏移量。通过判断偏移量的大小,即可获取到当前toolbar的状态

abstract class AppBarStateChangeListener : AppBarLayout.OnOffsetChangedListener {

    enum class State {
        EXPANDED, COLLAPSED, IDLE
    }

    private var mCurrentState = State.IDLE

    override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
        if (verticalOffset == 0) {
            if (mCurrentState != State.EXPANDED) {
                onStateChanged(appBarLayout, State.EXPANDED);
            }
            mCurrentState = State.EXPANDED
        } else if (appBarLayout != null) {
            if (Math.abs(verticalOffset) >= appBarLayout.totalScrollRange) {
                if (mCurrentState != State.COLLAPSED) {
                    onStateChanged(appBarLayout, State.COLLAPSED);
                }
                mCurrentState = State.COLLAPSED
            } else {
                if (mCurrentState != State.IDLE) {
                    onStateChanged(appBarLayout, State.IDLE);
                }
                mCurrentState = State.IDLE;
            }
        }
    }

    abstract fun onStateChanged(appBarLayout: AppBarLayout?, state: State)
}
  1. 通过CollapsingToolbarLayout的setScrimsShown函数
Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the vertical scroll may overwrite this value.
Params:
shown – whether the scrims should be shown
animate – whether to animate the visibility change
See Also:getStatusBarScrim(), getContentScrim()

public void setScrimsShown(boolean shown, boolean animate) {
  if (scrimsAreShown != shown) {
    if (animate) {
      animateScrim(shown ? 0xFF : 0x0);
    } else {
      setScrimAlpha(shown ? 0xFF : 0x0);
    }
    scrimsAreShown = shown;
  }
}

查看官方api文档,可以看到当内容发生变化时,该方法会被自动调用,因此我们可以通过扩展该方法

class NestCollapsingToolbarLayout : CollapsingToolbarLayout {

    private var mIsScrimsShown: Boolean = false
    private lateinit var scrimsShowListener: OnScrimsShowListener

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun setScrimsShown(shown: Boolean, animate: Boolean) {
        super.setScrimsShown(shown, animate)
        if (mIsScrimsShown != shown) {
            mIsScrimsShown = shown
            if (scrimsShowListener != null) {
                scrimsShowListener.onScrimsShowChange(this, mIsScrimsShown)
            }
        }
    }

    fun setOnScrimesShowListener(listener: OnScrimsShowListener){
        scrimsShowListener = listener
    }

    interface OnScrimsShowListener {
        fun onScrimsShowChange(
            nestCollapsingToolbarLayout: NestCollapsingToolbarLayout,
            isScrimesShow: Boolean
        )
    }
}

当然这两个方法会有区别,方法一监听的offset为整个AppBarLayout包含内容的高度,方法二只针对CollapsingToolbarLayout包含内容的高度。这里我使用方法二

布局设计

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:background="@android:color/white"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

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

            <com.example.behaviordemo.NestCollapsingToolbarLayout
                android:id="@+id/toolbarLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <ImageView
                        android:layout_width="match_parent"
                        android:layout_height="150dp"
                        android:scaleType="centerCrop"
                        android:src="@drawable/user_profile_header" />

                    <ImageView
                        android:id="@+id/ivHead"
                        android:layout_width="60dp"
                        android:layout_height="60dp"
                        android:layout_marginLeft="10dp"
                        android:layout_marginTop="125dp"
                        android:background="@drawable/empty_avatar_user" />

                    <LinearLayout
                        android:id="@+id/llAuthor"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_below="@+id/ivHead"
                        android:layout_marginLeft="10dp"
                        android:layout_marginTop="10dp"
                        android:gravity="center_vertical"
                        android:orientation="horizontal">

                        <TextView
                            android:id="@+id/author_name"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="PG_KING"
                            android:textColor="@android:color/black"
                            android:textSize="16sp" />

                        <ImageView
                            android:id="@+id/ivLevel"
                            android:layout_width="27dp"
                            android:layout_height="15dp"
                            android:layout_marginLeft="5dp"
                            android:background="@drawable/ic_user_big_lv1" />
                    </LinearLayout>


                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_below="@+id/llAuthor"
                        android:layout_marginLeft="8dp"
                        android:text="Android"
                        android:textColor="@android:color/darker_gray"
                        android:textSize="13sp" />


                    <TextView
                        android:layout_width="80dp"
                        android:layout_height="35dp"
                        android:layout_alignTop="@+id/llAuthor"
                        android:layout_alignParentRight="true"
                        android:layout_marginRight="10dp"
                        android:background="@drawable/bg_edit"
                        android:gravity="center"
                        android:text="编辑"
                        android:textColor="@color/theme_blue"
                        android:textSize="15sp" />
                </RelativeLayout>
            </com.example.behaviordemo.NestCollapsingToolbarLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="@android:color/white"
                android:orientation="horizontal">

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:orientation="vertical"
                    android:paddingLeft="10dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="10000"
                        android:textColor="@android:color/black"
                        android:textSize="14sp" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="关注"
                        android:textColor="@android:color/darker_gray"
                        android:textSize="13sp" />
                </LinearLayout>

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:orientation="vertical"
                    android:paddingLeft="10dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="10000"
                        android:textColor="@android:color/black"
                        android:textSize="14sp" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="关注"
                        android:textColor="@android:color/darker_gray"
                        android:textSize="13sp" />
                </LinearLayout>

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:orientation="vertical"
                    android:paddingLeft="10dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="10000"
                        android:textColor="@android:color/black"
                        android:textSize="14sp" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="关注"
                        android:textColor="@android:color/darker_gray"
                        android:textSize="13sp" />
                </LinearLayout>
            </LinearLayout>

            <View
                android:layout_width="match_parent"
                android:layout_height="10dp"
                android:background="@android:color/darker_gray" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tabLayout"
                android:layout_width="match_parent"
                android:layout_height="60dp"
                app:tabIndicatorColor="@color/theme_blue"
                app:tabIndicatorFullWidth="false"
                app:tabSelectedTextColor="@color/theme_blue"
                app:tabTextColor="@android:color/darker_gray" />

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

        <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <RelativeLayout
        android:id="@+id/rlTitle"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:paddingLeft="15dp"
        android:paddingRight="15dp">

        <ImageView
            android:id="@+id/ivBack"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_back" />

        <LinearLayout
            android:id="@+id/llSmallAuthor"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="30dp"
            android:layout_toRightOf="@+id/ivBack"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:visibility="invisible">

            <ImageView
                android:layout_width="35dp"
                android:layout_height="35dp"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/empty_avatar_user" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="PG_KING"
                android:textColor="@android:color/black"
                android:textSize="16sp" />

            <ImageView
                android:layout_width="27dp"
                android:layout_height="15dp"
                android:layout_marginLeft="5dp"
                android:background="@drawable/ic_user_big_lv1" />
        </LinearLayout>

        <ImageView
            android:id="@+id/ivShare"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_share" />

        <ImageView
            android:id="@+id/ivUserData"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_centerVertical="true"
            android:layout_marginRight="30dp"
            android:layout_toLeftOf="@+id/ivShare"
            android:src="@drawable/ic_userdata" />

    </RelativeLayout>

</FrameLayout>
  • AppBarLayout+CollapsingToolbarLayout需要实现折叠效果,必须选择CoordinatorLayout作为根布局
  • CollapsingToolbarLayout需要设置app:layout_scrollFlags="scroll|exitUntilCollapsed"
  • 定义AppBarLayout与滚动视图之间的联系:在RecyclerView或者任意支持嵌套滚动的view比如NestedScrollView上添加app:layout_behavior属性配置

实现滚动效果

appBarLayout.setOnScrimesShowListener(object :
    NestCollapsingToolbarLayout.OnScrimsShowListener {
    override fun onScrimsShowChange(
        nestCollapsingToolbarLayout: NestCollapsingToolbarLayout,
        isScrimesShow: Boolean
    ) {
        if (isScrimesShow) {
            rlTitle.setBackgroundColor(Color.WHITE)
            ivBack.setImageResource(R.drawable.ic_back_blue)
            ivShare.setImageResource(R.drawable.ic_share_blue)
            ivUserData.setImageDrawable(
                tintDrawable(
                    resources.getDrawable(R.drawable.ic_userdata),
                    ColorStateList.valueOf(Color.parseColor("#B6B6B6"))
                )
            )
            showSmallAuthor()
        } else {
            rlTitle.setBackgroundColor(Color.TRANSPARENT)
            ivBack.setImageResource(R.drawable.ic_back)
            ivShare.setImageResource(R.drawable.ic_share)
            ivUserData.setImageDrawable(
                tintDrawable(
                    ivUserData.drawable,
                    ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
                )
            )
            if (objectAnimator.isRunning) {
                objectAnimator.cancel()
            }
            llSmallAuthor.visibility = View.INVISIBLE
        }
    }

})

补充

1、这里还使用了TabLayout+ViewPager的组合,用于实现主内容区的逻辑。tablayout和viewpager的关联绑定

pagerAdapter = object : ListPagerAdapter(supportFragmentManager, fragments) {
    override fun getPageTitle(position: Int): CharSequence? {
        if (tabList.size > position) {
            return tabList.get(position)
        }
        return ""
    }
}
viewPager.adapter = pagerAdapter
tabLayout.setupWithViewPager(viewPager)

2、动态改变drawable的颜色

微信截图_20211202160154.png

微信截图_20211202160139.png

看上图,统计柱状图由白色变成灰色~这里没有使用两张图片进行切换,而是使用了系统提供的DrawableCompat.setTintList函数

fun tintDrawable(drawable: Drawable, colors: ColorStateList): Drawable {
    val wrappedDrawable = DrawableCompat.wrap(drawable)
    DrawableCompat.setTintList(wrappedDrawable, colors)
    return wrappedDrawable
}

// 使用
ivUserData.setImageDrawable(
    tintDrawable(
        resources.getDrawable(R.drawable.ic_userdata),
        ColorStateList.valueOf(Color.parseColor("#B6B6B6"))
    )
)

拓展

app:layout_scrollFlags属性介绍

<attr name="layout_scrollFlags">
  <!-- Disable scrolling on the view. This flag should not be combined with any of the other
       scroll flags. -->
  <flag name="noScroll" value="0x0"/>

  <!-- The view will be scroll in direct relation to scroll events. This flag needs to be
       set for any of the other flags to take effect. If any sibling views
       before this one do not have this flag, then this value has no effect. -->
  <flag name="scroll" value="0x1"/>

  <!-- When exiting (scrolling off screen) the view will be scrolled until it is
       'collapsed'. The collapsed height is defined by the view's minimum height. -->
  <flag name="exitUntilCollapsed" value="0x2"/>

  <!-- When entering (scrolling on screen) the view will scroll on any downwards
       scroll event, regardless of whether the scrolling view is also scrolling. This
       is commonly referred to as the 'quick return' pattern. -->
  <flag name="enterAlways" value="0x4"/>

  <!-- An additional flag for 'enterAlways' which modifies the returning view to
       only initially scroll back to it's collapsed height. Once the scrolling view has
       reached the end of it's scroll range, the remainder of this view will be scrolled
       into view. -->
  <flag name="enterAlwaysCollapsed" value="0x8"/>

  <!-- Upon a scroll ending, if the view is only partially visible then it will be
       snapped and scrolled to it's closest edge. -->
  <flag name="snap" value="0x10"/>

  <!-- An additional flag to be used with 'snap'. If set, the view will be snapped to its
       top and bottom margins, as opposed to the edges of the view itself. -->
  <flag name="snapMargins" value="0x20"/>
</attr>

查看官方api文档,我们整体梳理下:

  • 其他属性的使用需要配合scroll,否则会没有效果,正确用法举例:app:layout_scrollFlags="scroll|exitUntilCollapsed"
  • enterAlways:不管向下还是向上滚动,优先滚动的是AppBarLayout中的childView
  • exitUntilCollapsed:向上滚动,优先滚动的是AppBarLayout中的childView;向下滚动,优先滚动的是其他视图,直到其他视图已经滚动到顶部再滚动AppBarLayout
  • snap:可配合其他属性一起使用,可以保证AppBarLayout完全显示或者完全隐藏而不会显示部分

具体的效果,可以通过写例子进行体验。跑过效果理解起来才没那么枯燥~

最后附上demo地址:gitee-demo