首页 > 代码库 > 自定义 ViewGroup 支持无限循环翻页之三(响应回调事件)

自定义 ViewGroup 支持无限循环翻页之三(响应回调事件)

大家如果喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢

转载请标明出处,再次感谢

#######################################################################

自定义 ViewGroup 支持无限循环翻页系列

自定义 ViewGroup 支持无限循环翻页之一(重写 onLayout以及 dispatchDraw)

自定义 ViewGroup 支持无限循环翻页之二(处理触摸事件)

自定义 ViewGroup 支持无限循环翻页之三(响应回调事件)

#######################################################################


在上一篇博文中,我曾提到过,我是用了一个 TouchHandler 来处理 view 的 touch 事件,再在 TouchHandler 内通过回调接口通知 view 处理对应的回调事件.

现在我们就来详细分析一下,如何通过处理回调事件,实现界面的滑动与切换

首先,我们先了解一下在 Android 的View 类中是如何进行界面位置的

在 View 类中,有两个方法 scrollBy,scrollTo, 来控制当前屏幕的显示区域,同时内部有两个变量 mScrollX,mScrollY 来记录当前的位移量

对于 scrollBy 方法,他其实是通过之前记录的 mScrollX,mScrollY 来计算出应该位移到的坐标点,然后调用 scrollTo 方法进行移动,这个方法的作用也正如其名,就是用来将整个视图进行偏移的.

此外 Android 还为我们提供了一个 Scroller 类帮助我们进行平滑的移动, Scroller 类主要通过一个插值器计算经过某段时间后,当前界面的坐标位置,然后 Android 系统通过在调用 invalidate方法时所调用到得 computeScroll 方法中,对界面位置进行重新获取,从而让移动看起来连续.

Ok,现在我们正式开始响应回调事件,我们先查看一下起始界面,


如图所示,在默认情况下,我们一共有三组完全一致的图片,可能会被显示到屏幕上,以实现我们连续滑动的效果.

当我们手指按下,开始移动的时候,我们也希望界面能够跟随我们的手指进行移动,例如当我们手指向左滑动的时候,我们希望看到的界面是这样子的


因此,我们希望屏幕的显示区域进行一个位移,来达到这种效果.

我们在 TouchHandler 中的 handleTouchEvent 方法中,有过对手指移动的事件进行处理,如果手指移动区域超过某个限值后,我们会获取每次 touch 事件的差值,调用 onScrollBy 的回调方法,所以我们只需要在 SerialScreenLayout 中继承这个接口,并实现方法即可.

实现方案如下:

@Override
    public void onScrollBy(int dx, int dy) {
        scrollBy(dx, 0);
    }

根据接口方法的传入值进行界面偏移即可,注意由于我们只需要在 x 轴上做变换,因此 y 轴的偏移我们可以令他恒为0.

这样一来,当我们的手指在屏幕上进行滑动的时候,界面就会跟随我们手指的移动而移动了.

然后当我们的手指释放的时候,我们其实并不希望界面停留在之前的位置,而是希望他根据之前我们手指的位移偏量或者滑动速度,切换到一个独立的 View 界面去,如下:


所以我们在 TouchHandler 类中,当手指停止触摸,触发 ACTION_UP 的时候,我们向 View, 进行一次 onRelease 回调,然后在方法中对当前位置和速度进行判别,以确定应该跳转的屏幕位置,然后再进行一次屏幕平滑移动,详细代码如下:

@Override
    public void onRelease(int velocityX, int velocityY, boolean cancel) {
        cleanPosition();
        int targetPosition = mCurrPosition;
        if (mDirty) {
            mDirty = false;
            if (Math.abs(mLastPosition - getScrollX()) > mGutterSize && Math.abs(velocityX) > mMinimumVelocity) {
                if (mTmpDirectionLeft) {
                    if (velocityX > 0) {
                        targetPosition -= 1;
                    } else {
                        if (getScrollX() > mCurrPosition * mWidth) {
                            targetPosition += 1;
                        }
                    }
                } else {
                    if (velocityX > 0) {
                        if (getScrollX() > mCurrPosition * mWidth) {
                            targetPosition -= 1;
                        }
                    } else {
                        targetPosition += 1;
                    }
                }
            }
            scrollToPosition(targetPosition, 0);
        } else if (!cancel) {
            int startX = mWidth * mCurrPosition;
            int deltaX = getScrollX() - startX;
            if (deltaX > 0) {
                if (velocityX <= 0) {
                    if (Math.abs(deltaX) > mGutterSize) {
                        targetPosition = mCurrPosition + 1;
                    } else if (Math.abs(velocityX) > mMinimumVelocity) {
                        targetPosition = mCurrPosition + 1;
                    }
                }
            } else {
                if (velocityX >= 0) {
                    if (Math.abs(deltaX) > mGutterSize) {
                        targetPosition = mCurrPosition - 1;
                    } else if (Math.abs(velocityX) > mMinimumVelocity) {
                        targetPosition = mCurrPosition - 1;
                    }
                }
            }
            scrollToPosition(targetPosition, velocityX);
        }else{
            scrollToPosition(targetPosition, velocityX);
        }
    }


上面代码中的 mDirty 将会在之后的部分被讨论,我们先来看看其他的部分, cancel 仅当 touch 事件为 ACTION_CANCEL 时为 true,说明整个滑动过程被取消,所以移动到之前的位置,否则就根据当前的速度以及移动偏移进行判断,然后选择下一个位置进行平滑位移.

平滑位移的代码如下:

private void scrollToPosition(int targetPosition, int velocityX) {
        cleanPosition();
        targetPosition = formatPosition(targetPosition);
        if (mCurrPosition == 0 && targetPosition == getChildCount() - 1) {
            targetPosition = -1;
        } else if (mCurrPosition == getChildCount() - 1 && targetPosition == 0) {
            targetPosition = getChildCount();
        }
        int startX = getScrollX();
        int finalX = targetPosition * mWidth;
        final int delta = Math.abs(finalX - startX);
        velocityX = Math.abs(velocityX);

        int duration = (int) (1.0f * DEFAULT_DURATION * delta / mWidth);
        if (velocityX > mMinimumVelocity) {
            final int width = mWidth;
            final int halfWidth = width / 2;
            final float distanceRatio = Math.min(1f, 1.0f * delta / width);
            final float distance = halfWidth + halfWidth *
                    distanceInfluenceForSnapDuration(distanceRatio);
            int velocityDuration = 4 * Math.round(1000 * Math.abs(distance / velocityX));
            duration = Math.min(duration, velocityDuration);
        }
        mScroller.startScroll(startX, 0, finalX - startX, 0, duration);
        invalidate();
        mCurrPosition = targetPosition;
    }

在这里,我们根据滑动速度计算出一个移动耗时,然后启动 mScroller, 进行滑动位置计算,而后调用 invalidate 进行位移

这样一来,当我们松开手指,界面就会平滑的移动到对应的 View 去.

当时这样一来,一个新的问题就冒出来了,假如现在我们的位置是这样的:

那么我们移动的时候我们手指向右滑动,左侧仍然会出现空白的 View 而非最后一张,这样的话,其实我们就只是解决了中间部分的连续,但是边缘的连续还是会出现问题,为了解决这个问题,我们建了一个 cleanPosition()方法,用来计算当前的位置,代码如下:

  private void cleanPosition() {
        final int position = mCurrPosition;
        final int sx = position * mWidth;
        final int cx = getScrollX();
        int delta = cx - sx;
        if (Math.abs(delta) >= mWidth) {
            //scrolled to a new page
            if (delta > 0) {
                mCurrPosition = cx / mWidth;
            } else {
                mCurrPosition = cx / mWidth - 1;
            }
        }
        final int tmp = formatPosition(mCurrPosition);
        if (tmp != mCurrPosition) {
            scrollBy((tmp - mCurrPosition) * mWidth, 0);
            mCurrPosition = tmp;
        }
    }

    private int formatPosition(int position) {
        if (position < 0) {
            do {
                position += getChildCount();
            } while (position < 0);
        } else if (position >= getChildCount()) {
            do {
                position -= getChildCount();
            } while (position >= getChildCount());
        }
        return position;
    }

当我们的移动范围超过 View 的真实显示范围之后,我们会通过计算,将显示范围平移回正常的界面,这样,我们最终的显示范围始终都在基本的三个 View 内部,这样就解决了可能出现的边缘不连续问题.

当时,在实际滑动过程中,我们滑动的时候,如果双指交替滑动,那么我们就可以强行将 view 滑动至边缘而出现不连续问题,所以我们针对多点触控的回调方法中,再调用了 cleanPosition, 来保持 view 始终处于正确位置.

实际滑动过程中,还可能出现某次滑动未结束,我们手指就已经开始了下一次的滑动,那么,之前的 onRelease 判断就会出现逻辑问题,因为之前我们是根据滑动速度和偏移量进行判断的下一个位置,但是现在滑动还未结束,起始的偏移量和位置都与之前不同,因此我们在 onTouchEvent 中,做了如下实现:

 @Override
    public void onTouch(MotionEvent event) {
        if (!mScroller.isFinished()) {
            if (Math.abs(mScroller.getCurrX() - mScroller.getFinalX()) > mGutterSize) {
                mDirty = true;
                cleanPosition();
                mLastPosition = getScrollX();
                if (mScroller.getFinalX() > mScroller.getCurrX()) {
                    mTmpDirectionLeft = true;
                } else {
                    mTmpDirectionLeft = false;
                }
                mScroller.abortAnimation();
            }
        } else {
            mDirty = false;
        }
    }

在这里,我们记录下当前滑动的一些相关属性,并令 mDirty 变量为 true,使得自身能够判断是否是在滑动过程中突然中断,然后出发事件,然后再在 onRelease 中根据当前的相关属性进行判断,选择下一位置.

写到这里,整个 SerialScreenLayout 的实现逻辑已经介绍完了,其主要原理就是:

在 onLayout 中根据每个 view 的顺序让他们单独享用一个屏幕的固定空间,再在 dispatchDraw 的时候,在左右两侧分别多绘制一组图像,使得显示连续,最后处理 touch 事件,使得整个自定义 ViewGroup 能够移动,并在停止触摸后能够移动到某段屏幕位置

自定义 ViewGroup 支持无限循环翻页之三(响应回调事件)