Android中如何实现嵌套滚动
Android中如何实现嵌套滚动
这篇文章主要介绍了Android中如何实现嵌套滚动的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Android中如何实现嵌套滚动文章都会有所收获,下面我们一起来看看吧。
业务需求是:
VT容器可以滚动;
书籍封面可以滚动,并且有视差;
当VT容器滚动到顶部时,滚动列表,并且滚动可以衔接。
当列表滚动到顶部时,可以滚动书籍封面以及VT容器,并且滚动可以衔接
逻辑清楚了,接下来就看如何实现了。在android5以前,对于这种滚动,我们只能选择自己去拦截事件并处理,但在后面的某个版本,android推出了NestingScroll机制,开发者的日子就好过多了,并且android提供了一个非常好的容器类:CoordinatorLayout,极大的简化了开发者的工作。当然我们也需要投入精力去学习并运用这些新的Api了。
当然,我们也要知道如果没有这些API,我们应当如何去实现这些效果。因此本文会用三种方式去实现这个效果:
纯事件拦截与派发方案
基于NestingScroll机制的实现方案
基于CoordinatorLayout与Behavior方案的实现
示例代码放在Github上,可以clone下来结合文章观看
纯事件拦截与派发方案
这是最为原始的方案,当然也灵活性***的了。其它的方案原理上都是系统基于它提供的封装。使用这种方案时,我们需要解决以下几个问题:
view的滚动(Scroller);
view的速度追踪(VelocityTracker);
当VT容器滚动到顶部时,我们如何将事件传递给ListView?
当ListView滚动到顶部时,VT容器如何拦截到事件?
1、2两点属于滚动的基础知识,这里不会做细致的讲解。而第3点为何会出现呢?因为android系统在事件派发时,如果事件被拦截,那么之后的事件都将不会传递给子view了。其解决方案也很简单:在滚动到顶部时主动派发一次Down事件:
if(mTargetCurrentOffset+dy<=mTargetEndOffset){moveTargetView(dy);//重新dispatch一次down事件,使得列表可以继续滚动intoldAction=ev.getAction();ev.setAction(MotionEvent.ACTION_DOWN);dispatchTouchEvent(ev);ev.setAction(oldAction);}else{moveTargetView(dy);}
那么第4点是什么问题呢?这里就需要清楚一个坑点了:不是所用的事件都会走入onInterceptTouchEvent。有一种情况是子View主动调用parent.requestDisallowInterceptTouchEvent(true)来告诉系统说:这个事件我要了,父View不要拦截了。这就是所谓的内部拦截法。在ListView的某些时刻它会去调用这个方法。因此一旦事件传递给了ListView,外部容器就拿不到这个事件了。因此我们要打破它的内部拦截:
@OverridepublicvoidrequestDisallowInterceptTouchEvent(booleanb){//去掉默认行为,使得每个事件都会经过这个Layout}
方法如上,把requestDisallowInterceptTouchEvent的实现干掉就可以了。
主要的技术点已近提出来了。那么下面就看具体实现,首先看使用xml:
<org.cgspine.nestscroll.one.EventDispatchPlanLayoutandroid:id="@+id/scrollLayout"android:layout_marginTop="?attr/actionBarSize"android:layout_width="match_parent"android:layout_height="match_parent"app:header_view="@+id/book_header"app:target_view="@+id/scroll_view"app:header_init_offset="30dp"app:target_init_offset="70dp"><Viewandroid:id="@id/book_header"android:layout_width="120dp"android:layout_height="150dp"android:background="@color/gray"/><org.cgspine.nestscroll.one.EventDispatchTargetLayoutandroid:id="@id/scroll_view"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="@color/white"><android.support.design.widget.TabLayoutandroid:id="@+id/tab_layout"android:background="@drawable/list_item_bg_with_border_top_bottom"android:layout_width="match_parent"android:layout_height="@dimen/tab_layout_height"android:fillViewport="true"/><android.support.v4.view.ViewPagerandroid:id="@+id/viewpager"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"/></org.cgspine.nestscroll.one.EventDispatchTargetLayout></org.cgspine.nestscroll.one.EventDispatchPlanLayout>
EventDispatchTargetLayout实现了自定义接口ITargetView:
publicinterfaceITargetView{booleancanChildScrollUp();voidfling(floatvy);}
这是因为与具体业务抽离,我并不清楚内层盒子是怎样的(有可能就是ListView了,也有可能是ViewPager包裹ListView)
主要的实现在EventDispatchPlanLayout,使用时在xml中指定header_init_offset、target_init_offset等变量就可以了,基本上与业务逻辑独立。
其重点实现逻辑在onInterceptTouchEvent与onTouchEvent中了。个人不是很建议去动dispatchTouchEvent,虽然所有事件都会经过这里,但是这也明显会增加代码处理复杂度:
publicbooleanonInterceptTouchEvent(MotionEventev){ensureHeaderViewAndScrollView();finalintaction=MotionEventCompat.getActionMasked(ev);intpointerIndex;//不阻断事件的快路径:如果目标view可以往上滚动或者`EventDispatchPlanLayout`不是enabledif(!isEnabled()||mTarget.canChildScrollUp()){Log.d(TAG,"fastendonIntercept:isEnabled="+isEnabled()+";canChildScrollUp="+mTarget.canChildScrollUp());returnfalse;}switch(action){caseMotionEvent.ACTION_DOWN:mActivePointerId=ev.getPointerId(0);mIsDragging=false;pointerIndex=ev.findPointerIndex(mActivePointerId);if(pointerIndex<0){returnfalse;}//在down的时候记录初始的y值mInitialDownY=ev.getY(pointerIndex);break;caseMotionEvent.ACTION_MOVE:pointerIndex=ev.findPointerIndex(mActivePointerId);if(pointerIndex<0){Log.e(TAG,"GotACTION_MOVEeventbuthaveaninvalidactivepointerid.");returnfalse;}finalfloaty=ev.getY(pointerIndex);//判断是否draggingstartDragging(y);break;caseMotionEventCompat.ACTION_POINTER_UP://双指逻辑处理onSecondaryPointerUp(ev);break;caseMotionEvent.ACTION_UP:caseMotionEvent.ACTION_CANCEL:mIsDragging=false;mActivePointerId=INVALID_POINTER;break;}returnmIsDragging;}
代码逻辑很清晰,应该不用多说。接下来看onTouchEvent的处理逻辑。
publicbooleanonTouchEvent(MotionEventev){finalintaction=MotionEventCompat.getActionMasked(ev);intpointerIndex;if(!isEnabled()||mTarget.canChildScrollUp()){Log.d(TAG,"fastendonTouchEvent:isEnabled="+isEnabled()+";canChildScrollUp="+mTarget.canChildScrollUp());returnfalse;}//速度追踪acquireVelocityTracker(ev);switch(action){caseMotionEvent.ACTION_DOWN:mActivePointerId=ev.getPointerId(0);mIsDragging=false;break;caseMotionEvent.ACTION_MOVE:{pointerIndex=ev.findPointerIndex(mActivePointerId);if(pointerIndex<0){Log.e(TAG,"GotACTION_MOVEeventbuthaveaninvalidactivepointerid.");returnfalse;}finalfloaty=ev.getY(pointerIndex);startDragging(y);if(mIsDragging){floatdy=y-mLastMotionY;if(dy>=0){moveTargetView(dy);}else{if(mTargetCurrentOffset+dy<=mTargetEndOffset){moveTargetView(dy);//重新dispatch一次down事件,使得列表可以继续滚动intoldAction=ev.getAction();ev.setAction(MotionEvent.ACTION_DOWN);dispatchTouchEvent(ev);ev.setAction(oldAction);}else{moveTargetView(dy);}}mLastMotionY=y;}break;}caseMotionEventCompat.ACTION_POINTER_DOWN:{pointerIndex=MotionEventCompat.getActionIndex(ev);if(pointerIndex<0){Log.e(TAG,"GotACTION_POINTER_DOWNeventbuthaveaninvalidactionindex.");returnfalse;}mActivePointerId=ev.getPointerId(pointerIndex);break;}caseMotionEventCompat.ACTION_POINTER_UP:onSecondaryPointerUp(ev);break;caseMotionEvent.ACTION_UP:{pointerIndex=ev.findPointerIndex(mActivePointerId);if(pointerIndex<0){Log.e(TAG,"GotACTION_UPeventbutdon'thaveanactivepointerid.");returnfalse;}if(mIsDragging){mIsDragging=false;//获取瞬时速度mVelocityTracker.computeCurrentVelocity(1000,mMaxVelocity);finalfloatvy=mVelocityTracker.getYVelocity(mActivePointerId);finishDrag((int)vy);}mActivePointerId=INVALID_POINTER;//释放速度追踪releaseVelocityTracker();returnfalse;}caseMotionEvent.ACTION_CANCEL:releaseVelocityTracker();returnfalse;}returnmIsDragging;}
或许有人会说:为何与onInterceptTouchEvent与有很多重复代码?这是因为如果事件不打断,并且子类不处理,就会走进onTouchEvent逻辑,所以这些重复处理是有意义的(其实是抄SwipeRefreshLayout的)。里面主要的逻辑就是两个:
滚动容器
TouchUp时滚动到特定位置以及fling传递
滚动容器的逻辑:
privatevoidmoveTargetViewTo(inttarget){target=Math.max(target,mTargetEndOffset);//用offsetTopAndBottom来偏移viewViewCompat.offsetTopAndBottom(mTargetView,target-mTargetCurrentOffset);mTargetCurrentOffset=target;//滚动书籍封面view,根据TargetView进行定位intheaderTarget;if(mTargetCurrentOffset>=mTargetInitOffset){headerTarget=mHeaderInitOffset;}elseif(mTargetCurrentOffset<=mTargetEndOffset){headerTarget=mHeaderEndOffset;}else{floatpercent=(mTargetCurrentOffset-mTargetEndOffset)*1.0f/mTargetInitOffset-mTargetEndOffset;headerTarget=(int)(mHeaderEndOffset+percent*(mHeaderInitOffset-mHeaderEndOffset));}ViewCompat.offsetTopAndBottom(mHeaderView,headerTarget-mHeaderCurrentOffset);mHeaderCurrentOffset=headerTarget;}
TouchUp的滚动逻辑:
privatevoidfinishDrag(intvy){Log.i(TAG,"TouchUp:vy="+vy);if(vy>0){//向下触发fling,需要滚动到Init位置mNeedScrollToInitPos=true;mScroller.fling(0,mTargetCurrentOffset,0,vy,0,0,mTargetEndOffset,Integer.MAX_VALUE);invalidate();}elseif(vy<0){//向上触发fling,需要滚动到End位置mNeedScrollToEndPos=true;mScroller.fling(0,mTargetCurrentOffset,0,vy,0,0,mTargetEndOffset,Integer.MAX_VALUE);invalidate();}else{//没有触发fling,就近原则if(mTargetCurrentOffset<=(mTargetEndOffset+mTargetInitOffset)/2){mNeedScrollToEndPos=true;}else{mNeedScrollToInitPos=true;}invalidate();}}
当然这里会打上一些标志位,具体实现是在computeScroll中,这属于Scroller的功能,这里就不展开了。
这样大体逻辑就讲述清楚了,其它细节就请看官直接看源码了。
基于NestingScroll机制的实现方案
NestingScroll机制是在某个版本support包加入的,不过外界极少有文章介绍,所以应该大多数人并不知道这个机制。NestingScroll主要有两个接口:
NestedScrollingParent
NestedScrollingChild
当我们需要使用NestingScroll特性时,我们去实现这两个接口就好了。NestingScroll本质是内部拦截发然后将相应的接口开给外界。因此实现NestedScrollingChild接口是有难度的,不过像RecyclerView这些控件,官方已经帮我们实现好了NestedScrollingChild,要完成我们的需求,我们直接拿来用就好了(ListView就没办法使用了,当然你也可以去实现NestedScrollingChild接口)。并且NestedScrollingChild与NestedScrollingParent只要有嵌套关系就行了,并不一定NestedScrollingChild是直接的子View。
我们来来看看NestedScrollingParent的定义:
publicinterfaceNestedScrollingParent{//是否接受NestingScrollpublicbooleanonStartNestedScroll(Viewchild,Viewtarget,intnestedScrollAxes);//接受NestingScroll的Hook钩子publicvoidonNestedScrollAccepted(Viewchild,Viewtarget,intnestedScrollAxes);//NestingScroll结束publicvoidonStopNestedScroll(Viewtarget);//NestingScroll进行中。重要参数dxUnconsumed,dyUnconsumed:用于表示没有被消耗的滚动量,一般是列表滚动到头了,就会产生未消耗量publicvoidonNestedScroll(Viewtarget,intdxConsumed,intdyConsumed,intdxUnconsumed,intdyUnconsumed);//NestingScroll滚动之前。重要参数consumed:是用于告诉子View我消耗了多少。如果位全部消耗dy,那么子view就可以消耗了。publicvoidonNestedPreScroll(Viewtarget,intdx,intdy,int[]consumed);//fling时publicbooleanonNestedFling(Viewtarget,floatvelocityX,floatvelocityY,booleanconsumed);//fling之前:可以由父元素消耗这次fling事件publicbooleanonNestedPreFling(Viewtarget,floatvelocityX,floatvelocityY);//获取滚动轴:x轴或y轴publicintgetNestedScrollAxes();}
关于“Android中如何实现嵌套滚动”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Android中如何实现嵌套滚动”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道。
推荐阅读
-
android(如何快速开发框架 小米note开发版MIUI,安卓6.0,怎么安装Xposed框架)
稳定版,你必须先根除。你上网搜索安卓可以叫别人s框架,对方可以把框架做成jar包,把这个jar包加载到项目目录的libs文件中使...
-
android(studio 虚拟机启动不了 android studio可以当模拟器用吗)
androidstudio可以当模拟器用吗?AmdCUP引导模拟器有点复杂。雷电模拟器上的抖音怎么登录不上?不是,闪电模拟调用...
-
从实践中学习手机抓包与数据分析(android 手机抓包app)
android手机抓包app?netcapture抓包精灵app(手机抓包工具)又名sslcapture,是什么专业的安卓手机抓...
-
android(studio全局搜索 android studio怎么看app界面)
androidstudio怎么看app界面?在设备桌面点击运用直接进入到App界面,就也可以参与其他你的操作了。android-...
-
怎么把android框架源代码拉到本地(android studio如何运行别人的源代码)
androidstudio如何运行别人的源代码?androidstudio点击刚建在列表中你选择导入module,导入即可在用...
-
android(studio2022年使用教程 怎么安装Android studio详细教程)
怎么安装Androidstudio详细教程?androidstudio中haxm直接安装的方法追加:1、简单的方法打开Andr...
-
怎么使用Android基准配置文件Baseline Profile方案提升启动速度
-
HTML5如何实现禁止android视频另存为
-
学java好还是学php好?
-
Android如何实现多点触控功能