【移动应用开发技术】Android中怎么实现嵌套滚动_第1页
【移动应用开发技术】Android中怎么实现嵌套滚动_第2页
【移动应用开发技术】Android中怎么实现嵌套滚动_第3页
【移动应用开发技术】Android中怎么实现嵌套滚动_第4页
【移动应用开发技术】Android中怎么实现嵌套滚动_第5页
免费预览已结束,剩余2页可下载查看

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

【移动应用开发技术】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事件,使得列表可以继续滚动

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:<org.cgspine.nestscroll.one.EventDispatchPlanLayout

android: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">

<View

android:id="@id/book_header"

android:layout_width="120dp"

android:layout_height="150dp"

android:background="@color/gray"/>

<org.cgspine.nestscroll.one.EventDispatchTargetLayout

android: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.TabLayout

android: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.ViewPager

android: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: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;

//

获取瞬时速度

mVelocityTputeCurrentVelocity(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的)。里面主要的逻辑就是两个:滚动容器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主要有两个接口:NestedScrollingParentNestedScrollingChild当我们需要使用NestingScroll特性时,我们去实现这两个接口就好了。NestingScroll本质是内部拦截发然后将相应的接口开给外界。因此实现NestedScrollingChild接口是有难度的,不过像RecyclerView这些控件,官方已经帮我们实现好了NestedScrollingChild,要完成我们的需求,我们直接拿来用就好了(ListView就没办法使用了,当然你也可以

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论