1.开篇
网上相关的NestScrolling
介绍有很多,而NestScrolling
本身也是很简单的,说白了就是几个接口,里面定义了很多方法,而这些方法需要我们自己传递和处理。
我们平常能使用NestScrollView
和ReyclerView
等进行嵌套滑动,这些都是官方已经帮我们把接口处理完毕了
本篇打算自己实现接口完成简单的Demo练习,而不借助NestScrollView
等已经实现接口的View的继承,用最基本的几大布局LinearLayout
,FrameLayout
等来实现如下的效果
本篇涉及的点包含NestScrollParent
,NestScrollChild
以及自定义Behavior
的机制
2.Behavior
上面看起来很复杂,其实说白了也就是手势的处理和分发了,手势分发我们都特别属性,那么嵌套滑动的核心就是通过NestScrolling
相关的接口了,如果要使用behavior
那么就会大大减少操作的成本,因为behavior
一般会处理依赖关系,如果一个view的操作以及确定,而其他view依赖于这个view的变化,那么只要实时监控view的变化,根据变动处理即可,这块通常会在ViewTreeObserver.OnPreDrawListener
中进行监听,这是绘制前的监听,方法走到这里一般测量和布局已经完成了
比如上面gif中的顶层下滑view和缩放的滑块都是通过behavior
依赖关系实现的
同时我们在LayoutInflate
的源码中可以知道
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
......
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
......
}
解析布局的时候,会把子view添加LayoutParmas
,而这个参数是通过generateLayoutParams
完成的,这里传入了attrs
,也就是定义view的键值对属性,比如layout_width="match_parent"
就是一个默认命名空间的属性对,那么我们就可以自定义属性在这里进行拓展,实际上google
也是这么做的
比如我们定义IBehavior
interface IBehavior<T : View> {
fun dependsOn(parent: View, selfView: T, denpendView: View): Boolean
fun onDependViewChanged(parent: View, selfView: T, dependView: View): Boolean
fun fliping(target: T, velocityY: Float): Boolean
fun canScrollDown(target: T): Boolean
fun tryScrollTo(target: T, delx: Int, delY: Int)
}
我在这里定义了常用的dependsOn
,以及变化的onDependViewChanged
方法 后面这几个是我这里后面要用到的拓展方法
然后定义一个attr
属性,里面指定字符串,其实就是具体实现类的全类名
<declare-styleable name="My_Nest_Parent">
<attr name="myBehavior" format="string|reference"/>
</declare-styleable>
然后在我们后面要定义的NestParent
内中添加代码
class MNestParent @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
intStyle: Int = 0
) : FrameLayout(context, attributeSet, intStyle), NestedScrollingParent2 {
inner class MLayoutParams : FrameLayout.LayoutParams {
constructor(width: Int, height: Int) : super(width, height)
constructor(c: Context, attrs: AttributeSet?) : super(c, attrs)
var behavior: IBehavior<View>? = null
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
val parentParams: MLayoutParams = MLayoutParams(context, attrs)
attrs?.apply {
val types = context.obtainStyledAttributes(attrs, R.styleable.My_Nest_Parent)
val behavior = types.getString(R.styleable.My_Nest_Parent_myBehavior)
parentParams.behavior = behavior?.run {
parseBehavior(this)
}
}
return parentParams
}
fun parseBehavior(clazzName: String): IBehavior<View>? {
try {
val constructor = Class.forName(clazzName).getDeclaredConstructor()
constructor?.apply {
isAccessible = true
return constructor.newInstance() as IBehavior<View>
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
代码也很简单,我自定义了一个MLayoutParams
,里面添加了一个IBehavior
的属性,而这个属性是通过attr
获取,然后把全类名反射为一个Behavior
的实例
那么我们在xml
就可以这么使用
<TextView
android:layout_width="match_parent"
android:background="#0af"
android:text="最上面的"
android:gravity="center"
android:textColor="#fff"
app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp3"
android:layout_height="30dp"/>
是不是有种熟悉的感觉,MBehaviorImp3
就是我定义的实现类了
class MBehaviorImp3() : IBehavior<TextView> {
override fun dependsOn(parent: View, selfView: TextView, denpendView: View): Boolean {
if (denpendView is MAppTopChild) {
selfView.layout(0,-selfView.measuredHeight,parent.measuredWidth,0)
return true
}
return false
}
override fun onDependViewChanged(parent: View,selfView: TextView,dependView: View): Boolean {
val scrollRange = (dependView as MAppLayoutInterface).getScrollRange()
val scrollY = parent.scrollY
val rate = scrollY.toFloat() / scrollRange
val translateY = selfView.measuredHeight * rate +parent.scrollY
selfView.translationY = translateY
return false
}
......
}
可以看出上面的滑块处理就是这么简单,这里有几个类后面会提到,大概有个流程概念就行
那么有依赖就得监听依赖的变化,上面提到的onPreDrawListener
就派上用场了,同样在MNestParent
中添加代码
private var preListenerAdded = false
private val dependRefList: MutableList<DependViewInfo> = ArrayList()
private var appLayout: MAppLayoutInterface? = null
private val preListener = ViewTreeObserver.OnPreDrawListener{
onDenpendViewStateChanged()
true //kotlin最后一行就是返回值
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
prepareChildren()
ensurePreListener()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for (i in 0 until childCount) {
val childView = getChildAt(i)
if (childView is MAppLayoutInterface) appLayout = childView
if (childView !is MAppLayout && appLayout != null) {
val childHeightSpec = ViewGroup.getChildMeasureSpec(
heightMeasureSpec,
appLayout!!.getPinedHeight(),
childView.layoutParams.height
)
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,0,childView.layoutParams.width)
childView.measure(childWidthSpec, childHeightSpec)
}
}
if (appLayout != null) {
totalHeight = appLayout!!.getScrollRange() + measuredHeight
} else {
totalHeight = measuredHeight
}
}
fun prepareChildren() {
dependRefList.clear()
for (i in 0 until childCount) {
val childView = getChildAt(i)
val params = childView.layoutParams as MLayoutParams
if (params.behavior == null) continue
for (j in 0 until childCount) {
if (i == j) continue
val dependView = getChildAt(j)
if (params.behavior!!.dependsOn(this, childView, dependView)) {
dependRefList.add(DependViewInfo(childView, dependView))
params.behavior!!.onDependViewChanged(this, childView, dependView)
break
}
}
}
}
fun ensurePreListener() {
if (preListenerAdded) return
viewTreeObserver.addOnPreDrawListener(preListener)
preListenerAdded = true
}
fun onDenpendViewStateChanged() {
if (dependRefList.size == 0) return
for (i in dependRefList.indices) {
val dependInfo = dependRefList[i]
val childParams = dependInfo.selfview.layoutParams as MLayoutParams
val behavior = childParams.behavior
behavior!!.onDependViewChanged(this, dependInfo.selfview, dependInfo.dependview)
}
}
这里我在onLayout
中完成view的绑定关系,这里的布局已经完成可以做依赖关系的处理,然后我在onMeasure
中处理的view的重新测量,因为有了吸顶,子view占据的高度需要减去这一块。
然后我定义了appLayout
实现了MAppLayoutInterface
的接口,这里是模仿coordinlayout
,在我定义的appLayout
是个LinearLayout
,其中会以里面的最后一个view
进行吸顶固定,这里的scrollRange
是该view的总高度减去吸顶的高度,也就是可以可以滑动多少距离
最后监听有变化,通知所有的有关联的view即可,使用也很简单
到这里Behavior
的基本思路也就完成了,印象中那么难其实原理很简单
3.NestedScrollingParent
其实NestedScrollingParent
这个其实没多大意义,因为ViewGroup
中本身就有同名的方法(API>21
),甚至可以在NestedScrollingChildHelper
方法中看到一些痕迹
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (Build.VERSION.SDK_INT >= 21) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
ViewParent
本身就有相关的方法,当你自定义一个ViewGroup
去实现NestedScrollingParent
接口,会发现不会提示未实现的方法
而NestedScrollingParent2
和NestedScrollingParent3
则是对NestedScrollingParent
的拓展,新增了一些接口,而这些就是我们必须实现的
虽说默认不需要实现,但为了实现功能,该重写的还是得重写
下面简单讲一下这里的机制了
NestedScrollingParent
和NestedScrollingChild
是一对"父子"关系,更像是一个上下级关系
NestedScrollingChild
会发出一系列请求,这些请求最终会通知到NestedScrollingParent
中,NestedScrollingParent
会判断是否对这些请求进行处理,比如消耗,处理完毕后再把剩下的返给NestedScrollingChild
简单的说就是NestedScrollingChild
做什么都要先去获得NestedScrollingParent
的批准,如果NestedScrollingParent
不关心该操作那就自己处理,否则上级
先处理完再把处理完的结果给下级
从方法名字上就可以窥见一些痕迹,NestedScrollingChild
很多方法都是dispatch
开头,表示分发。NestedScrollingParent
很多方法都是on
开头,表示接收。也就是起点都是NestedScrollingChild
比如startNestedScroll
方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
这里会通过onStartNestedScroll
通知上级,这个方法就是NestedScrollingParent
中的。如果Parent
想要处理,也就是返回true
,然后会调用setNestedScrollingParentForType
设置操作的类型和对应的view,这个方法很重要
private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
switch (type) {
case TYPE_TOUCH:
return mNestedScrollingParentTouch;
case TYPE_NON_TOUCH:
return mNestedScrollingParentNonTouch;
}
return null;
}
private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
mNestedScrollingParentNonTouch = p;
break;
}
}
后面会根据这个进行依据判断,如果为null
,那么就认为没有上级要处理,完全交给自己处理
比如dispatchNestedPreScroll
方法,没有就直接返回了
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
这里重点关注consumed
这个数组,这里会先把数组的数据全部置0,然后通过onNestedPreScroll
交给上级,上级如果想要处理,那么就给这个数组中的数据进行赋值,当值发生变化,也就是consumed[0] != 0 || consumed[1] != 0;
成立,那么下级再把上级消耗的减去,就是自己真实要滑动的距离了
比如子View手势判断滑动距离是10
,把这个通知给父View,父View接收后处理了8
,那么最终反馈给子View就剩下2
了,然后再去处理真实滑动即可。如果父View把这些全处理,那么子View就压根滑不动了,这很好理解。
有了这些理解,我们就可以处理NestedScrollingParent
了,在上面定义的MNestParent
中添加nest相关处理
//这个方法只是为了在setNestedScrollingParentForType把当前view的父view设置
// 后续可以通过getNestedScrollingParentForType获取到viewparent
// 后续的dispatchNestedPreScroll等都会通过这个进行判断,没有就返回false 也就是不处理
override fun onStartNestedScroll( child: View,target: View,nestedScrollAxes: Int,type: Int): Boolean {
return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}
override fun onNestedPreScroll(target: View,dx: Int,dy: Int,consumed: IntArray, type: Int) {
var curScrollY = scrollY
var afterScrollY = curScrollY + dy
//当前是吸顶状态或者原始状态,需要判断子View是否都已经滑动到原始状态(scrollY=0),才能下滑父View
if (curScrollY == 0 || curScrollY == appLayout!!.getScrollRange()) {
if (dy < 0) {
for (i in 0 until childCount) {
val paraView = getChildAt(i)
var paramas = paraView.layoutParams as MLayoutParams
val behavior = paramas.behavior
//如果当前view不能下滑,也是就有子view的scrollY没有到0,通过behavior滑动子View
if (behavior != null && !behavior.canScrollDown(paraView)) {
behavior.tryScrollTo(paraView, 0, dy)
return
}
}
}
}
//区间内 全部由父亲接收
if (afterScrollY > 0 && afterScrollY < appLayout!!.getScrollRange()) {
if (target.scrollY == 0) {
consumed[1] = dy //全部消耗,一点都不留给子view,只滑动父view
scrollTo(0, afterScrollY)
}
} else {
if (curScrollY == 0 || curScrollY == appLayout!!.getScrollRange()) return
//强制滑动到0
if (afterScrollY < 0) {
afterScrollY = 0
consumed[1] = dy + afterScrollY
scrollTo(0, 0)
//强制滑动到吸顶状态
} else if (afterScrollY > appLayout!!.getScrollRange()) {
afterScrollY = appLayout!!.getScrollRange()
consumed[1] = dy + (afterScrollY - appLayout!!.getScrollRange())
scrollTo(0, appLayout!!.getScrollRange())
}
}
}
//这里可能实现不同子view的惯性联动,也是子view->A 惯性滑动到头后 ,交给父View,父view再把速度传给子view->B ,实现B的惯性滑动,也就是上面gif中的,在AppLayout中的惯性滑动最终在吸顶后反馈给下面的view,继续惯性滑动
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
for (i in 0 until childCount) {
val target = getChildAt(i)
val params = target.layoutParams as MLayoutParams
params.behavior?.apply {
params!!.behavior!!.fliping(target, velocityY)
}
}
return false
}
这里涉及到一个Behavior
,我先贴出来
class MBehaviorImp() : IBehavior<MNestChild> {
override fun dependsOn(parent: View, selfView: MNestChild, denpendView: View): Boolean {
if (denpendView is MAppLayout) {
selfView.layout(0,denpendView.measuredHeight,parent.measuredWidth,
selfView.measuredHeight + denpendView.measuredHeight
)
return true
}
return false
}
override fun onDependViewChanged(parent: View,selfView: MNestChild,dependView: View): Boolean {
return false
}
override fun fliping(target: MNestChild, velocityY: Float): Boolean {
target.fling(velocityY.toInt())
return true
}
override fun canScrollDown(target: MNestChild): Boolean {
if (target.scrollY != 0) return false //如果不为0,那么就不能下滑
return true
}
override fun tryScrollTo(target: MNestChild, delx: Int, delY: Int) {
target.tryScrollTo(delx, delY)
}
}
这个是我下面要定义的MNestChild
的,AppLayout
会反馈到父view中,再通过Behavior
反馈给MNestChild
4.NestedScrollingChild
NestedScrollingChild
的处理一遍需要借助一个工具类NestedScrollingChildHelper
,这个也只是分发的工具类,逻辑并不复杂,通过这个通知上级即可
Child的处理稍微复杂一点,因为需要处理手势和惯性滑动,这里就要借助无敌的Scroller
了,处理丝滑的滑动就靠这个了,不理解Scroller
的可以去查一下资料,这个可以认为就是一个算法工具,给定时间或速度,它就能给你算出相应时间对应的值,通过computeScrollOffset
进行当前时间值的计算,根据这个值进行滑动处理即可
那么开始定义MNestChild
,我这里直接继承了LinearLayout
,注意嵌套滑动需要调setNestedScrollingEnabled
设置可用
class MNestChild @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
intStyle: Int = 0
) : LinearLayout(context, attributeSet, intStyle), NestedScrollingChild {
private val mChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
init {
mChildHelper.isNestedScrollingEnabled = true
}
private var totalHeight: Int = 0
private var velocityTracker = VelocityTracker.obtain()
private val maxVelocityY = ViewConfiguration.get(context).scaledMaximumFlingVelocity
private val minVelocityY = ViewConfiguration.get(context).scaledMinimumFlingVelocity
private val mScroller = Scroller(context)
private val consumed = intArrayOf(0, 0)
private var lastTouchedY = 0
private var lastFlingY = 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
totalHeight = 0
for (i in 0 until childCount) {
val childView = getChildAt(i)
totalHeight += childView.measuredHeight
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
velocityTracker?.recycle()
}
}
先定义一些基本变量,包括总高度,速度捕捉,上一次的按下位置等,然后再onMeasure
中算出总共的高度,后面算滑动边界要用,最后要记得回收velocityTracker
接着处理手势
override fun onTouchEvent(event: MotionEvent): Boolean {
val actionMask = event.actionMasked
when (actionMask) {
MotionEvent.ACTION_DOWN -> {
//如果上次计算未完成,强制终止
if (!mScroller.isFinished) {
mScroller.abortAnimation()
mScroller.forceFinished(true)
}
//这里的Type默认会有两种
//TYPE_TOUCH 手势拖动
//TYPE_NON_TOUCH 非手势,一般指惯性滑动
mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
mChildHelper.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
//其实就是设置setNestedScrollingParentForType,如果父亲有处理的意愿就设置,否则父亲就不处理
mChildHelper.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL,
ViewCompat.TYPE_TOUCH
)
//这里按照getRawY进行处理,因为父View滑动可能会有些影响
lastTouchedY = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
//追踪这次的速度操作
velocityTracker.addMovement(event)
val curEventY = event.rawY.toInt()
var delY = (lastTouchedY - curEventY).toInt()
//分发,返回true表示距离被上级消耗了一部分,减去消耗的计算剩下的
if (mChildHelper.dispatchNestedPreScroll(0, delY, consumed, null)) {
delY -= consumed[1]
}
//如果还有剩下的
if (delY != 0) {
val curScrollY = scrollY
tryScrollTo(0, delY)
val resScrollY = scrollY
//子view滑动可能到边界,那么这些距离可能还会剩余一点,然后把这个再通知给上级
val unconsumeY = delY - (resScrollY - curScrollY)
//其实这个方法我上级并没有处理,实际传递方式也差不多是这样
mChildHelper.dispatchNestedScroll(0,0,
delY,unconsumeY,
null,ViewCompat.TYPE_TOUCH)
}
consumed[1] = 0
lastTouchedY = curEventY
}
MotionEvent.ACTION_UP -> {
//计算当前惯性的速度
velocityTracker.computeCurrentVelocity(1000)
var velocityY = velocityTracker.yVelocity
if (Math.abs(velocityY) > minVelocityY) {
if (Math.abs(velocityY) > maxVelocityY) {
//sign方法获取当前值的符号 比如 -5就是-1 6就是1
velocityY = velocityY.sign * maxVelocityY
}
//通知父view惯性滑动发生
if (!mChildHelper.dispatchNestedPreFling(0f, -velocityY)) {
//fling(velocityY.toInt()) //速度这个一般和我们滑动的偏移是相反的
//因为有behvaior处理,这里就不需要调用fliping了
}
}
//通知终止,终止的类型是TYPE_TOUCH
mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
return true
}
//滑动到指定位置
fun tryScrollTo(delX: Int, dexY: Int) {
if (dexY == 0) return
var currentScrollY = scrollY
var targetScrollY = currentScrollY + dexY
if (targetScrollY < 0) targetScrollY = 0
if (targetScrollY > totalHeight - measuredHeight) targetScrollY =
totalHeight - measuredHeight
scrollTo(0, targetScrollY)
}
下面就是Scroller
相关的操作
fun fling(velocityY: Int) {
if (childCount > 0) {
//开始滑动,类型为TYPE_NON_TOUCH
mChildHelper.startNestedScroll(
ViewCompat.SCROLL_AXIS_VERTICAL,
ViewCompat.TYPE_NON_TOUCH
)
lastFlingY = scrollY
//fling一般都会指定无界,传递速度会帮我们自动计算
mScroller.fling(
scrollX, scrollY,
0, -velocityY,
0, 0, Int.MIN_VALUE, Int.MAX_VALUE
)
invalidate()
}
}
override fun computeScroll() {
//这里通过是否结束进行判断
//如果直接用computeScrollOffset判断,如果父view把手势全部消耗,那么这里会很快返回false,但实际计算并没有停止
if (mScroller.isFinished) return
mScroller.computeScrollOffset()
var curY = mScroller.currY
var delY = lastFlingY - curY
//继续通知父亲滑动操作
if (mChildHelper.dispatchNestedPreScroll(0,delY,consumed,null,
ViewCompat.TYPE_NON_TOUCH)) {
delY -= consumed[1]
}
tryScrollTo(0, delY)
invalidate()
lastFlingY = curY
}
到这里应该基本使用思路就应该都了解了
最后的AppLayout
其实和MNestChild
实现差不多,就直接贴出代码了
class MAppLayout @JvmOverloads constructor(
context: Context,
attributes: AttributeSet? = null,
intStyle: Int = 0
) : LinearLayout(context, attributes, intStyle), MAppLayoutInterface, NestedScrollingChild {
private lateinit var lastChildPined: View
private var pinedHeight = 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//把最后一个当成固定的头部
lastChildPined = getChildAt(childCount - 1)
pinedHeight = lastChildPined.measuredHeight
}
override fun getPinedHeight(): Int {
return pinedHeight
}
override fun getPinedView(): View {
return lastChildPined
}
override fun getContentHeight(): Int {
return measuredHeight
}
override fun getScrollRange(): Int {
return measuredHeight - pinedHeight
}
private val mChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
init {
mChildHelper.isNestedScrollingEnabled = true
}
private var velocityTracker = VelocityTracker.obtain()
private val maxVelocityY = ViewConfiguration.get(context).scaledMaximumFlingVelocity
private val minVelocityY = ViewConfiguration.get(context).scaledMinimumFlingVelocity
private val mScroller = Scroller(context)
private val consumed = intArrayOf(0, 0)
private var lastTouchedY = 0
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val actionMask = event.actionMasked
when (actionMask) {
MotionEvent.ACTION_DOWN -> {
if (!mScroller.isFinished) {
mScroller.abortAnimation()
mScroller.forceFinished(true)
}
mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
mChildHelper.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
mChildHelper.startNestedScroll(
ViewCompat.SCROLL_AXIS_VERTICAL,
ViewCompat.TYPE_TOUCH
)
lastTouchedY = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
velocityTracker.addMovement(event)
val curEventY = event.rawY.toInt()
var delY = (lastTouchedY - curEventY).toInt()
if (mChildHelper.dispatchNestedPreScroll(0, delY, consumed, null)) {
delY -= consumed[1]
}
lastTouchedY = curEventY
}
MotionEvent.ACTION_UP -> {
velocityTracker.computeCurrentVelocity(1000)
var velocityY = velocityTracker.yVelocity
if (Math.abs(velocityY) > minVelocityY) {
if (Math.abs(velocityY) > maxVelocityY) {
velocityY = velocityY.sign * maxVelocityY
}
if (!mChildHelper.dispatchNestedPreFling(0f, -velocityY)) {
invalidate()
}
}
mChildHelper.stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
return super.dispatchTouchEvent(event)
}
}
interface MAppLayoutInterface {
fun getPinedHeight(): Int
fun getPinedView(): View
fun getContentHeight():Int
fun getScrollRange():Int
}
一直以为NestScrolling
很复杂,也一直很抗拒去处理,其实扒开代码,自己实现也不算很复杂,就是如果自定义,很多细节问题需要考虑周全,这里只是一个简单的学习Demo,不可避免会有很多问题,多多包涵
最后是布局文件了,也很简单
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.view.zero.learn.views.nestscroll.MNestParent
android:id="@+id/mNestParent"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.view.zero.learn.views.nestscroll.MAppLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:id="@+id/header_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="头部内容1"
android:textSize="30sp"
android:onClick="onClickViews"
/>
<TextView
android:id="@+id/header_2"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="头部内容2"
android:textSize="30sp"
android:onClick="onClickViews"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="130dp"
android:layout_marginTop="-30dp"
>
<TextView
android:id="@+id/pin_1"
android:layout_marginTop="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="固定内容"
android:background="#0aa"
android:textColor="#fff"
android:onClick="onClickViews"
android:textSize="30sp"
/>
</LinearLayout>
</com.view.zero.learn.views.nestscroll.MAppLayout>
<com.view.zero.learn.views.nestscroll.MNestChild
android:id="@+id/mNestChild"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp"
android:orientation="vertical">
</com.view.zero.learn.views.nestscroll.MNestChild>
<TextView
android:id="@+id/float_1"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#06a"
android:text="Dep"
android:onClick="onClickViews"
android:textSize="20sp"
android:gravity="center"
android:textColor="#fff"
app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp2"
/>
<TextView
android:layout_width="match_parent"
android:background="#0af"
android:text="最上面的"
android:gravity="center"
android:textColor="#fff"
app:myBehavior="com.view.zero.learn.views.nestscroll.MBehaviorImp3"
android:layout_height="30dp"/>
</com.view.zero.learn.views.nestscroll.MNestParent>
</LinearLayout>
5.recyclerView嵌套
因为Recyclerview
本身就实现了NestScrolling
相关的操作接口,我这里直接把我的MNestChild
替换成RecyclerView
,如果可以正常进行嵌套操作,那么就说明上述的理解和流程是正确的,这里需要重新定义一下Behavior
,我这里新增一个开始滑动的监听
class MRecyclerViewBehaviorImp() : IBehavior<RecyclerView> {
override fun dependsOn(parent: View, selfView: RecyclerView, denpendView: View): Boolean {
if (denpendView is MAppLayout) {
selfView.layout(
0,
denpendView.measuredHeight,
parent.measuredWidth,
selfView.measuredHeight + denpendView.measuredHeight
)
return true
}
return false
}
override fun onDependViewChanged(
parent: View,
selfView: RecyclerView,
dependView: View
): Boolean {
return false
}
private var fliping = false
override fun fliping(target: RecyclerView, velocityY: Float): Boolean {
if (fliping) return true
fliping = true
target.fling(0, velocityY.toInt())
return true
}
override fun canScrollDown(target: RecyclerView): Boolean {
val can = target.canScrollVertically(-1)
return !can
}
override fun tryScrollTo(target: RecyclerView, delx: Int, delY: Int) {
target.scrollBy(delx, delY)
}
override fun onStartScroll() {
fliping = false
}
}
RecyclerView
可以通过canScrollVertically
判断是否已经滑动到了顶端
把布局中的Behavior
和子View进行替换
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:myBehavior="com.view.zero.learn.views.nestscroll.MRecyclerViewBehaviorImp"
/>
下面是实际的展示效果,实际也是可以正常执行的,那么以后我们自定义嵌套NestScrolling
就可以根据这个作为大概的流程参考,其实也蛮好玩的
不仅是RecyclerView
,ViewPager
也是可以弄的,只要修改下Behavior
就行,讲真,到这里就可以看出Behavior
这个设计是真的叼