Android中如何实现嵌套滚动-成都快上网建站

Android中如何实现嵌套滚动

这篇文章主要介绍了Android中如何实现嵌套滚动的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Android中如何实现嵌套滚动文章都会有所收获,下面我们一起来看看吧。

创新互联建站是一家集网站建设,泰安企业网站建设,泰安品牌网站建设,网站定制,泰安网站建设报价,网络营销,网络优化,泰安网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。

业务需求是:

  1. VT容器可以滚动;

  2. 书籍封面可以滚动,并且有视差;

  3. 当VT容器滚动到顶部时,滚动列表,并且滚动可以衔接。

  4. 当列表滚动到顶部时,可以滚动书籍封面以及VT容器,并且滚动可以衔接

逻辑清楚了,接下来就看如何实现了。在android5以前,对于这种滚动,我们只能选择自己去拦截事件并处理,但在后面的某个版本,android推出了NestingScroll机制,开发者的日子就好过多了,并且android提供了一个非常好的容器类:CoordinatorLayout,极大的简化了开发者的工作。当然我们也需要投入精力去学习并运用这些新的Api了。

当然,我们也要知道如果没有这些API,我们应当如何去实现这些效果。因此本文会用三种方式去实现这个效果:

  1. 纯事件拦截与派发方案

  2. 基于NestingScroll机制的实现方案

  3. 基于CoordinatorLayout与Behavior方案的实现

示例代码放在Github上,可以clone下来结合文章观看

纯事件拦截与派发方案

这是最为原始的方案,当然也灵活性***的了。其它的方案原理上都是系统基于它提供的封装。使用这种方案时,我们需要解决以下几个问题:

  1. view的滚动(Scroller);

  2. view的速度追踪(VelocityTracker);

  3. 当VT容器滚动到顶部时,我们如何将事件传递给ListView?

  4. 当ListView滚动到顶部时,VT容器如何拦截到事件?

1、2两点属于滚动的基础知识,这里不会做细致的讲解。而第3点为何会出现呢?因为android系统在事件派发时,如果事件被拦截,那么之后的事件都将不会传递给子view了。其解决方案也很简单:在滚动到顶部时主动派发一次Down事件:

if (mTargetCurrentOffset + dy <= mTargetEndOffset) {     moveTargetView(dy);     // 重新dispatch一次down事件,使得列表可以继续滚动     int oldAction = ev.getAction();     ev.setAction(MotionEvent.ACTION_DOWN);     dispatchTouchEvent(ev);     ev.setAction(oldAction); } else {     moveTargetView(dy); }

那么第4点是什么问题呢?这里就需要清楚一个坑点了:不是所用的事件都会走入onInterceptTouchEvent。有一种情况是子View主动调用parent.requestDisallowInterceptTouchEvent(true)来告诉系统说:这个事件我要了,父View不要拦截了。这就是所谓的内部拦截法。在ListView的某些时刻它会去调用这个方法。因此一旦事件传递给了ListView,外部容器就拿不到这个事件了。因此我们要打破它的内部拦截:

@Override public void requestDisallowInterceptTouchEvent(boolean b) {     // 去掉默认行为,使得每个事件都会经过这个Layout }

方法如上,把requestDisallowInterceptTouchEvent的实现干掉就可以了。

主要的技术点已近提出来了。那么下面就看具体实现,首先看使用xml:

                                  

EventDispatchTargetLayout实现了自定义接口ITargetView:

public interface ITargetView {     boolean canChildScrollUp();     void fling(float vy); }

这是因为与具体业务抽离,我并不清楚内层盒子是怎样的(有可能就是ListView了,也有可能是ViewPager包裹ListView)

主要的实现在EventDispatchPlanLayout,使用时在xml中指定header_init_offset、target_init_offset等变量就可以了,基本上与业务逻辑独立。

其重点实现逻辑在onInterceptTouchEvent与onTouchEvent中了。个人不是很建议去动dispatchTouchEvent,虽然所有事件都会经过这里,但是这也明显会增加代码处理复杂度:

public boolean onInterceptTouchEvent(MotionEvent ev) {     ensureHeaderViewAndScrollView();     final int action = MotionEventCompat.getActionMasked(ev);     int pointerIndex;      // 不阻断事件的快路径:如果目标view可以往上滚动或者`EventDispatchPlanLayout`不是enabled     if (!isEnabled() || mTarget.canChildScrollUp()) {         Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = "                 + mTarget.canChildScrollUp());         return false;     }     switch (action) {         case MotionEvent.ACTION_DOWN:             mActivePointerId = ev.getPointerId(0);             mIsDragging = false;             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 return false;             }             // 在down的时候记录初始的y值             mInitialDownY = ev.getY(pointerIndex);             break;          case MotionEvent.ACTION_MOVE:             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                 return false;             }              final float y = ev.getY(pointerIndex);             // 判断是否dragging             startDragging(y);             break;          case MotionEventCompat.ACTION_POINTER_UP:             // 双指逻辑处理             onSecondaryPointerUp(ev);             break;          case MotionEvent.ACTION_UP:         case MotionEvent.ACTION_CANCEL:             mIsDragging = false;             mActivePointerId = INVALID_POINTER;             break;     }      return mIsDragging; }

代码逻辑很清晰,应该不用多说。接下来看onTouchEvent的处理逻辑。

public boolean onTouchEvent(MotionEvent ev) {     final int action = MotionEventCompat.getActionMasked(ev);     int pointerIndex;      if (!isEnabled() || mTarget.canChildScrollUp()) {         Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = "                 + mTarget.canChildScrollUp());         return false;     }    // 速度追踪    acquireVelocityTracker(ev);      switch (action) {         case MotionEvent.ACTION_DOWN:             mActivePointerId = ev.getPointerId(0);             mIsDragging = false;             break;          case MotionEvent.ACTION_MOVE: {             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                 return false;             }             final float y = ev.getY(pointerIndex);             startDragging(y);              if (mIsDragging) {                 float dy = y - mLastMotionY;                 if (dy >= 0) {                     moveTargetView(dy);                 } else {                     if (mTargetCurrentOffset + dy <= mTargetEndOffset) {                         moveTargetView(dy);                         // 重新dispatch一次down事件,使得列表可以继续滚动                         int oldAction = ev.getAction();                         ev.setAction(MotionEvent.ACTION_DOWN);                         dispatchTouchEvent(ev);                         ev.setAction(oldAction);                     } else {                         moveTargetView(dy);                     }                 }                 mLastMotionY = y;             }             break;         }         case MotionEventCompat.ACTION_POINTER_DOWN: {             pointerIndex = MotionEventCompat.getActionIndex(ev);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");                 return false;             }             mActivePointerId = ev.getPointerId(pointerIndex);             break;         }          case MotionEventCompat.ACTION_POINTER_UP:             onSecondaryPointerUp(ev);             break;          case MotionEvent.ACTION_UP: {             pointerIndex = ev.findPointerIndex(mActivePointerId);             if (pointerIndex < 0) {                 Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");                 return false;             }              if (mIsDragging) {                 mIsDragging = false;                 // 获取瞬时速度                 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);                 final float vy = mVelocityTracker.getYVelocity(mActivePointerId);                 finishDrag((int) vy);             }             mActivePointerId = INVALID_POINTER;             //释放速度追踪             releaseVelocityTracker();             return false;         }         case MotionEvent.ACTION_CANCEL:             releaseVelocityTracker();             return false;     }      return mIsDragging; }

或许有人会说:为何与onInterceptTouchEvent与有很多重复代码?这是因为如果事件不打断,并且子类不处理,就会走进onTouchEvent逻辑,所以这些重复处理是有意义的(其实是抄SwipeRefreshLayout的)。里面主要的逻辑就是两个:

  1. 滚动容器

  2. TouchUp时滚动到特定位置以及fling传递

滚动容器的逻辑:

private void moveTargetViewTo(int target) {     target = Math.max(target, mTargetEndOffset);     // 用offsetTopAndBottom来偏移view     ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrentOffset);     mTargetCurrentOffset = target;      // 滚动书籍封面view,根据TargetView进行定位     int headerTarget;     if (mTargetCurrentOffset >= mTargetInitOffset) {         headerTarget = mHeaderInitOffset;     } else if (mTargetCurrentOffset <= mTargetEndOffset) {         headerTarget = mHeaderEndOffset;     } else {         float percent = (mTargetCurrentOffset - mTargetEndOffset) * 1.0f / mTargetInitOffset - mTargetEndOffset;         headerTarget = (int) (mHeaderEndOffset + percent * (mHeaderInitOffset - mHeaderEndOffset));     }     ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrentOffset);     mHeaderCurrentOffset = headerTarget; }

TouchUp的滚动逻辑:

private void finishDrag(int vy) {     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();     } else if (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的定义:

public interface NestedScrollingParent {     // 是否接受NestingScroll     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);     // 接受NestingScroll的Hook钩子     public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);     // NestingScroll结束     public void onStopNestedScroll(View target);     // NestingScroll进行中。重要参数dxUnconsumed, dyUnconsumed: 用于表示没有被消耗的滚动量,一般是列表滚动到头了,就会产生未消耗量     public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);     // NestingScroll滚动之前。重要参数consumed: 是用于告诉子View我消耗了多少。如果位全部消耗dy,那么子view就可以消耗了。     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);     // fling时     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);     // fling之前:可以由父元素消耗这次fling事件     public boolean onNestedPreFling(View target, float velocityX, float velocityY);    // 获取滚动轴: x轴或y轴    public int getNestedScrollAxes(); }

关于“Android中如何实现嵌套滚动”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Android中如何实现嵌套滚动”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注创新互联行业资讯频道。


网站栏目:Android中如何实现嵌套滚动
标题URL:http://kswjz.com/article/jhocje.html
扫二维码与项目经理沟通

我们在微信上24小时期待你的声音

解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流