8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

嵌套滑动一直是 Android 中比较棘手的问题,根本原因是 Android 的事件分发机制导致的。不过这个问题终于在 API 21之后有了官方的解决方法,就是嵌套滑动机制。

基本原理

嵌套滑动的基本原理是在子控件接收到滑动一段距离的请求时,先询问父控件是否要滑动,如果滑动了父控件就通知子控件它消耗了一部分滑动距离,子控件就只处理剩下的滑动距离,然后子控件滑动完毕后再把剩余的滑动距离传给父控件。

具体实现

API 21之后嵌套滑动的相关逻辑作为普通方法直接写进了最新的(API 21之后)View和ViewGroup类。

向前兼容官方在android.support.v4兼容包中提供了两个接口NestedScrollingChild和NestedScrollingParent。两个接口分别定义上面提到的View和ViewParent新增的普通方法。

辅助类除了接口兼容包还提供了NestedScrollingChildHelper和NestedScrollingParentHelper两个辅助类,这两个辅助类实际上就是对应View和ViewParent中新增的普通方法。

相关方法

NestedScrollingChild内控件是嵌套滑动的发起者。startNestedScroll:起始方法,主要作用是找到接收滑动距离信息的外控件。

dispatchNestedPreScroll:在内控件处理滑动前把滑动信息分发给外控件。

dispatchNestedScroll:在内控件处理完滑动后把剩下的滑动距离信息分发给外控件。

stopNestedScroll:结束方法,主要作用就是清空嵌套滑动的相关状态。

setNestedScrollingEnabled和isNestedScrollingEnabled:一对 get&set 方法,用来判断控件是否支持嵌套滑动。

dispatchNestedPreFling和dispatchNestedFling:跟 Scroll 的对应方法作用类似,不过分发的不是滑动信息而是 Fling 信息。

NestedScrollingParent外控件通过onNestedPreScroll和onNestedScroll来接收内控件响应滑动前后的滑动距离信息。onStartNestedScroll:对应startNestedScroll,内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息。

onNestedScrollAccepted:当外控件确定接收滑动信息后该方法被回调,可以让外控件针对嵌套滑动做一些前期工作。

onNestedPreScroll:关键方法,接收内控件处理滑动前的滑动距离信息,在这里外控件可以优先响应滑动操作,消耗部分或者全部滑动距离。

onNestedScroll:关键方法,接收内控件处理完滑动后的滑动距离信息,在这里外控件可以选择是否处理剩余的滑动距离。

onStopNestedScroll:对应stopNestedScroll,用来做一些收尾工作。

getNestedScrollAxes:返回嵌套滑动的方向,区分横向滑动和竖向滑动,作用不大。

onNestedPreFling和onNestedFling:同上略。

从 NestedScrollView 看嵌套机制

接下来通过分析相对简单的支持嵌套滑动的容器NestedScrollView,来了解下怎样主动调起嵌套滑动的方法,以及嵌套滑动的具体逻辑。NestedScrollView简单地说就是支持嵌套滑动的ScrollView,内部逻辑简单,而且它既可以是内控件,也可以是外控件,所以选择分析它来了解嵌套滑动机制。

嵌套滑动是从startNestedScroll方法开始的,从源码中我们可以发现在两个地方调用了这个方法:onInterceptTouchEvent中ACTION_DOWN的情况

onTouchEvent中ACTION_DOWN的情况

因为ACTION_DOWN是滑动操作的开始事件,所以当接收到这个事件的时候尝试找对应的外控件。只有找到了外控件才有后续的嵌套滑动的逻辑发生。

接着我们看startNestedScroll是如何找对应的外控件的,因为NestedScrollView#startNestedScroll调用了辅助方法的startNestedScroll,所以下面直接看View#startNestedScroll。

25boolean (int axes){
// ...
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
// 关键代码
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
// ...
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}

遍历父控件,调用父控件的onStartNestedScroll,返回 true 表示找到了对应的外控件,找到外控件后马上调用onNestedScrollAccepted。所以可以知道:外控件不一定是内控件的直接父控件,但一定是最近的符合条件的外控件。

关于onStartNestedScroll的方法说明,返回 true 表示接收内控件的滑动信息。对于NestedScrollView#onStartNestedScroll内部逻辑很简单,只要是竖直滑动方向就返回 true。所以可以知道:NestedScrollView不支持横向嵌套滑动。

接着看NestedScrollView#onNestedScrollAccepted。

4void onNestedScrollAccepted(View child, View target, int nestedScrollAxes){
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}

辅助类的方法很简单,就是记录当前的滑动方向,在这里NestedScrollView又调用startNestedScroll来找它自己的外控件,这是为了连续嵌套NestedScrollView。

找到了外控件后ACTION_DOWN事件就没嵌套滑动的事了,要滑动肯定会在onTouchEvent中处理ACTION_MOVE事件。

27// NestedScrollView#onTouchEvent
case MotionEvent.ACTION_MOVE:
// ...
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
int deltaY = mLastMotionY - y;
// 让外控件先处理滑动距离
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];// 消耗滑动距离
// ...
}
// ...
if (mIsBeingDragged) {
// ...
// 内控件处理滑动距离
if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
0, true) && !hasNestedScrollingParent()) {
// ...
}
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
// ...
}
// ...
}
break;

先计算出本次滑动距离deltaY,得到滑动距离deltaY后,先把它传给dispatchNestedPreScroll,然后在结果返回 true 的时候, delta会减去mScrollConsumed[1]。

10// View.java
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow){
// ... 忽略状态判断
consumed[0] = 0;
consumed[1] = 0;
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
return consumed[0] != 0 || consumed[1] != 0;
// 其他情况返回false
}

dispatchNestedPreScroll的工作就是把滑动距离在内控件处理前分发给外控件,所以这里的关键代码也很简单,就是直接把相关的参数传给外控件的onNestedPreScroll,然后只要外控件消耗了滑动距离(不论横向还是竖向),就会返回 true。外控件如果想在内控件之前消耗滑动距离仅需要在onNestedPreScroll把消耗的值放到数组中返回给内控件。

onNestedPreScroll是决定外控件的嵌套滑动逻辑的关键方法,在不同的控件中应该是根据需要有不同的实现的,而在NestedScrollView中就是直接询问它自己的外控件是否消耗滑动距离。在我们自己修改嵌套滑动逻辑的时候需要注意滑动距离的正负号和内控件处理consumed数组的方式。

现在外控件已经比内控件先处理了滑动距离了,如果外控件没有完全消耗掉所有滑动距离,这时该内控件处理剩下的滑动距离了。 在NestedScrollView中通过NestedScrollView#overScrollByCompat来进行滑动,并且滑动结束后通过比对滑动前后的scrollY值得到了内控件消耗的滑动距离,然后得到剩下的滑动距离,最后传给dispatchNestedScroll。

dispatchNestedScroll的逻辑跟dispatchNestedPreScroll几乎一样,区别是它调用了外控件的onNestedScroll。因为到这里已经是处理滑动距离最后的机会了,所以onNestedScroll不会再影响内控件的处理逻辑,ACTION_MOVE事件就分析完毕了。

最后就是stopNestedScroll了,调用这个方法基本是新的滑动操作开始前,或者滑动操作结束/取消,代码逻辑就是进行一些变量的重置工作和调用onStopNestedScroll,而onStopNestedScroll也类似。

NestedScrollView的嵌套滑动逻辑基本就是这样,它代表了嵌套滑动的”约定”处理方式,虽然不同的控件实际的实现会有不同,不过应该遵循基本方法的调用顺序,确保参数的含义和参数的处理方式。