首先看下效果图:
实现这种效果使用behavior还是比较简单的,对于behavior还有些陌生的可以看看这篇文章
直接撸代码:
整体思路是,每个卡片是一个自定义view,然后用CoordinatorLayout将其包裹
1.卡片view又一个头部和一个Recyclerview组成:
package com.zztl.slidinglayout
import android.annotation.SuppressLint
import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.TextView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseViewHolder
/**
* @desc
* @auth ${user}
* @time 2019/1/7 10:29
*/
@CoordinatorLayout.DefaultBehavior(SlidingLayoutBehavior::class)
class SlidingLayout(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs) {
var backgound: Int = 0
var headerHeight:Int=0
var txt: String=""
lateinit var headerView: TextView
init {
initView(context, attrs)
}
@SuppressLint("WrongConstant")
private fun initView(context: Context?, attrs: AttributeSet?) {
context?.obtainStyledAttributes(attrs, R.styleable.slidinglayout)?.let {
backgound = it.getColor(R.styleable.slidinglayout_backgound, resources.getColor(R.color.colorPrimary))
txt = it.getString(R.styleable.slidinglayout_text)
it.recycle()
}
LayoutInflater.from(context).inflate(R.layout.sliding_layout, this)
headerView = findViewById<TextView>(R.id.tv).apply {
text = txt
setBackgroundColor(backgound)
}
findViewById<RecyclerView>(R.id.rv)?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = SimpleAdapter(R.layout.item, mutableListOf("a", "b", "c", "d", "e","a", "b", "c", "d", "e","a", "b", "c", "d", "e"))
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
if (w != oldw || h != oldh) {
headerHeight = headerView.measuredHeight
}
}
inner class SimpleAdapter(layout: Int, data: MutableList<String>?) : BaseQuickAdapter<String, BaseViewHolder>(layout, data) {
override fun convert(helper: BaseViewHolder?, item: String?) {
helper!!.setText(R.id.textview, item!!)
}
}
}
这里简单定义了一个头部的背景和文字描述的自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="slidinglayout">
<attr name="backgound" format="color"></attr>
<attr name="text" format="string"></attr>
</declare-styleable>
</resources>
布局文件sliding_layout:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:background="@android:color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="50dp"
></android.support.v7.widget.RecyclerView>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center_vertical|start"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="@android:color/white"
android:textSize="15sp"/>
</merge>
这里就不用多说,要注意的是:@CoordinatorLayout.DefaultBehavior(SlidingLayoutBehavior::class),加这个注解让我们的自定义Behavior生效最重要的是实现自定义的behavior
package com.zztl.slidinglayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.view.ViewCompat
import android.view.View
/**
* @desc
* @auth ${user}
* @time 2019/1/7 15:20
*/
class SlidingLayoutBehavior : CoordinatorLayout.Behavior<SlidingLayout>() {
var initOffset: Int = 0 //最初距离顶部的距离
override fun onMeasureChild(parent: CoordinatorLayout, child: SlidingLayout, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int): Boolean {
val childOffset = getChildOffset(parent, child)
child.measure(parentWidthMeasureSpec, View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.getSize(parentHeightMeasureSpec) - childOffset, View.MeasureSpec.EXACTLY))
return true
}
override fun onLayoutChild(parent: CoordinatorLayout, child: SlidingLayout, layoutDirection: Int): Boolean {
parent.onLayoutChild(child, layoutDirection)
/* getPreView(parent, child)?.let {
child.offsetTopAndBottom(it.top+it.headerHeight)
}*/
child.offsetTopAndBottom(parent.indexOfChild(child) * child.headerHeight)
initOffset = child.top
return true
}
private fun getChildOffset(parent: CoordinatorLayout, child: SlidingLayout): Int {
var offset: Int = 0
for (i in 0 until parent.childCount) {
parent.getChildAt(i)?.apply {
if (this != child && this is SlidingLayout) {
offset += this.headerHeight
}
}
}
return offset
}
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: SlidingLayout, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
//只监听竖直方向的滑动,并且处理的就是用户现在触摸的view,因为有多个child会使用这个Behavior,
return (axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0) and (directTargetChild == child)
}
/**
* 在嵌套滑动的子View未滑动之前调用,之所以要写这个方法 ,是为了让target在滚动时,child能进行上下移动,并把移动的距离赋值给consumed[1],
* 告知子View滑动做出相应的调整
*
* @param parent
* @param child
* @param target 具体嵌套滑动的子类
* @param dx 水平方向嵌套滑动的子View想要变化的距离
* @param dy 垂直方向嵌套滑动的子View想要变化的距离
* @param consumed 这个参数,告诉子View当前父View消耗的距离
* consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
*/
override fun onNestedPreScroll(parent: CoordinatorLayout, child: SlidingLayout, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (child.top > initOffset) {
//1 自己移动
val distance = scrooll(parent, child, dy)
//2 上下的View移动
stickScroll(parent, child, distance)
}
}
override fun onNestedScroll(parent: CoordinatorLayout, child: SlidingLayout, target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
if (parent.indexOfChild(child) != 0) {
//1 自己移动
val distance = scrooll(parent, child, dyUnconsumed)
//2 上下的View移动
stickScroll(parent, child, distance)
}
}
private fun scrooll(parent: CoordinatorLayout, child: SlidingLayout, dy: Int): Int {
val i = parent.childCount - parent.indexOfChild(child)
val scollDistance = Math.min(Math.max(initOffset, child.top - dy), parent.height - child.headerHeight*i) - child.top
child.offsetTopAndBottom(scollDistance)
return -scollDistance //负数表示向下滑,正数表示向上滑
}
private fun stickScroll(parent: CoordinatorLayout, child: SlidingLayout, distance: Int) {
if (distance == 0) {
return
}
if (distance > 0) { //向上滑
var currentView = child
var preView = getPreView(parent, currentView)
while (null != preView) {
(preView.top + preView.headerHeight - currentView.top).let {
if (it > 0) {
preView!!.offsetTopAndBottom(-it)
}
}
currentView = preView
preView = getPreView(parent, currentView)
}
} else { //向下滑
var currentView = child
var nextView = getNextView(parent, currentView)
while (null != nextView) {
(currentView.top + currentView.headerHeight - nextView.top).let {
if (it > 0) {
nextView!!.offsetTopAndBottom(it)
}
}
currentView = nextView
nextView = getNextView(parent, currentView)
}
}
}
/**
* 获取当前view的下一个view
*/
private fun getNextView(parent: CoordinatorLayout, currentView: SlidingLayout): SlidingLayout? {
val indexOfChild = parent.indexOfChild(currentView)
for (i in indexOfChild + 1 until parent.childCount) {
if (parent.getChildAt(i) is SlidingLayout) {
return parent.getChildAt(i) as SlidingLayout
}
}
return null
}
/**
* 获取当前view上一个view
*/
private fun getPreView(parent: CoordinatorLayout, child: SlidingLayout): SlidingLayout? {
val indexOfChild = parent.indexOfChild(child)
for (i in indexOfChild - 1 downTo 0) {
if (parent.getChildAt(i) is SlidingLayout) {
return parent.getChildAt(i) as SlidingLayout
}
}
return null
}
}
在计算当前view上下的view移动时距离分为两种情况
(1)向上移动时,当前view的前面view要进行移动:preView.top + preView.headerHeight - currentView.top,如下示意图:
h=preView.top + preView.headerHeight - currentView.top ,就是前view要移动的距离
(2)向下滑同理就是相反的,currentView.top + currentView.headerHeight - nextView.top
最后附上MainActivity的布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.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=".MainActivity">
<com.zztl.slidinglayout.SlidingLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgound="#cc3399"
app:text="卡片1"/>
<com.zztl.slidinglayout.SlidingLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgound="#99ffcc"
app:text="卡片2"/>
<com.zztl.slidinglayout.SlidingLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgound="#33ccff"
app:text="卡片3"/>
<com.zztl.slidinglayout.SlidingLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgound="#F4A460"
app:text="卡片4"/>
</android.support.design.widget.CoordinatorLayout>
这样,整个功能代码就完成啦。。。。。源码地址