ViewPager的两个问题


ViewPager滑动抽搐bug

当只有一个item,并且widthFactor<1手指向左会出现滑动抽搐

或者所有的offset加起来也<1时也会出现抽搐

问题原因

 经过一下午代码追踪找到问题位置

androidx.viewpager.widget.ViewPager#calculatePageOffsets

1 int pos = curItem.position - 1;
2 mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
3 mLastOffset = curItem.position == N - 1
4         ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;

 此时mLastOffset 就会为负数这个有什么用呢继续看下边

当我们手指滑动时最后会调用到ViewPager#performDrag其中就用到这个来计算滚动距离

private boolean performDrag(float x) {
    boolean needsInvalidate = false;

    final float deltaX = mLastMotionX - x;
    mLastMotionX = x;

    float oldScrollX = getScrollX();
    float scrollX = oldScrollX + deltaX;
    final int width = getClientWidth();

    float leftBound = width * mFirstOffset;
    float rightBound = width * mLastOffset;
    boolean leftAbsolute = true;
    boolean rightAbsolute = true;

    final ItemInfo firstItem = mItems.get(0);
    final ItemInfo lastItem = mItems.get(mItems.size() - 1);

// 此处不会执行,不会赋值
    if (firstItem.position != 0) {
        leftAbsolute = false;
        leftBound = firstItem.offset * width;
    }
    if (lastItem.position != mAdapter.getCount() - 1) {
        rightAbsolute = false;
        rightBound = lastItem.offset * width;
    }

    if (scrollX < leftBound) {
        if (leftAbsolute) {
            float over = leftBound - scrollX;
            mLeftEdge.onPull(Math.abs(over) / width);
            needsInvalidate = true;
        }
        scrollX = leftBound;
    } else if (scrollX > rightBound) {
        if (rightAbsolute) {
            float over = scrollX - rightBound;
            mRightEdge.onPull(Math.abs(over) / width);
            needsInvalidate = true;
        }
        scrollX = rightBound;
    }
    // Don't lose the rounded component
    mLastMotionX += scrollX - (int) scrollX;
    scrollTo((int) scrollX, getScrollY());
    pageScrolled((int) scrollX);

    return needsInvalidate;
}

 因为mLastOffset 为负数所以rightBound为负数那么就会让scrollX为负数

那么第一次虽然手指向左但最终却向相反方向

第二次计算后scrollX0因为上次负数此时会和手指方向一致

这样反反复复就出现了滑动抽搐

解决方法

 在androidx.viewpager.widget.ViewPager#calculatePageOffsets中添加一个校验即可

private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        final int N = mAdapter.getCount();
        final int width = getClientWidth();
 
        // Base all offsets off of curItem.
        final int itemCount = mItems.size();
        float offset = curItem.offset;
        int pos = curItem.position - 1;
        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
//        mLastOffset = curItem.position == N - 1 ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
        if (curItem.position == N - 1) {
            if (useLastOffset) {
                mLastOffset = curItem.offset + curItem.widthFactor - 1;
            } else {
                mLastOffset = curItem.offset;
            }
        } else {
            mLastOffset = Float.MAX_VALUE;
        }


        // 计算前面页面的偏移量(根据当前页面计算)
        // Previous pages
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
            if (ii.position == 0) mFirstOffset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;

        // 计算后面页面的偏移量(根据当前页面计算)
        // Next pages
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == N - 1) {
//                mLastOffset = offset + ii.widthFactor - 1;
                if (useLastOffset) {
                    mLastOffset = offset + ii.widthFactor - 1;
                } else {
                    mLastOffset = offset;
                }
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }

ViewPager fakeDrag 精度损失

如果做动画来fakeDrag时损失的精度累加会很大,导致最终滑动不精确。

下边分析是用的VerticalViewPager

public void fakeDragBy(float yOffset) {
    if (!mFakeDragging) {
        throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
    }

    if (mAdapter == null) {
        return;
    }

    mLastMotionY += yOffset;

    float oldScrollY = getScrollY();
    float scrollY = oldScrollY - yOffset;
    final int height = getClientHeight();

    // Don't lose the rounded component
    mLastMotionY += scrollY - (int) scrollY;
    scrollTo(getScrollX(), (int) scrollY);
    pageScrolled((int) scrollY);

    // Synthesize an event for the VelocityTracker.
    final long time = SystemClock.uptimeMillis();
    final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 0, mLastMotionY, 0);
    mVelocityTracker.addMovement(ev);
    ev.recycle();
}

虽然我们传递的offsetfloat但最终会强转成int会损失精度如果做动画来fakeDrag时损失的精度累加会很大导致最终滑动不精确

解决方法

l 如果不要求太精准传参时进行四舍五入

l 如果要求精准那么可以如下处理

val dargDistancePx = extra?.getFloat(UgcVideoGuideConstants.PARAM_KEY_DRAG_DISTANCE_PX) ?: 0f
var previousValue = 1f
var lossAccuracy = 0f
val startScrollY = parent.scrollY
addUpdateListener { valueAnimator ->
    val currentValue = valueAnimator.animatedValue as Float
    val dy = (previousValue - currentValue) * dargDistancePx
    val intDy = dy.toInt().toFloat()
    lossAccuracy += dy - intDy
    if (parent.isFakeDragging) {
        parent.fakeDragBy(intDy + lossAccuracy.toInt())
        lossAccuracy -= lossAccuracy.toInt().toFloat()
    }
    previousValue = currentValue
    if (valueAnimator.animatedFraction == 1f) {
        WZLogUtils.printInfoWithDefaultTag("scrollTo.total:${parent.scrollY - startScrollY}, lossAccuracy:$lossAccuracy")
    }
}

具体就是累计损失的精度超过1后会把整数部分算上然后继续累计