平臺(tái)類網(wǎng)站有哪些搜索引擎分類
嵌套滾動(dòng):內(nèi)外兩層均可滾動(dòng),比如上半部分是一個(gè)有限的列表,下半部分是WebView,在內(nèi)層上半部分展示到底的時(shí)候,外部父布局整體滾動(dòng)內(nèi)部View,將底部WevView拉起來(lái),滾動(dòng)到頂部之后再將滾動(dòng)交給內(nèi)部WebView,之后滾動(dòng)的就是內(nèi)部WebView,如下圖:
實(shí)現(xiàn):onInterceptTouchEvent或者NestedScroll
按照上下兩部分構(gòu)建父布局,父ViewGroup建議繼承FrameLayout/RelativeLayout來(lái)實(shí)現(xiàn),方便處理測(cè)量[無(wú)需復(fù)寫]與布局,在計(jì)算出全部View高度后,可以計(jì)算最大父布局滾動(dòng)距離:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {var top = tvar bottom = bfor (i in 0 until childCount) {getChildAt(i).layout(l, top, r, bottom)top += getChildAt(i).measuredHeightbottom += getChildAt(i).measuredHeighttotalHeight += getChildAt(i).measuredHeight}maxScrollHeight = totalHeight - measuredHeight
}
上述交互有兩種比較常用的方式,一種是onInterceptTouchEvent全局?jǐn)r擊Touch事件來(lái)實(shí)現(xiàn)拖動(dòng)與Fling的處理,另一種是借助后期推出的NestedScroll框架來(lái)實(shí)現(xiàn)。先簡(jiǎn)單看下傳統(tǒng)的onInterceptTouchEvent攔截的方式:核心的處理事兩個(gè)操作,一個(gè)是拖動(dòng)、一個(gè)是UP后的Fling,onInterceptTouchEvent首先要確定攔截的時(shí)機(jī):判斷有效拖動(dòng)
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {when (event.actionMasked) {MotionEvent.ACTION_DOWN -> {mLastY = event.rawYmDownY = event.rawYmDownX = event.rawXmBeDraging = false}MotionEvent.ACTION_MOVE -> if (abs(event.rawY - mDownY) > ViewConfiguration.get(context).scaledTouchSlop) {mBeDraging = truereturn true} else {mLastY = event.rawY}else -> {}}return super.onInterceptTouchEvent(event)
}
一般而言垂直滾動(dòng)超過(guò)某個(gè)TouchSlop就可以認(rèn)為拖動(dòng)有效,拖動(dòng)開(kāi)始,子View后續(xù)無(wú)法獲取到Touch事件,其實(shí)大多數(shù)場(chǎng)景而言,父布局接管之后,沒(méi)有必要再給子View分發(fā)事件,之后自行處理拖拽與Fling。
onInterceptTouchEvent方式處理拖拽與fling
處理拖拽與fling需要注意銜接,所以需要準(zhǔn)備好GestureDetector,用于將來(lái)的fling,建議放在dispatchTouchEvent整體處理事件的消費(fèi)
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {if (ev != null) {gestureDetector.onTouchEvent(ev)}if (ev?.action == MotionEvent.ACTION_DOWN)overScrollerNest.abortAnimation()if (ev?.action == MotionEvent.ACTION_MOVE && mBeDraging) {scrollInner((mLastY - ev.rawY).roundToInt())mLastY = ev.rawY}return super.dispatchTouchEvent(ev)
}
拖拽的控制方式:自己計(jì)算出可滾動(dòng)的距離,可以利用View的canScrollVertically判斷View是否能消費(fèi),從而決定留給哪個(gè)View
private fun scrollInner(dy: Int) {var pConsume: Int = 0var cConsume: Int = 0if (dy > 0) {if (scrollY in 1 until maxScrollHeight) {pConsume = dy.coerceAtMost(maxScrollHeight - scrollY)scrollBy(0, dy)cConsume = dy - pConsumeif (bottomView.canScrollVertically(cConsume) && cConsume != 0) {bottomView.scrollBy(0, cConsume)}} else if (scrollY == 0) {bottomView.scrollTo(0, 0)if (upView.canScrollVertically(dy)) {upView.scrollBy(0, dy)} else {if (canScrollVertically(dy)) {scrollBy(0, dy)}}} else if (scrollY >= maxScrollHeight) {scrollTo(0, maxScrollHeight)if (bottomView.canScrollVertically(dy)) {bottomView.scrollBy(0, dy)} else {overScrollerNest.abortAnimation()}}} else {if (scrollY in 1 until maxScrollHeight) {pConsume = Math.max(dy, -scrollY)scrollBy(0, pConsume)cConsume = dy - pConsumeif (bottomView.canScrollVertically(cConsume)) {bottomView.scrollBy(0, cConsume)}} else if (scrollY == maxScrollHeight) {if (bottomView.canScrollVertically(dy)) {bottomView.scrollBy(0, dy)} else {if (canScrollVertically(dy)) {scrollBy(0, dy)}}} else {if (upView.canScrollVertically(dy)) {upView.scrollBy(0, dy)}bottomView.scrollTo(0, 0)}}invalidate()}
拖拽結(jié)束后,fling跟上利用GestureDetector的onFling,直接讓Scroller接上即可 overScroller = OverScroller(context),一般用OverScroller的體驗(yàn)好一些:
private GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {if (!(Math.abs(e1.getX() - e2.getX()) > mTouchSlop && Math.abs(velocityX) > Math.abs(velocityY))) {<!--銜接滾動(dòng)-->overScroller.fling(0, 0, 0, (int) velocityY, 0, 0, -10 * ScreenUtil.getDisplayHeight(), 10 * ScreenUtil.getDisplayHeight());<!--必須觸發(fā)一次-->postInvalidate();}return super.onFling(e1, e2, velocityX, velocityY);}
});
在computeScroll里從新計(jì)算應(yīng)該滾動(dòng)的距離,可以看到全局接管,并利用scrollBy自行控制滾動(dòng)的偏移量是這種方案的核心
var mLastOverScrollerValue = 0override fun computeScroll() {super.computeScroll()if (overScrollerNest.computeScrollOffset()) {scrollInner(overScrollerNest.currY - mLastOverScrollerValue)mLastOverScrollerValue = overScrollerNest.currYinvalidate()}}
如此就可以利用onInterceptTouchEvent實(shí)現(xiàn)嵌套滾動(dòng),不涉及太多內(nèi)部View【僅僅是獲取了內(nèi)部View的高度及判斷是否可滾】,一切交給父布局即可。
利用NestedScrolling框架實(shí)現(xiàn)嵌套滑動(dòng)
Android5.0推出了嵌套滑動(dòng)機(jī)制NestedScrolling,讓父View和子View在滑動(dòng)時(shí)相互協(xié)調(diào)配合,為了向前兼容又抽離了NestedScrollingChild、NestedScrollingParent、NestedScrollingChildHelper、NestedScrollingParentHelper等支持類,不過(guò)在23年的場(chǎng)景下基本不需要使用這些輔助類了。NestedScrolling的核心是子View一直能收到Move事件,在自己處理之前先交給父View消費(fèi),父View處理完之后,再將余量還給子View,讓子View自己處理,可以看出這套框架必須父子配合,也就是NestedScrollingChild、NestedScrollingParent是配套的。5.0之后View與ViewGroup本身就實(shí)現(xiàn)了NestedScrollingChild+NestedScrollingParent的框架,自定義布局的時(shí)候只需要定制與啟用,也就是必須進(jìn)行二次開(kāi)發(fā),目前Google提供的最好用的就是RecyclerView。有張圖很清晰的描述NestedScrolling框架是如何工作的:
NestedScrolling只處理拖動(dòng)[target無(wú)法改變],Fling交給Parent處理
在這個(gè)框架中,子View必須主動(dòng)啟動(dòng)嵌套滑動(dòng)、并且在Move的時(shí)候主動(dòng)請(qǐng)求父ViewGroup進(jìn)行處理,這樣才能完成協(xié)同,并非簡(jiǎn)單的打開(kāi)開(kāi)關(guān),所有的定制邏輯仍舊需要開(kāi)發(fā)者自己處理,只是替代了onInterceptTouchEvent,提供了子View回傳事件給父View的能力,不用父View主動(dòng)攔截,也能獲取接管子View事件的能力。
以開(kāi)頭描述的場(chǎng)景為例,如果上部分用ScrollView下部分用WebView,那么必須將兩者都改造成NestedScrollingChild,也就是NestedScrollView與NestedWebView,NestedScrollView谷歌已經(jīng)提供,NestedWebView目前沒(méi)有,需要自己封裝,可以看看如何配合實(shí)現(xiàn)一套嵌套滑動(dòng)交互:
class NetScrollWebView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
) : WebView(context, attrs) {private val mTouchSlop = android.view.ViewConfiguration.get(context).scaledTouchSlopprivate val mScrollOffset = IntArray(2)private val mScrollConsumed = IntArray(2)init {<!--啟動(dòng)嵌套滑動(dòng)-->isNestedScrollingEnabled = true}private var mLastY: Float = 0fprivate var dragIng: Boolean = falseoverride fun dispatchTouchEvent(ev: MotionEvent?): Boolean {when (ev?.action) {MotionEvent.ACTION_MOVE -> {if (abs(ev.rawY - mLastY) > mTouchSlop) {dragIng = true} else {super.dispatchTouchEvent(ev)}if (dragIng) {if (parent != null) {parent.requestDisallowInterceptTouchEvent(true)}<!--主動(dòng)調(diào)用dispatchNestedPreScroll,請(qǐng)求父容器處理-->dispatchNestedPreScroll(0, (mLastY - ev.rawY).toInt(), mScrollConsumed, mScrollOffset)mLastY = ev.rawY}}MotionEvent.ACTION_DOWN -> {dragIng = falsesuper.dispatchTouchEvent(ev)<!--startNestedScroll啟動(dòng)嵌套滑動(dòng)-->startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)mScrollConsumed[1] = 0mLastY = ev.rawY}MotionEvent.ACTION_UP -> stopNestedScroll()else -> super.dispatchTouchEvent(ev)}return true}// 強(qiáng)制自己不消費(fèi)moveoverride fun onTouchEvent(ev: MotionEvent?): Boolean {if (dragIng || ev?.action == MotionEvent.ACTION_MOVE)return falsereturn super.onTouchEvent(ev)}}
- setNestedScrollingEnabled(true) ,后續(xù)的dispatch都依賴該開(kāi)關(guān)
- 子View收到DOWN事件的時(shí)候啟動(dòng)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL),【無(wú)論有沒(méi)有NestedScrollingParent】
- 父布局其實(shí)這個(gè)時(shí)候也會(huì)響應(yīng),只有存在支持嵌套滑動(dòng)的父布局,后續(xù)dispatchNestedPreScroll等函數(shù)才有意義才有意義
- 假設(shè)存在支持嵌套滑動(dòng)的父布局,在MOVE的時(shí)候,調(diào)用dispatchNestedPreScroll讓父布局處理
- 在MotionEvent.ACTION_UP的時(shí)候,stopNestedScroll ,由于WebView是ViewGroup,所以可以直接在dispatchTouchEvent處理,如果是View可以在onTouchEvent中處理
如此一個(gè)簡(jiǎn)單的NestedScrollingChild就完成了,但是只有這個(gè)并不能完成上述需求,還需要一個(gè)NestedScrollingParent來(lái)配合,其實(shí)這里大部分的功能跟上述onInterceptTouchEvent的實(shí)現(xiàn)的類似,只不過(guò)
class NestUpDownTwoPartsScrollView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0,defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {private val gestureDetector: GestureDetector =GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {override fun onFling(與onInterceptTouchEvent一致...})...<!--標(biāo)志父布局支持垂直的嵌套滑動(dòng)-->override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL}<!--被NestedScrollingChild回調(diào)-->override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {overScrollerNest.abortAnimation()scrollInner(dy)<!--完全給消費(fèi)-->consumed[1] = dy}
<!--攔截子View們的fling--> override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {// 獲取的fling速度有差異,原因不詳return true}var mLastOverScrollerValue = 0<!--自己處理fling-->override fun computeScroll() {super.computeScroll()<!--自己處理fling-->if (overScrollerNest.computeScrollOffset()) {scrollInner(overScrollerNest.currY - mLastOverScrollerValue)mLastOverScrollerValue = overScrollerNest.currYinvalidate()}}private lateinit var overScrollerNest: OverScrolleroverride fun computeVerticalScrollRange(): Int {return totalHeight}private fun scrollInner(dy: Int) {... 與onInterceptTouchEvent一致}override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {if (ev != null) {gestureDetector.onTouchEvent(ev)}if (ev?.action == MotionEvent.ACTION_DOWN)overScrollerNest.abortAnimation()return super.dispatchTouchEvent(ev)}
}
父布局的操作如下
- onStartNestedScroll 返回true 啟動(dòng)
- 被子View調(diào)用onNestedPreScroll開(kāi)始協(xié)同滑動(dòng)
- onNestedPreFling接管Fling
- 利用GestureDetector+OverScroller自行處理Fling
可以看到,在這個(gè)框架下,可以比較靈活的接管拖動(dòng),不用自己攔截,而且消費(fèi)多少,可以父子協(xié)商,關(guān)于Fling,可以處理成一致,而且有兩個(gè)滾動(dòng)布局銜接的時(shí)候,交給外部統(tǒng)一處理應(yīng)該也是最合理的做法,防止兩個(gè)View的Scroller不一致,而且嵌套滑動(dòng)也無(wú)法處理target切換的問(wèn)題。
NestedScrolling框架 使用注意點(diǎn)
- fling處理:盡量不使用內(nèi)層GestureDetector來(lái)獲取,因?yàn)閮?nèi)外側(cè)獲取MotionEvent不是統(tǒng)一的,所以內(nèi)外層獲取的fling初始速度可能不同,銜接易出問(wèn)題,還是統(tǒng)一給外層自己做
- move處理拖拽:盡量使用rawY,因?yàn)镸otionEvent獲取的Y在嵌套滾動(dòng)時(shí)候不如rawY直觀,rawY始終是相對(duì)屏幕,而Y是相對(duì)自己View,在父View進(jìn)行滾動(dòng)的時(shí)候,target的Y幾乎是不動(dòng)的
強(qiáng)大的RecyclerView
RecyclerView適配一切,利用RecyclerView內(nèi)嵌WebView也能實(shí)現(xiàn)上述效果:但需要主動(dòng)控制內(nèi)部可滾動(dòng)Item。RecyclerView自身實(shí)現(xiàn)了onInterceptTouchEvent邏輯,理論上內(nèi)部子View是無(wú)法獲取到攔截之后的事件,只能依賴外部主動(dòng)控制,否則WebView被拖到頂部就結(jié)束了,內(nèi)部無(wú)法繼續(xù)拖拽。但是RecyclerView本身實(shí)現(xiàn)了NestedScrollingChild3,可以看做是一個(gè)支持嵌套滑動(dòng)的Child,在NestedScrolling框架中Move事件時(shí)一般會(huì)直接調(diào)用dispatchNestedPreScroll,之后dispatchNestedPreScroll會(huì)區(qū)分是否能啟用嵌套滑動(dòng)。因此除了借助 onInterceptTouchEvent邏輯,還可以借助dispatchNestedPreScroll來(lái)處理,一種很猥瑣的做法:繼承RecyclerView,復(fù)寫dispatchNestedPreScroll,這個(gè)時(shí)候繼承類先父類RecyclerView獲取事件處理的優(yōu)先權(quán),在一定程度上看做實(shí)現(xiàn)了onInterceptTouchEvent的NestedScrollingChild,在復(fù)寫的dispatchNestedPreScroll種處理子View的滾動(dòng)。即可自身滾動(dòng),也能控制內(nèi)部可滾動(dòng)View的滾動(dòng),但很難做到那么通用。不過(guò)在做業(yè)務(wù)的時(shí)候,思路有時(shí)候勝過(guò)單純的技術(shù),尤其嵌套滑動(dòng),不需要過(guò)分追求通用型控件。
對(duì)于上述交互場(chǎng)景,只需在dispatchNestedPreScroll做如下處理:只需要主動(dòng)接手內(nèi)部子View的操控,外部的操控?zé)o需處理
override fun dispatchNestedPreScroll( dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {var consumedSelf = false// 先讓父布局處理,還是后處理?val parentScrollConsumed = mParentScrollConsumedval parentConsumed = super.dispatchNestedPreScroll( dx, dy, parentScrollConsumed, offsetInWindow, type)consumed?.let {consumed[1] += parentScrollConsumed[1]}<!--再交給自己已處理-->if (type == ViewCompat.TYPE_TOUCH) {<!--對(duì)于向上滾動(dòng),如果自身可是滾動(dòng)就直接滾動(dòng)自身,說(shuō)明還沒(méi)到頂部RecyclerView會(huì)自己處理,自己攔截過(guò)了無(wú)需外部干預(yù),如果自身不能滾,就滾動(dòng)內(nèi)部的可滾動(dòng)target-->if (!canScrollVertically(1)) {//外部自身的操控?zé)o需處理<!--fetchNestedChild是用來(lái)獲取內(nèi)部的可滾動(dòng)View,這個(gè)看具體業(yè)務(wù)操作-->val remain = dy - (consumed?.get(1) ?: 0)if (remain > 0) {// 已經(jīng)到頂了if (!canScrollVertically(1)) {val target = fetchBottomNestedScrollChild()target?.apply {this.scrollBy(0, remain)consumed?.let {it[1] += remain}consumedSelf = true}}}// down 其實(shí)還是自己控制,而不是底層控制if (remain < 0) {val target = fetchBottomNestedScrollChild()target?.apply {if (this.canScrollVertically(-1)) {this.scrollBy(0, remain)// 消耗完,不給底層機(jī)會(huì)consumed?.let {it[1] += remain}consumedSelf = true}}}}return consumedSelf || parentConsumed
}
拖拽是比較容易處理的,比較棘手的是對(duì)于fling的處理,fling是一次性的,如果Recycleview繼承類dispatchNestedPreFling自己處理了fling后,父布局就獲取不到,銜接就比較麻煩
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {fling(velocityY)return true}
如果Recycleview自身通過(guò)OverScroller處理完畢后,還有盈余,就需要將盈余給外部,先處理內(nèi)部,還是先處理外部都是可選的,看用戶自己
override fun computeScroll() {if (overScroller.computeScrollOffset()) {val current = overScroller.currYval dy = current - mCurrentFlingmCurrentFling = currentval target = fetchBottomNestedScrollChild()if (dy > 0) {if (canScrollVertically(1)) {scrollBy(0, dy)} else {if (target?.canScrollVertically(1) == true) {target.scrollBy(0, dy)} else {if (!overScroller.isFinished) {overScroller.abortAnimation()// fling 先內(nèi)部,給上面接管一部分startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)dispatchNestedFling(0f, overScroller.currVelocity, false)stopNestedScroll()}
處理方式就是內(nèi)部不可fling之后,主動(dòng)通過(guò)startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)與 dispatchNestedFling再次交給父布局。
總結(jié)
流暢交互全靠微調(diào)