ViewPaper 系列 — onLayout以及手势移动处理

路是一步一步走的,代码也是一行一行敲的,只要你始终把握好前行的方向,终究会到达你想要去的地方。

您还可以查看上一篇文章:
《viewpaper系列之子view的缓存基本原理》

onMeasure

在上一节中,还有一个地方没有说到,就是 lp.isDecor这个参数,指不是从adapter中添加子view时返回true,业务逻辑也就进入下面代码进行子view的测量。

int size = getChildCount();
Log.d(TAG_TEST,"size: "+ size);
for (int i = 0; i < size; ++i) {
    final View child = getChildAt(i);
    if (child.getVisibility() != GONE) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        // 这里的isDecor参数是指当你不是从中adapter添加子view时返回为true
        if (lp != null && lp.isDecor) {
            final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
            final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;

            int widthMode = MeasureSpec.AT_MOST;
            int heightMode = MeasureSpec.AT_MOST;
            boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
            boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;

            if (consumeVertical) {
                widthMode = MeasureSpec.EXACTLY;
            } else if (consumeHorizontal) {
                heightMode = MeasureSpec.EXACTLY;
            }

            int widthSize = childWidthSize;
            int heightSize = childHeightSize;
            if (lp.width != LayoutParams.WRAP_CONTENT) {
                widthMode = MeasureSpec.EXACTLY;
                if (lp.width != LayoutParams.FILL_PARENT) {
                    widthSize = lp.width;
                }
            }
            if (lp.height != LayoutParams.WRAP_CONTENT) {
                heightMode = MeasureSpec.EXACTLY;
                if (lp.height != LayoutParams.FILL_PARENT) {
                    heightSize = lp.height;
                }
            }
            final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
            final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
            child.measure(widthSpec, heightSpec);

            if (consumeVertical) {
                childHeightSize -= child.getMeasuredHeight();
            } else if (consumeHorizontal) {
                childWidthSize -= child.getMeasuredWidth();
            }
        }
    }
}

onLyout

final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    // 判断子view是否为显示状态
    if (child.getVisibility() != GONE) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        ItemInfo ii;
        if (!lp.isDecor && (ii = infoForChild(child)) != null) {
            int loff = (int) (childWidth * ii.offset);
            int childLeft = paddingLeft + loff;
            int childTop = paddingTop;
            if (lp.needsMeasure) {
                // This was added during layout and needs measurement.
                // Do it now that we know what we're working with.
                lp.needsMeasure = false;
                final int widthSpec = MeasureSpec.makeMeasureSpec(
                        (int) (childWidth * lp.widthFactor),
                        MeasureSpec.EXACTLY);
                final int heightSpec = MeasureSpec.makeMeasureSpec(
                        (int) (height - paddingTop - paddingBottom),
                        MeasureSpec.EXACTLY);
                child.measure(widthSpec, heightSpec);
            }
            if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
                    + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
                    + "x" + child.getMeasuredHeight());
            // 给每个子view进行布局
            child.layout(childLeft, childTop,
                    childLeft + child.getMeasuredWidth(),
                    childTop + child.getMeasuredHeight());
        }
    }
}

看完上面的代码,我们从上到下依次来说说注意点

第一点

我们来看看这个方法它的具体执行,因为其返回值直接影响到if语句的逻辑走向,导致是否执行child的layout操作。
ii = infoForChild(child)

ItemInfo infoForChild(View child) {
        for (int i=0; i

看到上面if中的判断条件,是不是有种熟悉的感觉,对的,他就是adapter中我们时常要复写的一个方法的调用。现在知道为什么要这样写了吧,因为这个返回值代表着当前这个view你是否给它去进行layout的布局方法的调用,那你肯定会问,为什么不直接返回true呢?仔细看你就会明白,因为返回true会导致所有的子view都会被布局,而viewpager的设计就是,只加载当前页面和预加载页面。所以没有必要给所有的子view进行布局。

@Override
public boolean isViewFromObject(View view, Object object) {
    return view == object;
}
第二点

这个条件
lp.needsMeasure
从命名就知道,是是否需要进行测量,主要是为了在layout进行时中添加子view时进行使用,在添加时给子view的layoutparam属性进行needsMeasure的赋值。

到此,子view的layout基本完毕,接下来就可以进入onTouchEvent来查看滑动时的逻辑了。

onTouchEvent

这个方法比较长,我们分别来看。

action_down
case MotionEvent.ACTION_DOWN: {
    mScroller.abortAnimation();
    mPopulatePending = false;
    populate();

    // Remember where the motion event started
    mLastMotionX = mInitialMotionX = ev.getX();
    mLastMotionY = mInitialMotionY = ev.getY();
    // 这个point为活动id
    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
    Log.d("zp_test","mActivePointerId: " + mActivePointerId);
    break;
}

什么是活动id呢?也就是当前能操作界面的这个点击事件的id,这个值在同一系列的点击事件中是不会发生改变的,所以通过这个id就可以始终获取到能够操作界面的那个手指的点击事件(比如,我第一个手指点下时,这是是手指1操作,这时,如果我再次用另一根手指2点击屏幕,那么这个时候能够操作屏幕的就是手指2,通过这个id就可以获取到手指2的位置信息等),这里主要是为了兼容多点触控的情况。

action_move

move就是很常见一系列移动view的操作,主要就是记录住手指移动的距离;
需了解的点:

  • 移动距离记得跟mTouchSlop(手指移动最小距离)进行比较
  • 回调监听的调用,手指状态发生改变,setScrollState(SCROLL_STATE_DRAGGING);
  • 从父元素那取得滑动事件的处理权,requestParentDisallowInterceptTouchEvent(true);
case MotionEvent.ACTION_MOVE:
        if (!mIsBeingDragged) {
    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
    if (pointerIndex == -1) {
        // 如果viewpager的子元素处理某个点击事件,有可能导致进入这个判断
        needsInvalidate = resetTouch();
        break;
    }
    final float x = MotionEventCompat.getX(ev, pointerIndex);
    final float xDiff = Math.abs(x - mLastMotionX);
    final float y = MotionEventCompat.getY(ev, pointerIndex);
    final float yDiff = Math.abs(y - mLastMotionY);
    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
    if (xDiff > mTouchSlop && xDiff > yDiff) {
        if (DEBUG) Log.v(TAG, "Starting drag!");
        mIsBeingDragged = true;
        requestParentDisallowInterceptTouchEvent(true);
        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                mInitialMotionX - mTouchSlop;
        mLastMotionY = y;
        // 手指状态发生改变,这时进行回调监听的调用。
        setScrollState(SCROLL_STATE_DRAGGING);
        setScrollingCacheEnabled(true);

        // Disallow Parent Intercept, just in case
        ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }
    }
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
    // Scroll to follow the motion event
    final int activePointerIndex = MotionEventCompat.findPointerIndex(
            ev, mActivePointerId);
    final float x = MotionEventCompat.getX(ev, activePointerIndex);
    needsInvalidate |= performDrag(x);
}

最后真正执行滑动操作的代码在performDrag(x);

private boolean performDrag(float x) {
    boolean needsInvalidate = false;
    // 手势滑动的距离
    final float deltaX = mLastMotionX - x;
    mLastMotionX = x;
    // 当前位置到该view的偏移量
    float oldScrollX = getScrollX();
    float scrollX = oldScrollX + deltaX;
    final int width = getClientWidth();
    // viewpager的左边界位置
    float leftBound = width * mFirstOffset;
    // viewpager的右边界位置
    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;
            needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
        }
        scrollX = leftBound;
    } else if (scrollX > rightBound) {
        if (rightAbsolute) {
            float over = scrollX - rightBound;
            needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
        }
        scrollX = rightBound;
    }
    // Don't lose the rounded component
    mLastMotionX += scrollX - (int) scrollX;
    // 最终实现随手指滑动的代码
    scrollTo((int) scrollX, getScrollY());
    pageScrolled((int) scrollX);

    return needsInvalidate;
}

到这里,我们可以知道,随手指进行滑动这类操作,主要使用到的方法就是scrollTo(),而传递进入的参数一般都是可以通过(当前偏移量)+(手指滑动距离)这样的组合进行传递。比如:getScrollX()+deltaX等等。

action_up
if (mIsBeingDragged) {
    final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
            velocityTracker, mActivePointerId);
    mPopulatePending = true;
    final int width = getClientWidth();
    final int scrollX = getScrollX();
    final ItemInfo ii = infoForCurrentScrollPosition();
    final float marginOffset = (float) mPageMargin / width;
    final int currentPage = ii.position;
    final float pageOffset = (((float) scrollX / width) - ii.offset)
            / (ii.widthFactor + marginOffset);
    Log.d("zp_test","pageOffset: " + pageOffset);
    final int activePointerIndex =
            MotionEventCompat.findPointerIndex(ev, mActivePointerId);
    final float x = MotionEventCompat.getX(ev, activePointerIndex);
    final int totalDelta = (int) (x - mInitialMotionX);
    // 根据滑动手势的快慢,偏移量来判断是否需要滑动到下一页
    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
            totalDelta);
    setCurrentItemInternal(nextPage, true, true, initialVelocity);

    needsInvalidate = resetTouch();
}

接下来看 setCurrentItemInternal(nextPage, true, true, initialVelocity)中有一部分代码如下:

...
if (mFirstLayout) {
    // We don't have any idea how big we are yet and shouldn't have any pages either.
    // Just set things up and let the pending layout handle things.
    mCurItem = item;
    if (dispatchSelected) {
        dispatchOnPageSelected(item);
    }
    requestLayout();
} else {
    populate(item);
    scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}

在非第一次layout的情况下会进行else中的代码执行,先计算所有子view,然后平滑移动到目的地。最终定位到以下代码

...
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
    duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
    final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
    final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
    duration = (int) ((pageDelta + 1) * 100);
}
duration = Math.min(duration, MAX_SETTLE_DURATION);

// Reset the "scroll started" flag. It will be flipped to true in all places
// where we call computeScrollOffset().
mIsScrollStarted = false;
// 平滑移动到指定的sx位置
mScroller.startScroll(sx, sy, dx, dy, duration);
ViewCompat.postInvalidateOnAnimation(this);
...

移动的任务就交给了mScroller.startScroll(sx, sy, dx, dy, duration);这句代码了。关于Scroller的原理,可以自行了解,其实现过程其实就是在指定时间内,把需要移动的距离等分为sx/duration段,然后在duration时间段内进行不断更新当前偏移量。

你可能感兴趣的:(ViewPaper 系列 — onLayout以及手势移动处理)