浅谈View的滑动

由于移动设备屏幕小的特点,View支持通过滑动来展示更多的内容就成为了必然,系统给我们提供了诸如ScrollView,ListView等滑动控件,但是这些最基础的自带控件往往难以满足我们的需求,这就需要我们具备能够自定义滑动View的能力来满足需求。还有就是如果要想实现绚丽的动画效果,也必须得依赖View的滑动来实现,所以,我们还是很有必要来了解下View的滑动原理以及自定义View滑动的实现。

实现View滑动最常见的方式有:通过View的ScrollTo/ScrollBy方法,通过动画,通过改变布局参数LayoutParams。
下面就一 一介绍下这几种方式的实现以及他们各自的优缺点。

1.通过View的ScrollTo/ScrollBy方法
这两个方法用于滑动View内的内容的位置,不能改变View自身的位置。
ScrollTo是绝对滑动,滑动到某个具体的位置;ScrollBy是相对滑动,滑动到距离当前位置某个距离的位置,其本质上也是通过ScrollTo来实现滑动的,这点从源码中可以很明显的看出来。

public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

这里需要了解下View的mScrollX,mScrollY 属性,它可以通过getScrollX(),getScrollY()来获取,mScrollX始终表示View左边缘到View内容左边缘的水平距离,mScrollY 始终表示View顶部边缘到View内容顶部边缘的垂直距离,我们通过图示具体说明下:
这里写图片描述
从原始位置滑动View内容,向左滑动,mScrollX为正,反之为负。向下滑动,mScrollY为正,反之为负。
设置scrollBy(100,100),则view内容相对于原始位置向左水平移动100px,向上水平移动100px。
设置scrollTo(100,100),则view内容左上点位于相对于view左上点的左边和上边,水平距离为100px,垂直距离为100px。

2. 动画实现滑动
分为利用View动画和利用属性动画来实现,两者主要是操作View的translationX,translationY来实现的。
View动画实现,只是把View的视图影像移动到了对应位置,并没有改变view的位置参数,宽高等,并且,view的点击事件区域还是在原处,当然,关于点击区域问题,我们可以采取对应的一些措施来解决,这里就不细说了。
这里写图片描述
属性动画则是实实在在的把view移动到了目标位置,但是属性动画是在android 3.0版本以上才会支持,会存在兼容性问题。
如,在1000ms内,把view向右移动100px,

ObjectAnimator.ofFloat(rl1, "translationX", 0, 100)
                        .setDuration(1000)
                        .start();

3. 改变布局参数LayoutParams
通过改变View的参数来达到滑动的效果,如要想让TextView向右滑动100px,可以将其左边距增加100px,

ViewGroup.MarginLayoutParams marginLayoutParams = 
                (ViewGroup.MarginLayoutParams) tv1.getLayoutParams();
marginLayoutParams.leftMargin+=100;
tv1.requestLayout();     

接下来我们说下实现弹性滑动的几种思路,毕竟滑动瞬间完成是一种很不好的用户体验。

1. 借助Scroller来实现弹性滑动
需要借助Scroller来实现,我们先给出通常实现代码。

public class SmoothScrollView extends RelativeLayout {
    public SmoothScrollView(Context context) {
        super(context);
        init(context);
    }

    public SmoothScrollView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SmoothScrollView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private Scroller mScroller;
    public void init(Context context)
    {
        mScroller = new Scroller(context);
    }

    /**
     * 平滑滑动
     * @param desX 目标位置x坐标
     * @param desY 目标位置y坐标
     */
    public void smoothScrollTo(int desX, int desY)
    {
       int scrollX = getScrollX();
       int deltaX = desX - scrollX;

       int scrollY = getScrollY();
       int deltaY = desY - scrollY;

       mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
       invalidate();
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset())
        {
            //实现滑动的本质还是调用了scrollTo
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
}

在实现代码中可以看到,实现view发生滑动还是scrollTo方法,那么Scroller到底做了什么让瞬间滑动变为弹性滑动呢?我们看下ScrollerstartScroll源码:

 /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

我们可以看到,在方法里面,只是保存了一些信息,并没有对targeView作什么操作,甚至于对targeView的引用都没有,是不是很奇怪?接着往下看,在mScroller.startScroll之后,我们调用了invalidate(),这个方法会引发View的重绘(draw),在draw方法中,会回调view的computeScroll,这个方法是个空实现,我们需要重写此方法,在这个方法实现中首先判断下mScroller.computeScrollOffset(),我们截取这个方法源码的一部分来看下:

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;

不知道注意到timePassed 这个变量没,还有if (timePassed < mDuration) 这个判断,没错,这个方法会告诉你是否还需要滑动,再往下看mScroller.getCurrX(), mScroller.getCurrY(),我们看下mScroller.getCurrX()源码实现:

   /**
     * Returns the current X offset in the scroll. 
     * 
     * @return The new X offset as an absolute distance from the origin.
     */
    public final int getCurrX() {
        return mCurrX;
    }

只是返回了mCurrX这个变量,mCurrX正是在computeScrollOffset中被赋值的,可以发现,mCurrX是根据时间的流逝来计算targetView当前的scrollX,scrollY值的,也就是应该scroll到的位置,然后调用targetView的scrollTo滑动,然后调用postInvalidate(),让view再次重绘,再次调用computeScroll,如此反复达到弹性滑动的目的,是不是觉得这种实现很巧妙?Scrooler本质上跟插值器类似。

2. 通过动画
动画本身就是一个渐进的过程,所以通过他来实现,天然就有弹性滑动效果,这里我们来看下利用动画的特性来实现scrollTo的弹性效果:

    /**
     * 平滑滑动
     * @param desX 目标位置x坐标
     * @param desY 目标位置y坐标
     */
    public void smoothScrollTo(int desX, int desY)
    {
       final int scrollX = getScrollX();
       final int deltaX = desX - scrollX;

       final int scrollY = getScrollY();
       final int deltaY = desY - scrollY;

       /*mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
       invalidate();*/

        final ValueAnimator valueAnimator = ValueAnimator.ofInt(0,1).setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float  fraction = valueAnimator.getAnimatedFraction();
                int x = scrollX + (int)(deltaX*fraction);
                int y = scrollY + (int)(deltaY*fraction);
                scrollTo(x,y);
            }
        });
        valueAnimator.start();
    }

我们通过拿到动画的执行进度(valueAnimator.getAnimatedFraction()),来计算出view该scroll到的位置。

3. 使用延时策略
利用HandlerpostDelayedViewpostDelayed,这里就不细说了,实现比较简单。

猜你喜欢

转载自blog.csdn.net/qq_15692039/article/details/80723452
今日推荐