0. 參考資料
來自鴻洋的博客的例子,非常感謝:
http://blog.csdn.net/lmj623565791/article/details/43649913
1. 嵌套滑動場合
如上面的例子,當內層的view,如:list,滑動到頂部時,即 firstChild.getTop = 0 的,我們需要將list的事件,轉而給外層(怎么給呢?),讓外層,去消耗,這點稍有些麻煩(鴻洋的博客中有解決);在Android的事件分發中,如果找到了target了,就一直會把后續的事件源源不斷的給target;
而此時,一般我們需要抬起手指,然后重新下拉,這個時候,外層就收到了事件了;
那可以實現嵌套滑動嗎?答案是可以的,我們可以用 nestedScrolling 的api來做,這塊,我還暫未了解。所有這里,采用原始的事件分發方案
來解決這個問題;
2. 準備開始吧
這里我簡化了一下代碼,僅供參考;
2.1 簡化了布局文件如下
<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="嵌套滑動"
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"/>
<!-- 更多內容 -->
</LinearLayout>
</ScrollView>
</test.com.widget.nested.StickyNavVerticalLayout2>
3. 過程實現
一步一步來實現;
3.1 先實現界面布局
創建StickyNavVerticalLayout
繼承自LinearyLayout,并添加相應的一些代碼,注釋在代碼里:
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)
// --- 內部 View 相關成員變量
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
}
}
現在的界面效果:
底部的scrollView可以滑動,但是不能滑動到底部;
3.2 添加手勢處理邏輯
實現 onInterceptTouchEvent
, onTouchEvent
, 注意,在這里,我們這2個方法,都返回true,表示事件由自己處理,不傳給子view(scrollView 收不到事件)
// --- 事件操作相關的成員變量
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) } // 添加運動軌跡
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 -> { // 抬起,運動軌跡判斷,是否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
}
}
大部分說明 在鴻洋的 博客里寫的很詳細了,這里就不再敘述了;
主要目的是,實現 StickyNavVerticalLayout2 的整體的滑動,滑翔等;
可以看到 scrollview
不能單獨滑動
實現效果為:
3.3 攔截事件的處理
去掉 onTouchEvent
的 return true;
重寫onInterceptTouchEvent
方法,讓其在特定的情況下攔截事件,需要攔截分為2種情況:
- topView 可見時攔截;
- topview不可見,并且 內部的
scrollView
在頂部,并且還在下拉的狀態下,進行攔截;
我們這里使用ViewCompat
來進行View是否還可以繼續滾動的判斷;我們來看代碼:
// 先添加成員變量,記錄 top 的可見狀態
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 -> { // 重點
val dy = y - lastY
if (Math.abs(dy) > touchSlop) {
// topView 可見 || (topView不可見 && scrollView不能再下拉 && 繼續下拉)
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)
}
到現在,已經完成了基本的需求了,但是嵌套滑動還是不行,不連貫,沒有那種一口氣 硬扯到底 的 感覺,需要放開,再拉 ;
效果圖,如下:
這個時候,就回到了,開頭留下的問題了,如何實現一拉到底中間整個過程沒有間斷;我們需要使用 dispatchTouchEvent 這個方法了;
3.4 重新 dispatchTouchEvent 嵌套的滑動實現
上面的問題,在于,當事件被 子 view 接收后,后續的事件,都會跑到子view;但事件的傳遞,都是從父到子的過程,事件的傳遞會經過父的dispatch,通過這個方法,將事件在父與子View中根據條件來傳遞,從而實現嵌套滑動;
private var isInControl = false // 是否已dispatch
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
MotionEvent.ACTION_MOVE -> { // 進行判斷,是否重發事件
val dy = ev.y - lastY
// 頭不可見,繼續下拉,重發事件
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方法加入以下代碼片段,即:在邊界時,將MOVE事件轉換成 DOWN事件,重新進行分發;
=== > onTouchEvent 方法中修改
MotionEvent.ACTION_MOVE -> {
val dy = y - lastY
if (!isDrag && Math.abs(dy) > touchSlop) {
isDrag = true
}
if (isDrag) {
scrollBy(0, -dy.toInt()) // 反向取反
}
// 如果滑到頂了,將事件轉換成點擊事情,發送
if (scrollY == topHeight) {
event.action = MotionEvent.ACTION_DOWN
dispatchTouchEvent(event)
isInControl = false
}
lastY = y.toInt()
}
scroller的優化,在onInterceptTouchEvent() 與 onTouchEvent中分別 加入:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val y = ev.y
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = y.toInt()
MotionEvent.ACTION_MOVE -> { // 重點
// 慣性未結束,攔截事件
if(!scroller.isFinished) {
return true
}
.......
.......
// onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
// .... 省略代碼
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastY = y.toInt()
if(!scroller.isFinished) { // 未結束時,結束scroller
scroller.abortAnimation()
return true
}
}
// ACTION_CANCEL時時,取消scroller
MotionEvent.ACTION_CANCEL -> {
isDrag = false
if(!scroller.isFinished) {
scroller.abortAnimation()
}
releaseVelocity()
}
最終效果如下:
水平滑動(加深印象)
效果如下: