这几天调试系统发现当快速滑动控件列表时会出现滑动回弹的情况,而且搜索不到结果,那只能进行跟踪分析了,今天找到原因并进行下记录
原因是Linux中的触摸屏驱动问题,多点触控协议上报使用TYPE B设备方式上报即可,最终证明跟系统是无关的,是驱动问题,但我将查找过程
记录下来,算是了解Android系统实现滑动的一个过程
首先需要先找一界面进行定位跟踪,我选中了近期任务列表这个界面
/frameworks/base/packages/SystemUI/src/com/android/systemui/recent/RecentsPanelView.java
该文件实现了近期任务列表,其中接口RecentsScrollView为主要实现类
顺藤摸瓜,发现
/frameworks/base/packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java
RecentsHorizontalScrollView类对RecentsScrollView接口进行了具体实现
在互动任务列表时会先后触发onInterceptTouchEvent跟onTouchEvent两个事件,
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
return mSwipeHelper.onInterceptTouchEvent(ev) ||
super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return mSwipeHelper.onTouchEvent(ev) ||
super.onTouchEvent(ev);
}
继续跟踪,这两个事件都调用了
/frameworks/base/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
中的onInterceptTouchEvent跟onTouchEvent事件,
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
Log.w(TAG, "onInterceptTouchEvent action is " + action);
switch (action) {
case MotionEvent.ACTION_DOWN:
mDragging = false;
mLongPressSent = false;
mCurrView = mCallback.getChildAtPosition(ev);
mVelocityTracker.clear();
if (mCurrView != null) {
mCurrAnimView = mCallback.getChildContentView(mCurrView);
mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
mVelocityTracker.addMovement(ev);
mInitialTouchPos = getPos(ev);
if (mLongPressListener != null) {
if (mWatchLongPress == null) {
mWatchLongPress = new Runnable() {
@Override
public void run() {
if (mCurrView != null && !mLongPressSent) {
mLongPressSent = true;
mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
mLongPressListener.onLongClick(mCurrView);
}
}
};
}
mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
}
}
break;
case MotionEvent.ACTION_MOVE:
if (mCurrView != null && !mLongPressSent) {
mVelocityTracker.addMovement(ev);
float pos = getPos(ev);
float delta = pos - mInitialTouchPos;
if (Math.abs(delta) > mPagingTouchSlop) {
mCallback.onBeginDrag(mCurrView);
mDragging = true;
mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
removeLongPressCallback();
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mDragging = false;
mCurrView = null;
mCurrAnimView = null;
mLongPressSent = false;
removeLongPressCallback();
break;
}
return mDragging;
}
public boolean onTouchEvent(MotionEvent ev) {
if (mLongPressSent) {
Log.w(TAG, "onTouchEvent mLongPressSent");
return true;
}
if (!mDragging) {
// We are not doing anything, make sure the long press callback
// is not still ticking like a bomb waiting to go off.
removeLongPressCallback();
Log.w(TAG, "onTouchEvent mDragging");
return false;
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
Log.w(TAG, "action is " + action);
switch (action) {
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_MOVE:
if (mCurrView != null) {
float delta = getPos(ev) - mInitialTouchPos;
// don't let items that can't be dismissed be dragged more than
// maxScrollDistance
if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
float size = getSize(mCurrAnimView);
float maxScrollDistance = 0.15f * size;
if (Math.abs(delta) >= size) {
delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
} else {
delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
}
}
setTranslation(mCurrAnimView, delta);
updateAlphaFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mCurrView != null) {
float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
float velocity = getVelocity(mVelocityTracker);
float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
// Decide whether to dismiss the current view
boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
(Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
(velocity > 0) == (getTranslation(mCurrAnimView) > 0);
boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
(childSwipedFastEnough || childSwipedFarEnough);
if (dismissChild) {
// flingadingy
dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
} else {
// snappity
mCallback.onDragCancelled(mCurrView);
snapChild(mCurrView, velocity);
}
}
break;
}
return true;
}
本以为到这里就找到原因了,但执行后变现在进入onTouchEvent事件之前mDragging变量基本都是false状态,所以下面的触控switch情况基本不会触发,
所以返回RecentsHorizontalScrollView类中继续查看两个触控事件发现返回false后会调用父类的触控事件,想想也应该,要不每个控件都得重新实现一遍
滑动事件那也太不人道了。寻找实现触控事件的父类,
/frameworks/base/core/java/android/widget/HorizontalScrollView.java
在这里找到了触控事件,并运行发现执行了相应触控手势
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionX is set to the x value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int x = (int) ev.getX(pointerIndex);
final int xDiff = (int) Math.abs(x - mLastMotionX);
if (xDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionX = x;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int x = (int) ev.getX();
if (!inChild((int) x, (int) ev.getY())) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionX = x;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged.
*/
mIsBeingDragged = !mScroller.isFinished();
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
postInvalidateOnAnimation();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionX = (int) ev.getX(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
Log.w(TAG, "onTouchEvent action is " + action);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionX = (int) ev.getX();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int x = (int) ev.getX(activePointerIndex);
int deltaX = mLastMotionX - x;
if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionX = x;
final int oldX = mScrollX;
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
mOverscrollDistance, 0, true)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
if (canOverscroll) {
final int pulledToX = oldX + deltaX;
if (pulledToX < 0) {
mEdgeGlowLeft.onPull((float) deltaX / getWidth());
if (!mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.onRelease();
}
} else if (pulledToX > range) {
mEdgeGlowRight.onPull((float) deltaX / getWidth());
if (!mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.onRelease();
}
}
if (mEdgeGlowLeft != null
&& (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
case MotionEvent.ACTION_UP:
Log.w(TAG, "onTouchEvent Up " + mIsBeingDragged);
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
Log.w(TAG, String.format("mMaximumVelocity=%d, initialVelocity=%d, mMinimumVelocity=%d, %d",
mMaximumVelocity, initialVelocity, mMinimumVelocity, getChildCount()));
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(mScrollX, mScrollY, 0,
getScrollRange(), 0, 0)) {
postInvalidateOnAnimation();
}
}
}
mActivePointerId = INVALID_POINTER;
mIsBeingDragged = false;
recycleVelocityTracker();
if (mEdgeGlowLeft != null) {
mEdgeGlowLeft.onRelease();
mEdgeGlowRight.onRelease();
}
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
mIsBeingDragged = false;
recycleVelocityTracker();
if (mEdgeGlowLeft != null) {
mEdgeGlowLeft.onRelease();
mEdgeGlowRight.onRelease();
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return true;
}
跟踪到此,发现不管快速还是慢速滑动在ACTION_UP动作后都执行了fling方法,这个方法也就是实现滑动的方法了,那为什么快速滑动会回弹回来
造成滑动无效呢?继续跟踪fling方法
public void fling(int velocityX) {
if (getChildCount() > 0) {
int width = getWidth() - mPaddingRight - mPaddingLeft;
int right = getChildAt(0).getWidth();
mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Math.max(0, right - width), 0, 0, width/2, 0);
final boolean movingRight = velocityX > 0;
View currentFocused = findFocus();
View newFocused = findFocusableViewInMyBounds(movingRight,
mScroller.getFinalX(), currentFocused);
if (newFocused == null) {
newFocused = this;
}
if (newFocused != currentFocused) {
Log.w(TAG, "newFocused " + movingRight);
newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
}
postInvalidateOnAnimation();
}
}
这里调用了OverScroller中的fling方法
/frameworks/base/core/java/android/widget/OverScroller.java
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
Log.w(TAG, String.format("oldVelocityX=%d, velocityX=%d", oldVelocityX, velocityX));
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX);
mScrollerY.fling(startY, velocityY, minY, maxY, overY);
}
该方法又调用了SplineOverScroller中的fling方法,SplineOverScroller类也在该文件中定义
void fling(int start, int velocity, int min, int max, int over) {
mOver = over;
mFinished = false;
mCurrVelocity = mVelocity = velocity;
mDuration = mSplineDuration = 0;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mCurrentPosition = mStart = start;
Log.w(TAG, "mStartTime is " + mStartTime);
Log.w(TAG, String.format("start=%d, velocity=%d, min=%d, max=%d, over=%d",
start, velocity, min, max, over));
if (start > max || start < min) {
startAfterEdge(start, min, max, velocity);
return;
}
mState = SPLINE;
double totalDistance = 0.0;
if (velocity != 0) {
mDuration = mSplineDuration = getSplineFlingDuration(velocity);
totalDistance = getSplineFlingDistance(velocity);
Log.w(TAG, String.format("mDuration=%d, totalDistance=%f", mDuration, totalDistance));
}
mSplineDistance = (int) (totalDistance * Math.signum(velocity));
mFinal = start + mSplineDistance;
Log.w(TAG, String.format("mSplineDistance=%d, mFinal=%d", mSplineDistance, mFinal));
// Clamp to a valid final position
if (mFinal < min) {
adjustDuration(mStart, mFinal, min);
mFinal = min;
}
if (mFinal > max) {
adjustDuration(mStart, mFinal, max);
mFinal = max;
}
Log.w(TAG, String.format("mFinal=%d, mDuration=%d", mFinal, mDuration));
}
追踪到这里基本到头了,但我从打印出来的日志中没有发现什么异常,除了快速滑动时速度值比较大,那是Android系统速度计算有问题?应该是的,
因为我每次快速滑动的时候速度值都比较高,那我将速度限制一下试试
/frameworks/base/core/java/android/view/ViewConfiguration.java
该文件对View中的一些参数进行了配置
其中MAXIMUM_FLING_VELOCITY为滑动的速度上限,我改为了1000,够小了吧,结果发现问题依旧。。。头疼了,既然是速度问题,那么看看
系统是如何计算速度的吧。。。
回到HorizontalScrollView类中的onTouchEvent事件里,进入ACTION_UP项
调用了velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);方法
1000是衡量单位,mMaximumVelocity是速度上限,那么就跟踪这个方法吧
/frameworks/base/core/java/android/view/VelocityTracker.java
在这个类中实现了computeCurrentVelocity方法
public void computeCurrentVelocity(int units, float maxVelocity) {
nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
}
这一看,调用JNI中方法了,好嘛。。。终于快接近真相了,找JNI方法
/frameworks/base/core/jni/android_view_VelocityTracker.cpp
void VelocityTrackerState::computeCurrentVelocity(int32_t units, float maxVelocity) {
BitSet32 idBits(mVelocityTracker.getCurrentPointerIdBits());
mCalculatedIdBits = idBits;
for (uint32_t index = 0; !idBits.isEmpty(); index++) {
uint32_t id = idBits.clearFirstMarkedBit();
float vx, vy;
mVelocityTracker.getVelocity(id, &vx, &vy);
vx = vx * units / 1000;
vy = vy * units / 1000;
if (vx > maxVelocity) {
vx = maxVelocity;
} else if (vx < -maxVelocity) {
vx = -maxVelocity;
}
if (vy > maxVelocity) {
vy = maxVelocity;
} else if (vy < -maxVelocity) {
vy = -maxVelocity;
}
Velocity& velocity = mCalculatedVelocity[index];
velocity.vx = vx;
velocity.vy = vy;
}
}
这里也只是获取速度值,没发现如何实现的,继续跟踪mVelocityTracker.getVelocity(id, &vx, &vy);。。。
/frameworks/native/libs/input/VelocityTracker.cpp
bool VelocityTracker::getVelocity(uint32_t id, float* outVx, float* outVy) const {
Estimator estimator;
if (getEstimator(id, &estimator) && estimator.degree >= 1) {
*outVx = estimator.xCoeff[1];
*outVy = estimator.yCoeff[1];
return true;
}
*outVx = 0;
*outVy = 0;
return false;
}
到这里基本跟踪不下去了,但在搜索该类资料时我搜索到了一个说明
该类定义了一个
const char* VelocityTracker::DEFAULT_STRATEGY = "lsq2";
触屏事件处理策略,并在
VelocityTrackerStrategy* VelocityTracker::createStrategy(const char* strategy) {
if (!strcmp("lsq1", strategy)) {
// 1st order least squares. Quality: POOR.
// Frequently underfits the touch data especially when the finger accelerates
// or changes direction. Often underestimates velocity. The direction
// is overly influenced by historical touch points.
return new LeastSquaresVelocityTrackerStrategy(1);
}
if (!strcmp("lsq2", strategy)) {
// 2nd order least squares. Quality: VERY GOOD.
// Pretty much ideal, but can be confused by certain kinds of touch data,
// particularly if the panel has a tendency to generate delayed,
// duplicate or jittery touch coordinates when the finger is released.
return new LeastSquaresVelocityTrackerStrategy(2);
}
if (!strcmp("lsq3", strategy)) {
// 3rd order least squares. Quality: UNUSABLE.
// Frequently overfits the touch data yielding wildly divergent estimates
// of the velocity when the finger is released.
return new LeastSquaresVelocityTrackerStrategy(3);
}
if (!strcmp("wlsq2-delta", strategy)) {
// 2nd order weighted least squares, delta weighting. Quality: EXPERIMENTAL
return new LeastSquaresVelocityTrackerStrategy(2,
LeastSquaresVelocityTrackerStrategy::WEIGHTING_DELTA);
}
if (!strcmp("wlsq2-central", strategy)) {
// 2nd order weighted least squares, central weighting. Quality: EXPERIMENTAL
return new LeastSquaresVelocityTrackerStrategy(2,
LeastSquaresVelocityTrackerStrategy::WEIGHTING_CENTRAL);
}
if (!strcmp("wlsq2-recent", strategy)) {
// 2nd order weighted least squares, recent weighting. Quality: EXPERIMENTAL
return new LeastSquaresVelocityTrackerStrategy(2,
LeastSquaresVelocityTrackerStrategy::WEIGHTING_RECENT);
}
if (!strcmp("int1", strategy)) {
// 1st order integrating filter. Quality: GOOD.
// Not as good as 'lsq2' because it cannot estimate acceleration but it is
// more tolerant of errors. Like 'lsq1', this strategy tends to underestimate
// the velocity of a fling but this strategy tends to respond to changes in
// direction more quickly and accurately.
return new IntegratingVelocityTrackerStrategy(1);
}
if (!strcmp("int2", strategy)) {
// 2nd order integrating filter. Quality: EXPERIMENTAL.
// For comparison purposes only. Unlike 'int1' this strategy can compensate
// for acceleration but it typically overestimates the effect.
return new IntegratingVelocityTrackerStrategy(2);
}
if (!strcmp("legacy", strategy)) {
// Legacy velocity tracker algorithm. Quality: POOR.
// For comparison purposes only. This algorithm is strongly influenced by
// old data points, consistently underestimates velocity and takes a very long
// time to adjust to changes in direction.
return new LegacyVelocityTrackerStrategy();
}
return NULL;
}
中进行了分类。
可通过property "debug.velocitytracker.strategy" 修改。如修改为lsq1 。这个需要和kernel中的TP驱动一起调试,
lsq2是最优效果,表示上报的速度是一个抛物线;lsq1上报的速度是一个加速度直线。这个需要和kernel上报一致。才能触动流畅。
看到这里恍然大悟,应该是我的触屏驱动有问题,然后修改触摸屏驱动直到今天搞定该问题。。。
总的来说还是学的太过肤浅,想靠小聪明整好Android系统有些可笑了。。。