0. 參考資料
來(lái)自鴻洋的博客的例子失暂,非常感謝:
http://blog.csdn.net/lmj623565791/article/details/43649913
1. 嵌套滑動(dòng)場(chǎng)合
如上面的例子,當(dāng)內(nèi)層的view友绝,如:list靖榕,滑動(dòng)到頂部時(shí)语御,即 firstChild.getTop = 0 的梯醒,我們需要將list的事件儿子,轉(zhuǎn)而給外層(怎么給呢?)漓藕,讓外層陶珠,去消耗,這點(diǎn)稍有些麻煩(鴻洋的博客中有解決)撵术;在Android的事件分發(fā)中背率,如果找到了target了,就一直會(huì)把后續(xù)的事件源源不斷的給target嫩与;
而此時(shí),一般我們需要抬起手指交排,然后重新下拉划滋,這個(gè)時(shí)候,外層就收到了事件了埃篓;
那可以實(shí)現(xiàn)嵌套滑動(dòng)嗎处坪?答案是可以的,我們可以用 nestedScrolling 的api來(lái)做架专,這塊同窘,我還暫未了解。所有這里部脚,采用原始的事件分發(fā)方案
來(lái)解決這個(gè)問(wèn)題想邦;
2. 準(zhǔn)備開(kāi)始吧
這里我簡(jiǎn)化了一下代碼,僅供參考委刘;
2.1 簡(jiǎn)化了布局文件如下
<test.com.widget.nested.StickyNavVerticalLayout2
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 頭部 -->
<RelativeLayout
android:id="@+id/id_stickynavlayout_topview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#4400ff00">
<TextView
android:layout_width="match_parent"
android:layout_height="256dp"
android:gravity="center"
android:text="嵌套滑動(dòng)"
android:textSize="30sp"
android:textStyle="bold"/>
</RelativeLayout>
<!-- 假的懸浮頭 -->
<TextView
android:id="@+id/id_stickynavlayout_indicator"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ffffffff"
android:gravity="center"
android:text="懸浮頭"/>
<!-- 嵌套的 scrollView -->
<ScrollView
android:id="@+id/id_stickynavlayout_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:divider="?android:attr/listDivider"
android:showDividers="middle"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="text1"/>
<!-- 更多內(nèi)容 -->
</LinearLayout>
</ScrollView>
</test.com.widget.nested.StickyNavVerticalLayout2>
3. 過(guò)程實(shí)現(xiàn)
一步一步來(lái)實(shí)現(xiàn)丧没;
3.1 先實(shí)現(xiàn)界面布局
創(chuàng)建StickyNavVerticalLayout
繼承自LinearyLayout,并添加相應(yīng)的一些代碼锡移,注釋在代碼里:
class StickyNavVerticalLayout2(context: Context, attrs: AttributeSet?, defAttrStyle: Int) : LinearLayout(context, attrs, defAttrStyle) {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
// --- 內(nèi)部 View 相關(guān)成員變量
private var top: View? = null
private var nav: View? = null
private var scrollView: View? = null
private var topHeight: Int? = 0 // top的高度
init {
}
override fun onFinishInflate() {
super.onFinishInflate()
top = findViewById(R.id.id_stickynavlayout_topview)
nav = findViewById(R.id.id_stickynavlayout_indicator)
scrollView = findViewById(R.id.id_stickynavlayout_scrollview)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
scrollView?.layoutParams?.height = measuredHeight.minus(nav?.measuredHeight ?: 0)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
topHeight = top?.measuredHeight?: 0
}
}
現(xiàn)在的界面效果:
底部的scrollView可以滑動(dòng)呕童,但是不能滑動(dòng)到底部;
3.2 添加手勢(shì)處理邏輯
實(shí)現(xiàn) onInterceptTouchEvent
, onTouchEvent
淆珊, 注意夺饲,在這里,我們這2個(gè)方法,都返回true往声,表示事件由自己處理擂找,不傳給子view(scrollView 收不到事件)
// --- 事件操作相關(guān)的成員變量
private var lastY: Int = 0
private var isDrag = false // 是否拖拽
private var scroller: Scroller = Scroller(getContext())
private var velocityTracker: VelocityTracker? = null
private var touchSlop: Int = 0
private var maxFlingVelocity: Int = 0
private var minFlingVelocity: Int = 0
init {
val config = ViewConfiguration.get(context)
touchSlop = config.scaledTouchSlop
maxFlingVelocity = config.scaledMaximumFlingVelocity
minFlingVelocity = config.scaledMinimumFlingVelocity
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
val y = it.y
initVelocityTracker()
velocityTracker?.let { it.addMovement(event) } // 添加運(yùn)動(dòng)軌跡
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastY = y.toInt()
}
MotionEvent.ACTION_MOVE -> {
val dy = y - lastY
if (!isDrag && Math.abs(dy) > touchSlop) {
isDrag = true
}
if (isDrag) {
scrollBy(0, -dy.toInt()) // 反向取反
}
lastY = y.toInt()
}
MotionEvent.ACTION_UP -> { // 抬起,運(yùn)動(dòng)軌跡判斷烁挟,是否fling
isDrag = false
velocityTracker?.let {
it.computeCurrentVelocity(1000, maxFlingVelocity?.toFloat())
if (Math.abs(it.yVelocity) > minFlingVelocity) {
fling(-it.yVelocity.toInt())
}
}
releaseVelocity()
}
MotionEvent.ACTION_CANCEL -> {
isDrag = false
releaseVelocity()
}
}
}
return true
//return super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true
//return super.onInterceptTouchEvent(ev)
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(0, scroller.currY)
invalidate()
}
}
/**
* 邊界處理
*/
override fun scrollTo(x: Int, y: Int) {
var tmpY = y
if (y < 0) tmpY = 0
if (y > topHeight) tmpY = topHeight
super.scrollTo(x, tmpY)
}
private inline fun fling(velocityY: Int) {
scroller.let {
it.fling(0, scrollY, 0, velocityY, 0, 0, 0, topHeight) // 滑翔
invalidate()
}
}
private inline fun initVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
}
private inline fun releaseVelocity() {
velocityTracker?.let {
it.recycle()
velocityTracker = null
}
}
大部分說(shuō)明 在鴻洋的 博客里寫(xiě)的很詳細(xì)了婴洼,這里就不再敘述了;
主要目的是撼嗓,實(shí)現(xiàn) StickyNavVerticalLayout2 的整體的滑動(dòng)柬采,滑翔等;
可以看到 scrollview
不能單獨(dú)滑動(dòng)
實(shí)現(xiàn)效果為:
3.3 攔截事件的處理
去掉 onTouchEvent
的 return true且警;
重寫(xiě)onInterceptTouchEvent
方法粉捻,讓其在特定的情況下攔截事件,需要攔截分為2種情況:
- topView 可見(jiàn)時(shí)攔截斑芜;
- topview不可見(jiàn)肩刃,并且 內(nèi)部的
scrollView
在頂部,并且還在下拉的狀態(tài)下杏头,進(jìn)行攔截盈包;
我們這里使用ViewCompat
來(lái)進(jìn)行View是否還可以繼續(xù)滾動(dòng)的判斷;我們來(lái)看代碼:
// 先添加成員變量醇王,記錄 top 的可見(jiàn)狀態(tài)
private var topHide = false
override fun scrollTo(x: Int, y: Int) {
var tmpY = y
if (y < 0) tmpY = 0
if (y > topHeight) tmpY = topHeight
super.scrollTo(x, tmpY)
topHide = scrollY == topHeight // 更新 topHide
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
.....省略代碼.....
// return true
return super.onTouchEvent(event)
}
/**
* 攔截判斷
*/
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val y = ev.y
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = y.toInt()
MotionEvent.ACTION_MOVE -> { // 重點(diǎn)
val dy = y - lastY
if (Math.abs(dy) > touchSlop) {
// topView 可見(jiàn) || (topView不可見(jiàn) && scrollView不能再下拉 && 繼續(xù)下拉)
if (!topHide || (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0)) {
lastY = y.toInt()
isDrag = true
initVelocityTracker()
velocityTracker?.let {
it.addMovement(ev)
}
return true
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isDrag = false
releaseVelocity()
}
}
return super.onInterceptTouchEvent(ev)
}
到現(xiàn)在呢燥,已經(jīng)完成了基本的需求了,但是嵌套滑動(dòng)還是不行寓娩,不連貫叛氨,沒(méi)有那種一口氣 硬扯到底 的 感覺(jué),需要放開(kāi)棘伴,再拉 寞埠;
效果圖,如下:
這個(gè)時(shí)候焊夸,就回到了仁连,開(kāi)頭留下的問(wèn)題了,如何實(shí)現(xiàn)一拉到底中間整個(gè)過(guò)程沒(méi)有間斷淳地;我們需要使用 dispatchTouchEvent 這個(gè)方法了怖糊;
3.4 重新 dispatchTouchEvent 嵌套的滑動(dòng)實(shí)現(xiàn)
上面的問(wèn)題,在于颇象,當(dāng)事件被 子 view 接收后伍伤,后續(xù)的事件,都會(huì)跑到子view遣钳;但事件的傳遞扰魂,都是從父到子的過(guò)程,事件的傳遞會(huì)經(jīng)過(guò)父的dispatch,通過(guò)這個(gè)方法劝评,將事件在父與子View中根據(jù)條件來(lái)傳遞姐直,從而實(shí)現(xiàn)嵌套滑動(dòng);
private var isInControl = false // 是否已dispatch
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
MotionEvent.ACTION_MOVE -> { // 進(jìn)行判斷蒋畜,是否重發(fā)事件
val dy = ev.y - lastY
// 頭不可見(jiàn)声畏,繼續(xù)下拉,重發(fā)事件
if (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0 && !isInControl) {
isInControl = true
ev.action = MotionEvent.ACTION_CANCEL
val ev2 = MotionEvent.obtain(ev)
dispatchTouchEvent(ev)
ev2.action = MotionEvent.ACTION_DOWN
return dispatchTouchEvent(ev2)
}
}
}
return super.dispatchTouchEvent(ev)
}
我們需要在 onTouchEvent方法加入以下代碼片段姻成,即:在邊界時(shí)插龄,將MOVE事件轉(zhuǎn)換成 DOWN事件,重新進(jìn)行分發(fā)科展;
=== > onTouchEvent 方法中修改
MotionEvent.ACTION_MOVE -> {
val dy = y - lastY
if (!isDrag && Math.abs(dy) > touchSlop) {
isDrag = true
}
if (isDrag) {
scrollBy(0, -dy.toInt()) // 反向取反
}
// 如果滑到頂了均牢,將事件轉(zhuǎn)換成點(diǎn)擊事情,發(fā)送
if (scrollY == topHeight) {
event.action = MotionEvent.ACTION_DOWN
dispatchTouchEvent(event)
isInControl = false
}
lastY = y.toInt()
}
scroller的優(yōu)化才睹,在onInterceptTouchEvent() 與 onTouchEvent中分別 加入:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val y = ev.y
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = y.toInt()
MotionEvent.ACTION_MOVE -> { // 重點(diǎn)
// 慣性未結(jié)束徘跪,攔截事件
if(!scroller.isFinished) {
return true
}
.......
.......
// onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
// .... 省略代碼
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastY = y.toInt()
if(!scroller.isFinished) { // 未結(jié)束時(shí),結(jié)束scroller
scroller.abortAnimation()
return true
}
}
// ACTION_CANCEL時(shí)時(shí)琅攘,取消scroller
MotionEvent.ACTION_CANCEL -> {
isDrag = false
if(!scroller.isFinished) {
scroller.abortAnimation()
}
releaseVelocity()
}
最終效果如下:
水平滑動(dòng)(加深印象)
效果如下: