参考资料:
- http://blog.csdn.net/dingding_android/article/details/52948379
- http://www.jianshu.com/p/1bc9e712ee71
- https://segmentfault.com/a/1190000002873657
实例完整代码,请参考:
https://github.com/momodae/KotlinWidget/blob/master/widget/src/main/java/cz/widget/layout/SlidingUpLayout.kt
嵌套滑动的解释
在传统的android事件分发过程中,如果子view消费了事件,后续的事件都会交给该子view;但在某些场合,上如一篇,在 listview 滑动下拉到头时,如果继续下拉,希望,父能够拉下来;这就涉及到了嵌套滑动了;
简而言之,子view与父view一起来协作处理滑动的过程,整个流程在开启在子view;
用我一好哥们的解释:android传统的事件分发解决方案处理,像中式教育(孩子指子view
如果你想解决这个问题,那你自己解决就好了),而新的嵌套滑动机制,更像西式教育(父与孩子一起解决问题);
NestedScrolling嵌套滑动相关类与接口
- NestedScrollingChild
- NestedScrollingParent
- NestedScrollingChildHelper
- NestedScrollingParentHelper
如果想实现NestedScrolling,子view需要实现NestedScrollingChild接口,父View实现NestedScrollingParent接口, Helper为其帮助类;
接口方法非常的多,我们来看一下:
NestedScrollingChild 接口方法:
4个主要的方法,分别标上了注释,其他方法需要在实战中理解
public void setNestedScrollingEnabled(boolean enabled) {
}
public boolean isNestedScrollingEnabled() {
return false;
}
/**
* 由子View开启NestedScrolling机制,通知父容器,我要和你配合处理
Touch事件;
* 这个函数内部会去寻找实现了NestedScrolling机制的父容器,
* 如果找到了就返回true,如果没有则返回false。
**/
public boolean startNestedScroll(int axes) {
return false;
}
/**
* 结束整个流程
*/
public void stopNestedScroll() {
}
public boolean hasNestedScrollingParent() {
return false;
}
/**
* 在子元素滑动之后调用,向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return false;
}
/**
* 一般在onTouch的MOVE事件中调用,用来通知父容器,我要和你配合处理Touch事件
* 如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回
true,否则为false
* @param dx 水平滑动距离
* @param dy 垂直滑动距离
* @param consumed consumed数组中存放着父容器消费掉的距离,
consumed[0]是x轴上的距离,consumed[1]是y轴上的距离
* @param offsetInWindow offsetInWindow偏移量
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return false;
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return false;
}
NestedScrollingParent 接口方法:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return false;
}
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
}
public void onStopNestedScroll(View target) {
}
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
}
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
public int getNestedScrollAxes() {
return 0;
}
整个流程对应关系:
一般是子view发起调用,父view接受回调。
子View | 父View |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
NestedScrollingHelper 相关源码请参考:
http://www.jianshu.com/p/7548398c41ff
分析的很详细;
实战例子 - 上拉显示商品详情页
前面的介绍中,有2个实例,都非常好;可以试试;这里参考一好哥们实现的代码,将其思路一步一步展示出来;
类似于jd的商品详情页;
布局文件
通过自定义布局,嵌入2个或者3个子View(布局的上部分 topLayout
与下部分bottomLayout
都用 NestScrollingView
包裹起来,并设置了id值);
topLayout 用于展示默认状态下的商品基本信息;
bottomLayout 当上拉完成时,用于展示商品详情
布局文件示例如下:
......
新建SlidingUpLayout类,用来对应布局文件
实现 NestedScrollingParent
接口,重写布局、测量方法,并设置一些减速标记信息;
主要方法在 onNestedPreScroll
中
class SlidingUpLayout(context: Context, attrs: AttributeSet?, defAttrStyle: Int) :
ViewGroup(context, attrs, defAttrStyle), NestedScrollingParent {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
companion object {
val TWO_CHILD_COUNT = 2
val THREE_CHILD_COUNT = 3
val TAG = "SlidingUpLayout"
val DEBUG = true
}
private val viewConfig: ViewConfiguration = ViewConfiguration.get(context)
private var velocityTracker: VelocityTracker? = null
private val scroller = ScrollerCompat.create(context)
private val maxFlingVelocity = viewConfig.scaledMaximumFlingVelocity
private val minFlingVelocity = viewConfig.scaledMinimumFlingVelocity
private var scrollDuration = 1000
private val resistance = 1.8f // 阻力系数
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (childCount != TWO_CHILD_COUNT && THREE_CHILD_COUNT != childCount) {
throw IllegalArgumentException("Error child count! must two or three child count")
}
measureChildren(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var layoutTop = paddingTop
(0..childCount - 1).map { getChildAt(it) }.forEach {
it.layout(l, layoutTop, r, layoutTop + it.measuredHeight)
layoutTop += it.measuredHeight
}
}
// -------------------- 嵌套滑动 -----------------------
override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
return 0 != (ViewCompat.SCROLL_AXIS_VERTICAL and nestedScrollAxes) // 开启
}
/**
* target 会发生变化,要么是topLayout,要么是bottomLayout
*/
/**
* target 会发生变化,要么是topLayout,要么是bottomLayout
*/
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
val topLayout = getChildAt(0)
val bottomLayout = getChildAt(childCount - 1)
if (null != topLayout.findViewById(target.id)) {
if (dy > 0 && !ViewCompat.canScrollVertically(target, dy)) { // (上拉)
consumed[1] = (dy / resistance).toInt()
scrollBy(dx, consumed[1])
} else if (dy < 0 && scrollY > 0) { // 下拉 && SlidingUpLayout整体 往上偏移时) 消费dy
consumed[1] = dy
scrollBy(dx, (consumed[1] / resistance).toInt())
}
} else if (null != bottomLayout.findViewById(target.id)) {
// (下拉) or (bottomLayout没有完整显示时),拦截
// 完整显示时:bottomLayout.top = scrollY
if ((dy < 0 && !ViewCompat.canScrollVertically(target, dy)) ||
bottomLayout.top > scrollY) {
consumed[1] = dy
var dy = (dy / resistance).toInt()
if (scrollY + dy > bottomLayout.top) { // 不能越界
dy = bottomLayout.top - scrollY
}
scrollBy(dx, dy)
}
}
}
看一下效果:
展示与关闭详情页逻辑 (onStopNestedScroll)
在手指抬起那一刻,我们判断一下,滑动的距离,进行详情页的关闭与打开;
在 onStopNestedScroll
回调方法中,进行距离判断,因 onStopNestedScroll
方法nestedScrolling开始时,便会调用,所以在这里加入成员变量记录一下,避免关闭or打开方法的无效执行;
private var isNestedPreScroll = false //此标记标记nested内为拖动事件
// onNestedPreScroll 发生了设置为true
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
...
isNestedPreScroll = true
}
override fun onStopNestedScroll(target: View) {
if (DEBUG) Log.e(TAG, "onStopNestedScroll")
if (isNestedPreScroll) {
val topLayout = getChildAt(0)
val bottomLayout = getChildAt(childCount - 1)
if (null != topLayout.findViewById(target.id)) {
if (scrollY > minScrollDistance) {
setLayoutShadow(Gravity.END) // 显示bottom
} else {
setLayoutShadow(Gravity.START)
}
} else if (null != bottomLayout.findViewById(target.id)) {
if (scrollY + minScrollDistance < bottomLayout.top) {
setLayoutShadow(Gravity.START)
} else {
setLayoutShadow(Gravity.END) // 显示bottom
}
}
}
isNestedPreScroll = false
}
/**
* 打开或关闭当前布局体
* 展开上边
* @see Gravity.TOP Gravity.START
* 展开底部
* @see Gravity.BOTTOM Gravity.END
*/
fun setLayoutShadow(gravity: Int) {
if (gravity == Gravity.BOTTOM || gravity == Gravity.END) {
val bottomLayout = getChildAt(childCount - 1)
scroller.startScroll(scrollX, scrollY, 0, bottomLayout.top - scrollY, scrollDuration)
} else if (gravity == Gravity.TOP || gravity == Gravity.START) {
scroller.startScroll(scrollX, scrollY, 0, -scrollY, scrollDuration)
}
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
效果如下:
处理Fling
监听瞬间加速度,重写系统的 dispatchTouchEvent
,在这里进行加速度的判断;新增成员变量 isNestedFling
用来记录是否 fling,避免 onStopNestedScroll 再次执行
private var isNestedFling = false//此标记标记nested本次流程内为惯性滑动事件
// 修改一下,加入变量判断
override fun onStopNestedScroll(target: View) {
if (isNestedPreScroll && !isNestedFling) {
....
}
isNestedPreScroll = false
isNestedFling = false
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
initVelocityTrackerIfNotExists()
velocityTracker?.addMovement(ev)
when (ev.action) {
MotionEvent.ACTION_UP -> {
velocityTracker?.let {
it.computeCurrentVelocity(1000, maxFlingVelocity.toFloat())
if (Math.abs(it.yVelocity) > minFlingVelocity) {
val topLayout = getChildAt(0)
val bottomLayout = getChildAt(childCount - 1)
val scrollView = getCurrentScrollView(ev.y)
if (DEBUG) {
Log.e(TAG, "fling===> scrollY:$scrollY yVelocity: ${it.yVelocity} scrollView: $scrollView")
}
if (scrollY > 0 && topLayout == scrollView) { // 向上滑动
if (it.yVelocity > 0) {
setLayoutShadow(Gravity.START)
} else {
setLayoutShadow(Gravity.END)
}
} else if (bottomLayout == scrollView && (scrollY < bottomLayout.top)) { // 向下滑动
if (it.yVelocity > 0) {
setLayoutShadow(Gravity.START)
} else {
setLayoutShadow(Gravity.END)
}
}
isNestedFling = true
}
}
releaseVelocity()
}
MotionEvent.ACTION_CANCEL -> releaseVelocity()
}
return super.dispatchTouchEvent(ev)
}
效果如下:
细节整理
scroller 未结束的一些整理
override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean{
if(!scroller.isFinished){
scroller.abortAnimation()
invalidate()
}
return 0!=(ViewCompat.SCROLL_AXIS_VERTICAL and nestedScrollAxes)
}