需求是先滑動(dòng)里面的列表号胚,滑動(dòng)到一個(gè)位置時(shí)外面滑動(dòng)女器,外面滑動(dòng)一段距離后再里面滑動(dòng)酸役。最初想用 CoordinatorLayout 加 RecyclerView,但效果不好直接用驾胆,或者用 NestedScrollView 與 RecyclerView 組合使用涣澡。
但 NestedScrollView 與 RecyclerView 組合時(shí)怎么也不能使 RecyclerView 自己滑動(dòng),而 NestedScrollView 不滑動(dòng)丧诺,事件攔截入桂,禁止嵌套滑動(dòng),NestedScrollView 是否消費(fèi) RecyclerView 發(fā)過來的距離驳阎,怎么試都不行抗愁,最終還是嘗試用 CoordinatorLayout 和 RecyclerView 組合。
實(shí)現(xiàn)效果如下:
要讓 CoordinatorLayout 一開始不滑動(dòng)呵晚,然后可以滑動(dòng)蜘腌,再然后又不可以滑動(dòng),所以想自定義一個(gè)控件饵隙,重寫 onNestedPreScroll 方法看能否有用撮珠。
class MyCoordinatorLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : CoordinatorLayout(context, attrs, defStyleAttr) {
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
super.onNestedPreScroll(target, dx, dy, consumed, type)
}
}
頁面布局如下:
<?xml version="1.0" encoding="utf-8"?>
<pot.ner347.androiddemo.view.MyCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbarLayout"
android:layout_width="match_parent"
android:layout_height="150dp"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<RelativeLayout
android:id="@+id/titleLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!-- 滑上去后要顯示的內(nèi)容,需求是一個(gè)復(fù)雜 View金矛,Demo 就用 TextView -->
<TextView
android:id="@+id/smallTitle"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:alpha="0"
android:background="#ffffff"
android:gravity="center"
android:text="標(biāo)題"
android:textColor="#000000"
android:textSize="20sp" />
<!-- 一開始顯示的復(fù)雜 View -->
<TextView
android:id="@+id/bigTitle"
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#0000ff"
android:gravity="center"
android:text="行程"
android:textColor="#ffffff"
android:textSize="20sp" />
</RelativeLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</pot.ner347.androiddemo.view.MyCoordinatorLayout>
先填充假數(shù)據(jù)
class ScrollActivity : AppCompatActivity() {
private lateinit var layoutManager : LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scroll)
layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
recyclerView.adapter = Adapter()
}
private inner class Adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> () {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val tv = TextView(parent.context)
tv.textColor = Color.WHITE
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
tv.gravity = Gravity.CENTER
return ViewHolder(tv)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val tv = holder.itemView as TextView
var height = 200
when (position) {
0 -> {
tv.text = "上面"
tv.backgroundColor = Color.BLACK
}
1 -> {
tv.text = "吸頂標(biāo)題"
tv.backgroundColor = resources.getColor(R.color.colorPrimary)
height = dp2px(50)
}
else -> {
tv.text = "position:$position"
tv.backgroundColor = Color.BLACK
}
}
val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
tv.layoutParams = lp
}
override fun getItemCount() = 20
}
private inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
fun dp2px(dip: Int): Int {
return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dip.toFloat(), resources.displayMetrics))
}
}
滑動(dòng)效果
監(jiān)聽 AppBarLayout 和 RecyclerView 的滑動(dòng)事件劫瞳,當(dāng)要吸頂?shù)?Item 滑到要隱藏的時(shí)候,讓外層 CoordinatorLayout 消費(fèi)滑動(dòng)距離绷柒。
class MyCoordinatorLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : CoordinatorLayout(context, attrs, defStyleAttr) {
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (canScroll) {
// fling 事件會(huì)瞬間滑動(dòng)很多志于,控制一下只消費(fèi) height + appbarLayout.y 的距離
if (scrollHeight + appbarLayout.y < dy) {
super.onNestedPreScroll(target, dx, (scrollHeight + appbarLayout.y).toInt(), consumed, type)
} else {
super.onNestedPreScroll(target, dx, dy, consumed, type)
}
} else {
// 本身不消費(fèi)任何距離,讓 RecyclerView 自己滑動(dòng)
super.onNestedPreScroll(target, dx, 0, consumed, type)
}
}
private var canScroll = true
fun canScroll(can: Boolean) {
canScroll = can
}
private var scrollHeight: Int = 0
// Activity 傳過來的废睦,AppBarLayout 從最大到最小可移動(dòng)的距離
fun setScrollHeight(height: Int) {
scrollHeight = height
}
}
修改 Activity 添加監(jiān)聽
private val STICK_TITLE_INDEX = 1 // 吸頂標(biāo)題在 RecyclerView 的位置
private var barHeightDistance: Int = 0 // 上面行程標(biāo)題大小兩種的高度差
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scroll)
barHeightDistance = dp2px(100)
coordinatorLayout.setScrollHeight(barHeightDistance)
// ...
appbarLayout.addOnOffsetChangedListener { _, _ -> calculate() }
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
calculate()
}
})
}
/**
* 控制 AppBar 是否可以滑動(dòng)
*/
private fun calculate() {
val firstPosition = layoutManager.findFirstVisibleItemPosition()
// 可見位置還沒有要吸頂?shù)?View伺绽,讓 CoordinatorLayout 不可以滑動(dòng)
if (firstPosition < STICK_TITLE_INDEX) {
coordinatorLayout.canScroll(false)
} else {
// 要吸頂?shù)奈恢玫搅耍屚饷鏉L動(dòng),此時(shí) appbarLayout.y 從 0 開始慢慢變小奈应,
// 變到 -barHeightDistance 時(shí)不再滑動(dòng)澜掩,讓 RecyclerView 滑動(dòng)
coordinatorLayout.canScroll(appbarLayout.y > -barHeightDistance)
}
}
吸頂后懸浮
要使吸頂?shù)?View 停在外面,單獨(dú)做一個(gè) View 覆蓋在 RecyclerView 上杖挣。
<!--<android.support.v7.widget.RecyclerView-->
<!--android:id="@+id/recyclerView"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
<!--app:layout_behavior="@string/appbar_scrolling_view_behavior" />-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 這個(gè) View 和 RecyclerView 里第一個(gè)位置的 View 一樣 -->
<TextView
android:visibility="gone"
android:id="@+id/stickyTitle"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="吸頂標(biāo)題"
android:textColor="#ffffff"
android:textSize="18sp" />
</RelativeLayout>
修改 calculate 方法
private fun calculate() {
val firstPosition = layoutManager.findFirstVisibleItemPosition()
if (firstPosition < STICK_TITLE_INDEX) {
stickyTitle.visibility = View.GONE // 第一個(gè)可見的小于 1肩榕,懸浮標(biāo)題還能看見
coordinatorLayout.canScroll(false)
} else {
// 第一個(gè)可見的大于 STICK_TITLE_INDEX,說明滑到上面了惩妇,顯示這個(gè)單獨(dú)的 View
stickyTitle.visibility = View.VISIBLE
coordinatorLayout.canScroll(appbarLayout.y > -barHeightDistance)
}
// 控制透明度變化
smallTitle.alpha = Math.abs(appbarLayout.y / barHeightDistance.toFloat())
bigTitle.alpha = 1 - smallTitle.alpha
}
自動(dòng)到頂部
如果 AppBarLayout 滑到一半手松開了株汉,想自動(dòng)收起來,讓 RecyclerView 發(fā)個(gè)假的滑動(dòng)歌殃。如果想漸進(jìn)的乔妈,可以做個(gè) ValueAnimator,每次發(fā)一點(diǎn)氓皱。
然后發(fā)現(xiàn)一個(gè)問題路召,向下拉時(shí)由于 canScroll 成 false 時(shí),發(fā)假的也沒用波材,所以修改
private fun calculate() {
// 只要 appbarLayout 在這不上不下的位置股淡,都可以滑動(dòng)
coordinatorLayout.canScroll(appbarLayout.y <=0 && appbarLayout.y >= -barHeightDistance)
val firstPosition = layoutManager.findFirstVisibleItemPosition()
// 可見位置還沒有要吸頂?shù)?View,讓 CoordinatorLayout 不可以滑動(dòng)
if (firstPosition < STICK_TITLE_INDEX) {
stickyTitle.visibility = View.GONE // 第一個(gè)可見的小于 1廷区,懸浮標(biāo)題還能看見
} else {
// 要吸頂?shù)奈恢玫搅舜Х牵屚饷鏉L動(dòng),此時(shí) appbarLayout.y 從 0 開始慢慢變小躲因,
// 變到 -barHeightDistance 時(shí)不再滑動(dòng),讓 RecyclerView 滑動(dòng)
stickyTitle.visibility = View.VISIBLE
}
// 控制透明度變化
smallTitle.alpha = Math.abs(appbarLayout.y / barHeightDistance.toFloat())
bigTitle.alpha = 1 - smallTitle.alpha
}
然后監(jiān)聽 View 的觸摸事件
recyclerView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) autoToTop()
false
}
coordinatorLayout.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) autoToTop()
false
}
其中 autoToTop 方法沒有做動(dòng)畫效果忌傻,一次性的
private fun autoToTop() {
if (appbarLayout.y < 0 && appbarLayout.y > -barHeightDistance) {
// 在這中間大脉,分發(fā)一個(gè)假的滑動(dòng)事件
recyclerView.dispatchNestedPreScroll(0, (appbarLayout.y + barHeightDistance).toInt(), intArrayOf(0,0), intArrayOf(0,0), ViewCompat.TYPE_TOUCH)
}
}
源碼: