专栏首页胡飞洋的Android进阶View的滑动方式 详细介绍

View的滑动方式 详细介绍

目录

一、坐标系

二、VelocityTracker、GestureDetector

1、VelocityTracker

2、GestureDetector

三、View的滑动

1、layout()

2、offsetLeftAndRight、offsetTopAndBottom

3、LayouParams

4、动画

5、scrollTo、scrollBy

6、Scroller

一、坐标系

  1. 上图圆点是手指触摸点,蓝色的是MotionEvent的方法,点击事件走到onTouchEvent,获得点击事件的各种坐标:getX、getY是相对view;getRawX、getRawY是相对屏幕。
  2. 绿色的是View获取坐标的方法。ViewGroup是View的父布局。getLeft、getRight、getTop、getBottom相对父布局。 width = getRight()- getLeft() height = getTop()- getBottom()

二、VelocityTracker、GestureDetector

1、VelocityTracker

用于追踪手指滑动速度的。例如相册的图片,手指快速左右滑动会切换图片,慢则不会切换。获取速度前,要先调用computeCurrentVelocity计算速度,如下代码。效果是手指滑的快时,就会弹Toast。

    private void init() {
        //获取速度追踪器
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //获取触摸点坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                //这句是为了随手指滑动,下面会讲
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //速度器添加事件
                mVelocityTracker.addMovement(event);

                break;
            case MotionEvent.ACTION_UP:

                //计算手指滑动速度:1000ms内滑过的像素,(终点-起点)/时间段,所以从右向左滑 为负值。
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                Log.i(TAG, "onTouchEvent: xVelocity = " + xVelocity);
                if (Math.abs(xVelocity) > 100) {
                    Toast.makeText(getContext(), "滑的有点快!", Toast.LENGTH_SHORT).show();
                }

                break;
            default:
                break;
        }

2、GestureDetector

手势检测。通常监听双击才使用GestureDetector,其他的滑动就在onTouchEvent中实现(DOWN、MOVE、UP)就可以了。

    private void init() {
        //要设置两个监听OnGestureListener、OnDoubleTapListener
        mGestureDetector = new GestureDetector(getContext(),this);
        mGestureDetector.setOnDoubleTapListener(this);
    }


    @Override
    public boolean onDown(MotionEvent e) {
        //手指触摸的一瞬间,由1个DOWN触发
        Log.i(TAG, "onDown: ");
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        //手指触摸的状态,由1个DOWN触发,强调的是没有拖动的状态,就是按着没动。
        Log.i(TAG, "onShowPress: ");
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        //单击,UP触发
        Log.i(TAG, "onSingleTapUp: ");
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //滚动,1个DOWN,多个MOVE触发
        Log.i(TAG, "onScroll: ");
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        //长按
        Log.i(TAG, "onLongPress: ");
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        //快速move后up
        Log.i(TAG, "onFling: ");
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        //确认的单击,不是双击中的某一击
        Log.i(TAG, "onSingleTapConfirmed: ");
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        //双击,两个单击组成。和onSingleTapConfirmed不能共存
        Log.i(TAG, "onDoubleTap: ");
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        //发生了双击行为,双击期间DOWN、MOVE、UP都会触发此回调
        Log.i(TAG, "onDoubleTapEvent: ");
        return false;
    }

三、View的滑动

滑动是自定义view的基础。共有6种滑动方法:

1、layout()

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //获取触摸点坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //DOWN时,即刚开始的触摸点相对view的坐标。
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //滑动的距离 = 触摸点滑动到的坐标 - 开始触摸的坐标 (都是相对于view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上这个滑动距离——有多重方式:

                //方法一,layout()
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

每次移动都会调layout重新布局,也就是滑动的效果。

效果如图,随着手指滑动:

2、offsetLeftAndRight、offsetTopAndBottom

            case MotionEvent.ACTION_MOVE:
                //滑动的距离 = 触摸点滑动到的坐标 - 开始触摸的坐标 (都是相对于view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上这个滑动距离——有多重方式:

                //方法一,layout()
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //方法二,offsetLeftAndRight、offsetTopAndBottom
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);

3、LayouParams

            case MotionEvent.ACTION_MOVE:
                //滑动的距离 = 触摸点滑动到的坐标 - 开始触摸的坐标 (都是相对于view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上这个滑动距离——有多重方式:

                //方法一,layout()
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //方法二,offsetLeftAndRight、offsetTopAndBottom
//                offsetLeftAndRight(offsetX);
//                offsetTopAndBottom(offsetY);

                //方法三,LayoutParams
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);

4、动画

关于动画,后面单独写一篇详细讲。现在知道有 :View动画(不能改变位置参数)、属性动画可以实现。

                //方法四,动画(一般在外面调用)
                //1、view动画(最终效果是滑动第一下可以滑动,后面再滑不行,因为view不能改变view的位置参数,不能真正的交互。)
                float toXDelta = offsetX / getWidth();
                float toYDelta = offsetY / getHeight();
                TranslateAnimation animation = new TranslateAnimation(
                        TranslateAnimation.RELATIVE_TO_SELF, 0,
                        TranslateAnimation.RELATIVE_TO_SELF, toXDelta,
                        TranslateAnimation.RELATIVE_TO_SELF, 0,
                        TranslateAnimation.RELATIVE_TO_SELF, toYDelta);
                animation.setDuration(0);
                animation.setFillAfter(true);
                startAnimation(animation);

                //2、属性动画(横移,貌似不适合放这里使用,效果会闪)
                ObjectAnimator.ofFloat(this,"translationX",0, offsetX).setDuration(0).start();

5、scrollTo、scrollBy

scrollTo(x,y)是瞬间移动到(x,y),scrollBy(deltaX,deltaY)是移动增量。代码使用方法:

            case MotionEvent.ACTION_MOVE:
                //滑动的距离 = 触摸点滑动到的坐标 - 开始触摸的坐标 (都是相对于view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上这个滑动距离——有多重方式:

                //方法五,scrollTo、scrollBy。

//                ((View) getParent()).scrollTo(getScrollX() - offsetX, getScrollY() - offsetY);
                //scrollBy同理:
                ((View)getParent()).scrollBy(-offsetX, -offsetY);

说明1:

scrollBy还是调的scrollTo :

    /**
     * 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);
    }

mScrollX:View左边缘和 view的内容(即view的子view)左边缘的距离。负值表示 view左边缘在view内容的左侧。

mScrollY:VIew上边缘和 view的内容(即view的子view)上边缘的距离。负值表示 view上边缘在view内容的上面。

说明2:

scrollBy、scrollTo移动的是view的内容,如果是ViewGroup使用,即移动其所有的子view,若没有子view就没有效果。所以上面代码使用getParent()来调用。

或者,换一种理解方式(个人感觉这个更好理解)scrollBy、scrollTo移动的就是view本身,而view的内容不动,只不过此时屏幕也随view本身一起移动,视觉上就是 view的内容 就会反向移动。 例如,使用view.scrollBy(100,0),那么view和屏幕一起右移100,即视觉上view的内容左移100。所以要让view的内容视觉上右移100,需要view的父view左移100,view.scrollBy(-100,0)。所以,上面用getParent,而且传的是负值

6、Scroller

Scroller是处理滑动效果的工具类,来实现有过度效果的弹性滑动(有个过程,不是瞬间完成的)。

Scroller本身不能实现弹性,需要结合View的computeScroll()方法。基本是固定的代码:先new 一个Scroller,然后重写computeScroll(),在写一个方法供调用,这里是smoothScrollTo。

    private void init() {
        mScroller = new Scroller(getContext());
    }

    /**
     * 弹性滑动
     *
     * @param desX     目标X
     * @param desY     目标Y
     * @param duration 时间
     */
    private void smoothScrollTo(int desX, int desY, int duration) {
        int deltaX = desX - getScrollX();
        int deltaY = desY - getScrollY();

        //前两个是起点左边,中间两个是滑动距离,duration是时间。此时仅仅是存入数据,并没有滑动。
        mScroller.startScroll(getScrollX(), getScrollY(), deltaX, deltaY, duration);

        //invalidate会导致重新绘制,即走draw(),然后走computeScroll()
        invalidate();
    }

    @Override
    public void computeScroll() {
        //计算本次滚动的位置,数据保存在Scroller中。返回true表示滚动未结束。
        if (mScroller.computeScrollOffset()) {
            //从Scroller中取出计算好的位置,并使用父view调scrollerTo来滑动 本身。
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //再次调绘制,又会走computeScroll(),继续这个过程,直到mScroller.computeScrollOffset()返回false结束滑动。
            invalidate();
        }
    }

看下startScroll方法,仅仅保存了传入的起点、移动距离、时间,并没有真正移动。

computeScroll方法:根据时间计算当前一个滑动的距离,返回true表示滑动还没结束。

所以,整个过程是:调用startScroll向Scroller传入滑动的距离和时间,然后调用了invalidate(),invalidate会导致重新绘制,即走draw(),然后走computeScroll(),computeScroll中计算本次滚动的位置,数据保存在Scroller中,返回true表示滚动未结束。然后就调用scrollTo传入计算好的当前的滑动距离,这样就是实现了一小段的滑动。然后又调用invalidate(),就会继续这个过程。最终实现弹性动画。

参考资料: 《Android进阶之光》第三章 《Android开发艺术探索》第三章

本文分享自微信公众号 - 胡飞洋(hfydwxgzh),作者:胡飞洋

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 带你彻底搞懂-View的工作原理!

    1.ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带。View的三大流程是通过ViewRoot完成的。...

    胡飞洋
  • 这次,我把Android事件分发机制翻了个遍

    作者:积木zz 链接:https://juejin.im/post/5eb3e0d6f265da7c002028cd 以下是原文:

    胡飞洋
  • 曝光埋点方案:recyclerView中的item曝光逻辑实现

    首先,客户端要考虑的就是只管调用api上报:上报item可见、上报item不可见。至于是否是有效曝光,就是公共埋点SDK(中台提供)去计算了。

    胡飞洋
  • Android6.0源码分析之View(二)--measure Android6.0源码分析之View(一)

    接着上一篇 Android6.0源码分析之View(一) 紧接着来学习view的measure,(注,开始写博客之后,很明显我的学习效率高多了,研究了俩星期硬是...

    fanfan
  • Android6.0源码分析之View(二)--measure

    紧接着来学习view的measure,(注,开始写博客之后,很明显我的学习效率高多了,研究了俩星期硬是没有研究view的measure,接下来终于可以来好好研究...

    fanfan
  • 微信小程序--基于ColorUI构建皮皮虾短视频去水印组件(仅供学习使用)

    没错,我是皮友,我想学习舞蹈(/doge)和瑜伽 ,要无水印的那种有助于我加深学习。

    Kindear
  • 回文素数

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    喜欢ctrl的cxk
  • 微信小程序入门(三)

    四个组成部分,其它三个前面介绍过了,主要WXS: WXS:对wxml增强的一种脚本语言,可以对请求的数据进行filter或者做计算处理,帮助wxml快速构建出...

    zhang_derek
  • 购物车

    达达前端
  • uni-app提交表单成功之后跳转首页

    功能需求: 1:填写表单内容,点击提交审批按钮,提交成功之后,弹出弹框,提示已经提交成功。2:提交成功之后,两秒钟之后自动跳转到首页

    王小婷

扫码关注云+社区

领取腾讯云代金券