最近无聊刷手机的时候, 发现小米时钟的嵌套滑动很有意思, 就试着做了下
先上对比图:
分析下小米时钟的滑动
- 闹钟列表 向上滑动时, 时钟面 透明度上升, 快到最大滑动时逐渐显示数字时钟.
在闹钟列表向上滑动的过程中,时钟面和数字时钟的位置看起来并没有发生变化, 而且时钟面能分发到滑动信息
如果尝试用 AppBarLayout + appbar_scrolling_view_behavior , 会出现滑动信息被消耗的情况, 尝试滑动 AppBarLayout 的子view会使整体滑动,
像这样:
自定义behavior
如果用 AppBarLayout 不方便, 那我们就自己来, 先上布局:
<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"
tools:context="com.jimzjy.demo.MainActivity">
<com.jimzjy.networkspeedview.NetworkSpeedView
android:id="@+id/main_network_speed_view"
android:layout_width="match_parent"
android:layout_height="@dimen/top_size"
android:background="@color/colorPrimary"
app:speed_unit="Auto_same"/>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/main_scrim_view"
android:layout_width="match_parent"
android:layout_height="@dimen/top_size"
android:background="@color/colorPrimary"
android:alpha="0"
app:layout_behavior=".DeviceRecyclerScrollerBehavior"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/main_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:layout_marginTop="@dimen/top_size"
app:layout_behavior=".MeasureRecyclerBehavior"
tools:listitem="@layout/rv_item"
tools:itemCount="7"/>
</android.support.design.widget.CoordinatorLayout>
</FrameLayout>
复制代码
Framelayout中分了两个层次, 底层 NetworkSpeedView ,再 CoordinatorLayout . CoordinatorLayout 中用一个 View 遮住
底层的 NetworkSpeedView , 一开始让 View 的 alpha 值为0, 在 CoordinatorLayout 向上滑动的过程中, 提高 alpha 值, 这样就达到了近似 NetworkSpeedView 透明度改变的效果.
为了方便看, 顶部的 text 现在还不添加
关于嵌套滑动和自定义Behavior可以看
一点见解: Android嵌套滑动和NestedScrollView
CoordinatorLayout高级用法-自定义Behavior
Behavior :
class DeviceRecyclerScrollerBehavior(ctx: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(ctx, attrs) {
//顶部文字的高度
private val mTopText = ctx.resources.getDimension(R.dimen.top_text_size)
//总共需要上移的高度
private val mTotalOffsetY = ctx.resources.getDimension(R.dimen.top_size) - mTopText
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
}
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
/*
判断是否要消耗 dy
dy > 0 上移, dy < 0 下移
向上,如果已经达到向上最大滑动/向下,内部的 RecyclerView 还可以下滑 . 则不消耗
*/
val toCost = (dy > 0 && coordinatorLayout.scrollY < mTotalOffsetY)
|| (dy < 0 && coordinatorLayout.scrollY > 0 && !target.canScrollVertically(-1))
if (toCost) {
coordinatorLayout.scrollBy(0, dy)
//改变透明度
child.alpha = coordinatorLayout.scrollY / (mTotalOffsetY - mTopText)
consumed[1] = dy
}
}
}
class MeasureRecyclerBehavior(ctx: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<RecyclerView>(ctx, attrs) {
private val mTopText = ctx.resources.getDimension(R.dimen.top_text_size)
override fun onMeasureChild(parent: CoordinatorLayout?, child: RecyclerView?, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int): Boolean {
//测量大小, 让 RecyclerView 的高度 = CoordinatorLayout的高度 - 顶部text的高度
child?.measure(parentWidthMeasureSpec, View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(parentHeightMeasureSpec) - mTopText.toInt(), View.MeasureSpec.EXACTLY))
return true
}
}
复制代码
通过 MeasureRecyclerBehavior 测量 RecyclerView 的高度(设定准确值), 而不是根据父布局, 就不会出现向上滑动时感觉底下少一块的情况,像这样:
DeviceRecyclerScrollerBehavior 会设置消耗滑动距离, 以及修改透明度处理顶部的 TextView
因为顶部的 TextView 不随着 CoordinatorLayout 的滑动而滑动, 所以把顶部的 TextView 放到 FrameLayout(直接在 Coordinator上面) :
<FrameLayout xmlns:android>
<com.jimzjy.networkspeedview.NetworkSpeedView/>
<CoordinatorLayout/>
<TextView
android:id="@+id/main_top_text"
android:layout_width="match_parent"
android:layout_height="@dimen/top_text_size"
android:gravity="center_vertical|center_horizontal"
android:text="100 | 100"
android:textSize="22sp"
android:textColor="@android:color/white"
android:alpha="0"/>
</FrameLayout>
复制代码
因为 TextView 不是 CoordinatorLayout 的子 View , 所以就不能通过 behavior 来设置它的 alpha 了.
那就通过设置监听, 每次滑动都会调用 CoordinatorLayout 的 scrollTo() ( scrollBy() 内部调用的也是scrollTo() ), 所以继承 CoordinatorLayout , 覆写 scrollTo() , 写个回调监听就好了.
class ReCoordinatorLayout(ctx: Context, attrs: AttributeSet?) : CoordinatorLayout(ctx, attrs) {
private val mTopText = ctx.resources.getDimension(R.dimen.top_text_size)
private val mTop = ctx.resources.getDimension(R.dimen.top_size)
private val mTopOffsetY = mTop - mTopText
private val mAlphaChangeStart = mTopOffsetY - mTopText
private var mShowText: ((alpha: Float) -> Unit)? = null
constructor(ctx: Context) : this(ctx, null)
override fun scrollTo(x: Int, y: Int) {
var sY = y
//防止滑动过多
when {
sY < 0 -> sY = 0
sY > mTopOffsetY -> sY = mTopOffsetY.toInt()
}
if (sY != scrollY) super.scrollTo(x, sY)
when {
sY > mAlphaChangeStart -> mShowText?.invoke((sY - mAlphaChangeStart) / mTopText)
sY <= mAlphaChangeStart -> mShowText?.invoke(0f)
}
}
//设置回调函数
fun setShowtext(showText: (alpha: Float) -> Unit) {
this.mShowText = showText
}
}
复制代码
在 scrollTo 中调用回调函数 mShowText() 来设置 alpha 值.
设置回调函数
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
... ...
main_coordinator_layout.setShowtext { main_top_text.alpha = it }
}
复制代码
总结
GitHub地址
总的来说实现的还是比较粗糙的, 但是这样非常简单