欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-05 10:06:06
...

  新年第一更,之前也有看过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时,滑动结束,即整个滑动过程完成。

  总结

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

  参考资料:

《Android开发艺术探索》

相关标签: view滑动 Scroller