延吉市网站建设,wordpress 多站点配置,国内开源平台,网络推广主要工作内容嵌套滚动#xff1a;内外两层均可滚动#xff0c;比如上半部分是一个有限的列表#xff0c;下半部分是WebView#xff0c;在内层上半部分展示到底的时候#xff0c;外部父布局整体滚动内部View#xff0c;将底部WevView拉起来#xff0c;滚动到顶部之后再将滚动交给内部…嵌套滚动内外两层均可滚动比如上半部分是一个有限的列表下半部分是WebView在内层上半部分展示到底的时候外部父布局整体滚动内部View将底部WevView拉起来滚动到顶部之后再将滚动交给内部WebView之后滚动的就是内部WebView如下图 实现onInterceptTouchEvent或者NestedScroll
按照上下两部分构建父布局父ViewGroup建议继承FrameLayout/RelativeLayout来实现方便处理测量[无需复写]与布局在计算出全部View高度后可以计算最大父布局滚动距离
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全局拦击Touch事件来实现拖动与Fling的处理另一种是借助后期推出的NestedScroll框架来实现。先简单看下传统的onInterceptTouchEvent拦截的方式核心的处理事两个操作一个是拖动、一个是UP后的FlingonInterceptTouchEvent首先要确定拦截的时机判断有效拖动
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)
}一般而言垂直滚动超过某个TouchSlop就可以认为拖动有效拖动开始子View后续无法获取到Touch事件其实大多数场景而言父布局接管之后没有必要再给子View分发事件之后自行处理拖拽与Fling。
onInterceptTouchEvent方式处理拖拽与fling
处理拖拽与fling需要注意衔接所以需要准备好GestureDetector用于将来的fling建议放在dispatchTouchEvent整体处理事件的消费
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)
}拖拽的控制方式自己计算出可滚动的距离可以利用View的canScrollVertically判断View是否能消费从而决定留给哪个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()}拖拽结束后fling跟上利用GestureDetector的onFling直接让Scroller接上即可 overScroller OverScroller(context)一般用OverScroller的体验好一些
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))) {!--衔接滚动--overScroller.fling(0, 0, 0, (int) velocityY, 0, 0, -10 * ScreenUtil.getDisplayHeight(), 10 * ScreenUtil.getDisplayHeight());!--必须触发一次--postInvalidate();}return super.onFling(e1, e2, velocityX, velocityY);}
});在computeScroll里从新计算应该滚动的距离可以看到全局接管并利用scrollBy自行控制滚动的偏移量是这种方案的核心 var mLastOverScrollerValue 0override fun computeScroll() {super.computeScroll()if (overScrollerNest.computeScrollOffset()) {scrollInner(overScrollerNest.currY - mLastOverScrollerValue)mLastOverScrollerValue overScrollerNest.currYinvalidate()}} 如此就可以利用onInterceptTouchEvent实现嵌套滚动不涉及太多内部View【仅仅是获取了内部View的高度及判断是否可滚】一切交给父布局即可。
利用NestedScrolling框架实现嵌套滑动
Android5.0推出了嵌套滑动机制NestedScrolling让父View和子View在滑动时相互协调配合为了向前兼容又抽离了NestedScrollingChild、NestedScrollingParent、NestedScrollingChildHelper、NestedScrollingParentHelper等支持类不过在23年的场景下基本不需要使用这些辅助类了。NestedScrolling的核心是子View一直能收到Move事件在自己处理之前先交给父View消费父View处理完之后再将余量还给子View让子View自己处理可以看出这套框架必须父子配合也就是NestedScrollingChild、NestedScrollingParent是配套的。5.0之后View与ViewGroup本身就实现了NestedScrollingChildNestedScrollingParent的框架自定义布局的时候只需要定制与启用也就是必须进行二次开发目前Google提供的最好用的就是RecyclerView。有张图很清晰的描述NestedScrolling框架是如何工作的 NestedScrolling只处理拖动[target无法改变]Fling交给Parent处理
在这个框架中子View必须主动启动嵌套滑动、并且在Move的时候主动请求父ViewGroup进行处理这样才能完成协同并非简单的打开开关所有的定制逻辑仍旧需要开发者自己处理只是替代了onInterceptTouchEvent提供了子View回传事件给父View的能力不用父View主动拦截也能获取接管子View事件的能力。
以开头描述的场景为例如果上部分用ScrollView下部分用WebView那么必须将两者都改造成NestedScrollingChild也就是NestedScrollView与NestedWebViewNestedScrollView谷歌已经提供NestedWebView目前没有需要自己封装可以看看如何配合实现一套嵌套滑动交互:
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 {!--启动嵌套滑动--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)}!--主动调用dispatchNestedPreScroll请求父容器处理--dispatchNestedPreScroll(0, (mLastY - ev.rawY).toInt(), mScrollConsumed, mScrollOffset)mLastY ev.rawY}}MotionEvent.ACTION_DOWN - {dragIng falsesuper.dispatchTouchEvent(ev)!--startNestedScroll启动嵌套滑动--startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)mScrollConsumed[1] 0mLastY ev.rawY}MotionEvent.ACTION_UP - stopNestedScroll()else - super.dispatchTouchEvent(ev)}return true}// 强制自己不消费moveoverride fun onTouchEvent(ev: MotionEvent?): Boolean {if (dragIng || ev?.action MotionEvent.ACTION_MOVE)return falsereturn super.onTouchEvent(ev)}}setNestedScrollingEnabled(true) ,后续的dispatch都依赖该开关子View收到DOWN事件的时候启动startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)【无论有没有NestedScrollingParent】父布局其实这个时候也会响应只有存在支持嵌套滑动的父布局后续dispatchNestedPreScroll等函数才有意义才有意义假设存在支持嵌套滑动的父布局在MOVE的时候调用dispatchNestedPreScroll让父布局处理在MotionEvent.ACTION_UP的时候stopNestedScroll 由于WebView是ViewGroup所以可以直接在dispatchTouchEvent处理如果是View可以在onTouchEvent中处理
如此一个简单的NestedScrollingChild就完成了但是只有这个并不能完成上述需求还需要一个NestedScrollingParent来配合其实这里大部分的功能跟上述onInterceptTouchEvent的实现的类似只不过 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一致...})...!--标志父布局支持垂直的嵌套滑动--override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {return nestedScrollAxes ViewCompat.SCROLL_AXIS_VERTICAL}!--被NestedScrollingChild回调--override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {overScrollerNest.abortAnimation()scrollInner(dy)!--完全给消费--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 启动被子View调用onNestedPreScroll开始协同滑动onNestedPreFling接管Fling利用GestureDetectorOverScroller自行处理Fling
可以看到在这个框架下可以比较灵活的接管拖动不用自己拦截而且消费多少可以父子协商关于Fling可以处理成一致而且有两个滚动布局衔接的时候交给外部统一处理应该也是最合理的做法防止两个View的Scroller不一致而且嵌套滑动也无法处理target切换的问题。
NestedScrolling框架 使用注意点
fling处理尽量不使用内层GestureDetector来获取因为内外侧获取MotionEvent不是统一的所以内外层获取的fling初始速度可能不同衔接易出问题还是统一给外层自己做move处理拖拽尽量使用rawY因为MotionEvent获取的Y在嵌套滚动时候不如rawY直观rawY始终是相对屏幕而Y是相对自己View在父View进行滚动的时候target的Y几乎是不动的
强大的RecyclerView
RecyclerView适配一切利用RecyclerView内嵌WebView也能实现上述效果但需要主动控制内部可滚动Item。RecyclerView自身实现了onInterceptTouchEvent逻辑理论上内部子View是无法获取到拦截之后的事件只能依赖外部主动控制否则WebView被拖到顶部就结束了内部无法继续拖拽。但是RecyclerView本身实现了NestedScrollingChild3可以看做是一个支持嵌套滑动的Child在NestedScrolling框架中Move事件时一般会直接调用dispatchNestedPreScroll之后dispatchNestedPreScroll会区分是否能启用嵌套滑动。因此除了借助 onInterceptTouchEvent逻辑还可以借助dispatchNestedPreScroll来处理一种很猥琐的做法继承RecyclerView复写dispatchNestedPreScroll这个时候继承类先父类RecyclerView获取事件处理的优先权在一定程度上看做实现了onInterceptTouchEvent的NestedScrollingChild在复写的dispatchNestedPreScroll种处理子View的滚动。即可自身滚动也能控制内部可滚动View的滚动但很难做到那么通用。不过在做业务的时候思路有时候胜过单纯的技术尤其嵌套滑动不需要过分追求通用型控件。
对于上述交互场景只需在dispatchNestedPreScroll做如下处理只需要主动接手内部子View的操控外部的操控无需处理 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) {!--对于向上滚动如果自身可是滚动就直接滚动自身说明还没到顶部RecyclerView会自己处理自己拦截过了无需外部干预如果自身不能滚就滚动内部的可滚动target--if (!canScrollVertically(1)) {//外部自身的操控无需处理!--fetchNestedChild是用来获取内部的可滚动View这个看具体业务操作--val remain dy - (consumed?.get(1) ?: 0)if (remain 0) {// 已经到顶了if (!canScrollVertically(1)) {val target fetchBottomNestedScrollChild()target?.apply {this.scrollBy(0, remain)consumed?.let {it[1] remain}consumedSelf true}}}// down 其实还是自己控制而不是底层控制if (remain 0) {val target fetchBottomNestedScrollChild()target?.apply {if (this.canScrollVertically(-1)) {this.scrollBy(0, remain)// 消耗完不给底层机会consumed?.let {it[1] remain}consumedSelf true}}}}return consumedSelf || parentConsumed
}拖拽是比较容易处理的比较棘手的是对于fling的处理fling是一次性的如果Recycleview继承类dispatchNestedPreFling自己处理了fling后父布局就获取不到衔接就比较麻烦 override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {fling(velocityY)return true}如果Recycleview自身通过OverScroller处理完毕后还有盈余就需要将盈余给外部先处理内部还是先处理外部都是可选的看用户自己
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 先内部给上面接管一部分startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)dispatchNestedFling(0f, overScroller.currVelocity, false)stopNestedScroll()}处理方式就是内部不可fling之后主动通过startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)与 dispatchNestedFling再次交给父布局。
总结
流畅交互全靠微调