View的事件体系之二 View的滑动以及弹性滑动

  新年第一更,之前也有看过View体系系列文章,内容有点生疏了,重新温习一下,基础篇已经整理过了,接下来会重新梳理一遍关于View的整个体系的知识,权当复习了。

  在Android设备上,滑动几乎是应用的标配,不管是下拉刷新还是recyclerView和listView等控件的滑动,他们的基础都是滑动,不管哪种滑动,首先他们滑动的基本思想是一致的:当触摸事件传到View时,系统记录下触摸点的坐标,手指移动后系统也会记录下移动后的触摸点的坐标,然后算出偏移量,并通过偏移量来修改View的坐标。实现View的滑动目前来说主要有有以下三种方式:
1. 通过View本身提供的scrollTo/scrollBy方法来实现
2. 通过动画给View施加平移效果
3. 改变View的LayoutParams使的View重新布局

   1.1 使用scrollTo和scrollBy实现View的滑动

先看源码解析:

 /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    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();
            }
        }
    }

    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

从上面的两个方法可以看出,scrollBy实际上也是调用了scrollTo方法,他实现了基于当前位置的相对滑动,也就是相对于父View左上角坐标位置移动到传进来的参数x,y加上他本身所在位置的坐标的位置。而scrollTo则是实现了基于所传参数的绝对滑动,也就是说相对于父View左上角坐标位置移动传进来的参数x,y的位置。

再换句话说:两种滑动方式的参照物不同,scrollBy是将本身作为参照物,scrollTo是将父View作为参照物,也可以这么记scrollBy就是滑动了,scrollTo就是滑动到,整个滑动的过程是:

在滑动的过程中,mScrollX的值总等于View左边缘和View内容左边缘在水平方向上的值。而mScrollY的值总等于View上边缘和View内容上边缘在垂直方向上的值。View边缘是指View的位置,即View的四个顶点组成,而View内容边缘是指View中的内容的边缘,scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。mScrollX和mScrollY的单位是像素,并且当View的左边缘在View的内容的左边缘的右边时,mScrollX是正值。反之为负值。也就是说,不管怎么滑动,View本身不能移动,只是将View的内容进行移动。

举个例子:

   /**
     * 触摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //触摸点的坐标
        int x= (int) event.getX();
        int y= (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                //求得移动后的偏移量
                int offsetX=x-lastX;
                int offsetY=y-lastY;
                ((View)getParent()).scrollBy(-offsetX,-offsetY);
                break;
            case MotionEvent.ACTION_DOWN:
                //记录移动后的触摸点的坐标
                lastX=x;
                lastY=y;
                break;
            default:
                break;
        }
        return true;
    }
   1.2 使用动画实现View的滑动

  通过动画我们能够让一个View进行平移,而平移本就是一种滑动。使用动画来移动View,主要操作还是View的translationX和transLationY属性。在这里我们可以使用传统的动画,也可以使用属性动画。

  接下来分别采用两种方式将View在100ms内从原始位置移动到右下角100个像素的位置。

res——>anim——>translate.xml 的代码:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:zAdjustment="normal"
    >
    <translate
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100"
        android:duration="100"/>
</set>
activity中的使用:

        //使用补间动画
       btn.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));
        //使用属性动画
        //ObjectAnimator.ofFloat(btn,"translationX",0,500).setDuration(10000).start();
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(AnimationActivity.this, "点击了Button", Toast.LENGTH_SHORT).show();
            }
        });

  这里有两个很重要的点:

(1)补间动画属于View动画,即只对View的影像进行操作,并没有改变View的实际参数,包括宽高,并且,要想动画后的状态得以保留还必须将fillAfter属性值设置为true。否则动画完成后View就会恢复至原先的状态.通过我们的点击事件也可以验证出这个结果。
(2)属性动画可以解决此问题,但是无法兼容到Android3.0以下。

   1.3 改变布局参数,即LayoutParams

  LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。

   /**
     * 触摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //触摸点的坐标
        int x= (int) event.getX();
        int y= (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                //求得移动后的偏移量
                int offsetX=x-lastX;
                int offsetY=y-lastY;
                LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);
                break;
            case MotionEvent.ACTION_DOWN:
                //记录移动后的触摸点的坐标
                lastX=x;
                lastY=y;
                break;
            default:
                break;
        }
        return true;
    }

  由于父控件是LinearLayout,所以我们用了LinearLayout里的LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:

   1.4 其他的几种方式

使用layout();offsetLeftAndRight()与offsetTopAndBottom()也可以实现滑动,具体使用方法和上面方法一致,在onTouch()事件中的MotionEvent.ACTION_MOVE下:

case MotionEvent.ACTION_MOVE:
                //求得移动后的偏移量
                int offsetX=x-lastX;
                int offsetY=y-lastY;
               layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY;
                break;

或者

case MotionEvent.ACTION_MOVE:
                //求得移动后的偏移量
                int offsetX=x-lastX;
                int offsetY=y-lastY;
                //对left和right进行偏移
                offsetLeftAndRight(offsetX);
                //对top和bottom进行偏移
                offsetTopAndBottom(offsetY);

   1.5 弹性滑动

  相对于普通的滑动方式来说,弹性滑动的方式就是实现渐进式的滑动,实现弹性滑动的方式有很多种,但他们具有一个共同的思想就是:将一次大的滑动分为若干次小的滑动,并在同一个时间段内完成,首先介绍Scroller

   1.5.1 使用Scroller

  scroller的工作原理就是:当我们构造一个Scroller对象并且调用他的startScroll()方法时,Scroller内部其实什么也没做,他只是保存了传递的几个参数,我们从Scroller类的源码中就可以看到:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        //duration表示的是整个滑动过程的完成所需要的时间,默认的滑动时间为250毫秒. 
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        //startX和startY表示的是滑动的起点
        mStartX = startX;
        mStartY = startY;
        //dx和dy分别表示要在横纵坐标上滑动的距离
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

这里需要注意:这里的滑动并非View本身位置的改变而是View内容的滑动,仅仅调用startScroll()方法是没法让View滑动的,真正的幕后是invalidate方法,他会导致View重绘,通过使View重绘,会间接的执行computeScroll()方法。实例解析

 private void smoothScrollTo(int dx,int dy){
        //获取开始滑动时的坐标
        int sX=this.getScrollX();
        int sY=this.getScrollY();
        //将参数保存到Scroller中
        //dx-sX,dy-sY是横纵坐标滑动的距离,1000为整个滑动过程为1000毫秒,
        scroller.startScroll(sX,sY,dx-sX,dy-sY,1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        //判断滑动是否结束
        if(scroller.computeScrollOffset()){
            //getCurrX()返回当前的X轴偏移量,值等于当前View位置的左边界减去View内容的左边界。可以理解为View 中的mScrollX。
            //getCurrY()值等于View位置的上边界减去view内容的上边界。类似于View中的mScrollY.
            this.scrollTo(scroller.getCurrX(),scroller.getCurrY());
            postInvalidate();
        }
    }

而在源代码中computeScroll是一个空方法,需要我们自己实现,于是通过调用Scroller中的computeScrollOffset这个方法判断滑动是否结束,看一下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);
        //当执行动画已经花费的时间小于整个滑动过程完成的时间的时候,返回为true,计算出mCurrX和mCurrY的值,也就是当前View内容左边缘的x、y坐标的值
        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;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }
                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);
                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

从源码注释中我们可以得知,当调用这个方法我们可以知道滑动的新的坐标位置,并且当其返回值为true时,滑动动画就没有结束,当执行动画已经花费的时间小于整个滑动过程完成的时间的时候,返回为true,计算出mCurrX和mCurrY的值,也就是当前View内容左边缘的x、y坐标的值,当返回为true时,调用scrollTo方法, this.scrollTo(scroller.getCurrX(),scroller.getCurrY());而scroller.getCurrX(),scroller.getCurrY()获得的就是mCurrX,mCurrY也就是View内容左边缘的x、y坐标的值,然后再调用invalidate(),直到computeScrollOffset返回false时,滑动结束,即整个滑动过程完成。

  总结

  通过回顾,发现有好多细节自己之前都没有注意到,所以还是老话说的好,温故而知新啊。如果有哪些点你觉得我的理解不对。欢迎留言指正,最近开通了自己的微信公众号,偶尔更新文章,生活感悟,好笑的段子,欢迎订阅
这里写图片描述
文章中所用到的demo的下载地址

  参考资料:

《Android开发艺术探索》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值