一个跟随手指移动的 View 产生的抖动问题

比如有这样一个常见的 app 功能,一个跟随手指移动的按钮,常见如屏幕悬浮球、拖拽手柄等。

实现这样的需求,必然要处理 view 的 onTouch 事件,大概的一个代码模板就是这样:

private float panelLastY;
private float panelStartX;
private float panelStartY;

@SuppressWarnings("ClickableViewAccessibility")
private void initTouchEvent() {
    panelView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    panelLastY = event.getRawY();
                    panelStartX = event.getX();
                    panelStartY = event.getRawY();
                    break;

                case MotionEvent.ACTION_MOVE:
                    Log.e("yifeng", "onTouchY: " + event.getY());
                    Log.e("yifeng", "onTouchRawY: " + event.getRawY());
                    int deltaY = (int) (event.getRawY() - panelLastY);
                    panelLastY = event.getRawY();
                    //TODO 通过 margin 或者 translationY 控制 view 移动
                    break;

                case MotionEvent.ACTION_UP:
                    float panelEndX = event.getX();
                    float panelEndY = event.getRawY();
                    if (Math.abs(panelEndX - panelStartX) < 10 && Math.abs(panelStartY - panelEndY) < 10) {
                        //TODO 处理 Click 事件
                        break;
                    }
                    //TODO 处理离开事件
                    break;
            }
            // 返回 true 拦截后续事件传递
            return true;
        }
    });
}

借助这个例子说明几点问题,这段代码主要是获取并处理 event 事件的 Y 值。其中有几点需要注意的是:

第一,onTouch 方法返回值必须为 true,表示当前触摸事件已被消费,拦截后续事件传递;

第二,click 事件。由于第一点中 onTouch 返回值为 true,view 的 click 事件自然也被拦截,那么就需要手动处理 click 事件。可以通过判断 event 偏移量来实现;

第三,一个完善的跟随手指移动的功能,通常少不了快速滑动的功能,判定滑动速度,需要用到专业的 GestureDetector 类。当然这里没有写,因为使用这个类后,相关逻辑都要搬出 onTouch 事件,到 Gesture 中处理。桥接代码类似:

private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.e("yifeng", "onFling: " + velocityX + " " + velocityY);
            if (Math.abs(velocityX) > 100 || Math.abs(velocityY) > 100) {
                //TODO 处理快速滑动事件,通常与 ACTION_UP 处理结果相同
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };
    private GestureDetector gestureDetector = new GestureDetector(gestureListener);
    
public boolean onTouch(View v, MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
}

第四,使用 getRawX 与 getRawY 方法而不是 getX 与 getY 方法。这才是我想强调的最重要的一点。前者表示获取 touch event 事件产生的屏幕坐标值,对应于屏幕坐标系;后者表示相对于父容器 View 的坐标值,对应于 View 所处父容器的位置。看似这两种坐标系函数都可以使用,计算偏差而已,实际上后者的使用会有问题。

还拿最开始那段代码示例,同一段手势向下的操作,这是 getY 的打印日志:

E/yifeng: onTouchY: 34.89081
E/yifeng: onTouchY: 88.31476
E/yifeng: onTouchY: 60.086853
E/yifeng: onTouchY: 67.28284
E/yifeng: onTouchY: 64.049866
E/yifeng: onTouchY: 69.03436
E/yifeng: onTouchY: 75.53076
E/yifeng: onTouchY: 73.07654
E/yifeng: onTouchY: 67.24097
E/yifeng: onTouchY: 66.67041
E/yifeng: onTouchY: 60.636597
E/yifeng: onTouchY: 63.618164
E/yifeng: onTouchY: 60.124634
E/yifeng: onTouchY: 58.584595
E/yifeng: onTouchY: 55.103027

这是 getRawY 的打印日志:

E/yifeng: onTouchRawY: 793.8908
E/yifeng: onTouchRawY: 854.31476
E/yifeng: onTouchRawY: 886.08685
E/yifeng: onTouchRawY: 924.28284
E/yifeng: onTouchRawY: 959.04987
E/yifeng: onTouchRawY: 998.03436
E/yifeng: onTouchRawY: 1042.5308
E/yifeng: onTouchRawY: 1084.0765
E/yifeng: onTouchRawY: 1119.241
E/yifeng: onTouchRawY: 1153.6704
E/yifeng: onTouchRawY: 1181.6366
E/yifeng: onTouchRawY: 1211.6182
E/yifeng: onTouchRawY: 1237.1246
E/yifeng: onTouchRawY: 1260.5846
E/yifeng: onTouchRawY: 1280.103
E/yifeng: onTouchRawY: 1315.1056

发现区别了吗?getY 值不稳定,相邻值前后大小变来变去;getRawY 值则是线性的一个方向上的变化,符合实际手势的操作行为。

具体反应在跟随手指移动的 View 上面,会发现使用 getY 函数产生 View 抖动特别明显,效果很差。而使用 getRawY 方法不会导致 View 抖动,比较顺畅。

所以,理论上使用 motionEvent 获取 touch 事件的坐标值,不论使用什么方法都能计算正确,但是由于 getX 与 getY 函数结果的不稳定性,只能选择基于屏幕坐标系的 getRawX 与 getRawY 函数。这是实践中特别需要的一个细节问题。

推荐阅读:1024 程序员节,是时候薅羊毛屯书了如何像 IDE 一样浏览 GitHub 网站的项目?简直是搜索引擎界的新起之秀,你值得拥有!

长按识别二维码,关注我,一名爱叨叨的程序员

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值