下载本文档
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
【移动应用开发技术】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. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- GB/T 45008-2024稀土热障涂层材料锆酸钆镱粉末
- 电工电子技术(第3版) 课件 5.7 放大电路的负反馈
- 2023年铬系铁合金投资申请报告
- 银行内部审计档案管理制度
- 采购物资质量管理与追溯制度
- 2021年能源化工行业市场分析报告
- 【大学课件】计算机科学技术面临的挑战 高可信软件技术
- 《信访代理工作培训》课件
- 第3章 图形的初步认识 七年级上册数学华师大版(2024)单元质检B卷(含答案)
- 《机电一体化》课件 项目五 控制系统的设计
- 竣工验收程序流程图
- 资产处置拆除施工现场消防、安全保障协议书
- Q∕GDW 10799.6-2018 国家电网有限公司电力安全工作规程 第6部分:光伏电站部分
- 口腔科诊断证明书模板
- 商业银行授权管理办法
- 苏州小吃学习教案
- 妇科5个病种临床路径
- 精华网校高东辉一二三讲讲义
- 2021年全国中等职业学校学生服装设计与工艺技能大赛理论题库
- 五方单位评估报告
- 储罐制作安装预算书
评论
0/150
提交评论