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为负数,
那么第一次虽然手指向左,但最终却向相反方向,
第二次计算后scrollX为0,因为上次负数,此时会和手指方向一致,
这样反反复复就出现了滑动抽搐。
解决方法
在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(); }
虽然我们传递的offset是float,但最终会强转成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后会把整数部分算上,然后继续累计。
ViewPager的ItemInfo和mItems
在读源码时ViewPager的几个变量/属性特别重要,直接影响了我对代码逻辑的理解,通过反复阅读源码加上查阅其他源码分析的博客最终搞懂了viewpager的整个流程。
- mItems:是一个ArrayList,其中存储的是ItemInfo,表示已经缓存的页面信息,会缓存当前页面,和前后2个(offscreenPageLimit为1时)。
- ItemInfo:是用来保存页面信息的,
1 static class ItemInfo { 2 Object object; 3 int position; 4 boolean scrolling; 5 float widthFactor; 6 float offset; 7 }
offset表示当前页面的偏移量,进行布局或者跳转时会使用到。
当首次 或者 把mItems清空的操作(setAdapter/dataSetChanged),会把当前currentItem的offset设置为0,作为基准点,之前的页面offset就是减去mAdapter.getPageWidth(pos),之后的页面就是加上mAdapter.getPageWidth(pos)