美文网首页Android开发经验谈Android开发Android技术知识
缩放手势 ScaleGestureDetector 源码解析,

缩放手势 ScaleGestureDetector 源码解析,

作者: 圆号本昊 | 来源:发表于2019-06-28 15:10 被阅读7次

    其实在我们日常的编程中,对于缩放手势的使用并不是很经常,这一手势主要是用在图片浏览方面,比如下方例子。但是(敲重点),作为 Android 入门的基础来说,学习 ScaleGestureDetector 的使用,算是不得不过的一道坎,好在 ScaleGestureDetector 使用起来非常简单,就是源码分析上得花些功夫。

    本文首先将简单的介绍下 ScaleGestureDetector 的使用,在重点给大家分析下源码(由于源码方面是我自己的理解,可能有偏差,希望各位大佬能在评论区指出,万分感谢~)

    <img src="https://user-gold-cdn.xitu.io/2019/6/27/16b996688722ec94?w=1280&h=904&f=jpeg&s=235091" width="500" height="300" align=center />


    ScaleGestureDetector 使用

    ScaleGestureDetector 包括一个监听器,以及它所有方法的空实现:

    名称 用途
    ScaleGestureDetector 缩放手势的监听器
    SimpleOnScaleGestureListener 该监听器的空实现,在其中重写方法

    ScaleGestureDetector 方法

    名称 用途
    onScaleBegin 当 >= 2 个手指碰触屏幕时调用,若返回 false 则忽略改事件调用
    onScale 滑动(缩放)过程中调用,若成功处理,则用户返回 true,监听器继续记录下一个缩放等动作,若为 false 表明数据未处理,则监听器继续积累
    onScaleEnd 全部手指离开屏幕,结束监听

    通常情况下,手势监听会结合自定义 View 来讲,这里我给出一个最简单的使用,具体的使用实例,以后再结合自定义 View 讲讲。

        private void iniScaleGestureListener(){
            mListener = new ScaleGestureDetector.SimpleOnScaleGestureListener(){
                @Override
                public boolean onScaleBegin(ScaleGestureDetector detector) {
                    return super.onScaleBegin(detector);
                }
    
                @Override
                public boolean onScale(ScaleGestureDetector detector) {
                    MyLog.d("X:" + detector.getFocusX());
                    MyLog.d("Y:" + detector.getFocusY());
                    MyLog.d("scale:" + detector.getScaleFactor());
                    return super.onScale(detector);
                }
    
                @Override
                public void onScaleEnd(ScaleGestureDetector detector) {
                    super.onScaleEnd(detector);
                }
            };
    
            detector = new ScaleGestureDetector(getContext(), mListener);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            detector.onTouchEvent(event);
            return true;
        }
    

    ScaleGestureDetector 的使用

    ScaleGestureDetector 在具体项目的使用有点复杂,我打算过段时间结合自定义 View 写一篇用来总结,所以这篇我们就先了解下 ScaleGestureDetector 的基本使用。


    ScaleGestureDetector 源码分析

    好了,现在我们进入本章重点,ScaleGestureDetector 源码分析,敲黑板敲黑板。首先,我们打开 ScaleGestureDetector 的源码可以看到,几乎所有的代码都集中在了 onTouchEvent 这个方法上,所以在这里,我就主要给大家介绍这个方法的实现。

    第一部分:前期准备

            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    
            mCurrTime = event.getEventTime();
    
            final int action = event.getActionMasked();
    
            // Forward the event to check for double tap gesture
            if (mQuickScaleEnabled) {
                mGestureDetector.onTouchEvent(event);
            }
    
            final int count = event.getPointerCount();
            final boolean isStylusButtonDown =
                    (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
    

    mInputEventConsistencyVerifier

    • 输入事件一致性验证器 @有道
    • 根据名字以及前面的定义
    • 我们可以猜测这个对象应该是手势监听 Event 是否注册(连接到硬件)
    • 所以,如果他为空,那么我们在这里调用 onTouchEvent 进行注册
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    

    mCurrTime

    • 获得事件发生时的时间
            mCurrTime = event.getEventTime();
    

    action

    • 获得事件类型
            final int action = event.getActionMasked();
    

    mQuickScaleEnabled

    • Forward the event to check for double tap gesture
    • @有道 转发事件以检查双击手势
    • 首先是 mQuickScaleEnabled 这个对象
    • 翻译过来是: @有道 启用快速扩展
    • 作用大概就是调用双击监听事件,比如双击最大化
            if (mQuickScaleEnabled) {
                mGestureDetector.onTouchEvent(event);
            }
    

    count

    • 获得屏幕上手指的数目
            final int count = event.getPointerCount();
    

    isStylusButtonDown

    这个主要是由于判断手写笔是否按下
    由于我们很少处理手写笔,所以这里不做过多说明

            final boolean isStylusButtonDown =
                   (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
    

    第二部分:处理与手势变化

    用户的缩放手势不总是一定的,就是说对于用户而言,随时可能有手指碰触或离开屏幕,这就使得缩放中心的(焦点)随时可能发生变化,这部分主要是用来处理这一变化,并做出响应。

            final boolean anchoredScaleCancelled =
                   mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
           
           final boolean streamComplete = action == MotionEvent.ACTION_UP ||
                   action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
    
           // 如果发生了上面这种小动作,或者说有一手指离开了屏幕,进行调用
           if (action == MotionEvent.ACTION_DOWN || streamComplete) {
               // Reset any scale in progress with the listener.
               // If it's an ACTION_DOWN we're beginning a new event stream.
               // This means the app probably didn't give us all the events. Shame on it.
    
               if (mInProgress) {
                   mListener.onScaleEnd(this);
                   mInProgress = false;
                   mInitialSpan = 0;
                   mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
               } else if (inAnchoredScaleMode() && streamComplete) {
                   mInProgress = false;
                   mInitialSpan = 0;
                   mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
               }
    
               if (streamComplete) {
                   return true;
               }
           }
    

    anchoredScaleCancelled

    • @Google 锚定规模取消
    • 我的理解是:用于判断滑动事件是否被取消
            final boolean anchoredScaleCancelled =
                    mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
    

    streamComplete

    • @Google Translate: 流完成
    • 我的理解是,这个布尔变量用于标记
    • 当前动作是否完成
    • 我这里说的动作有两种
    • 这里指的是:在大动作如三指触屏放大过程中,又一个手指离开了屏幕这种
    • 在大动作三指触屏中发生的一个小动作,离开一指
            final boolean streamComplete = action == MotionEvent.ACTION_UP ||
                    action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
    

    action == MotionEvent.ACTION_DOWN || streamComplete

    • 如果发生了上面这种小动作,或者说有一手指离开了屏幕,就进行调用
    if (action == MotionEvent.ACTION_DOWN || streamComplete) {...}
    

    if (mInProgress)

    • @google Translate:重置侦听器正在进行的任何缩放。
    • 如果是ACTION_DOWN,我们开始一个新的事件流。
    • 这意味着应用程序可能没有给我们所有的事件。很遗憾。
    • 首先判断该进程(从第一个手指碰上屏幕,到最后一个手指离开屏幕为止)是否结束
    • 如果仍在运行中,这调用回调方法:onScaleEnd 使其结束
                if (mInProgress) {
                    mListener.onScaleEnd(this);
                    mInProgress = false;
                    mInitialSpan = 0;
                    mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
                }
    

    else if (inAnchoredScaleMode() && streamComplete)

    • 如果当前进程已经结束
    • 判断 mAnchoredScaleMode 是否为 ANCHORED_SCALE_MODE_STYLUS 状态
    • 同时判断操作流 streamComplete 是否完成
    • 都符合的情况下结束这一手势变化
                else if (inAnchoredScaleMode() && streamComplete) {
                    mInProgress = false;
                    mInitialSpan = 0;
                    mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
                }
    

    if (streamComplete)

    • 结束本次 onTouchEvent 方法的调用,等待下一次调用发生
                if (streamComplete) {
                    return true;
                }
    

    总结: 可以看到,当触发 down 或者触发 up,cancel 时,如果之前处于缩放计算的状态,会将其状态重置, 并调用 onScaleEnd 方法。


    进入锚定比例模式

    • 当判断用户动作,如果为双击这类点击事件,进入该模式
    • 与正常缩放区分。这个模式功能一般是:双击最大化和最小化
            if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
                    && !streamComplete && isStylusButtonDown) {
                // Start of a button scale gesture
                mAnchoredScaleStartX = event.getX();
                mAnchoredScaleStartY = event.getY();
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
                mInitialSpan = 0;
            }
    

    mAnchoredScaleStartX & mAnchoredScaleStartY

    • 后文中将用于重新计算焦点
                mAnchoredScaleStartX = event.getX();
                mAnchoredScaleStartY = event.getY();
    

    mAnchoredScaleMode

    • 赋值之后,再次调用 inAnchoredScaleMode() 方法,返回值变为 true
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
    

    计算缩放中心

            final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
                    action == MotionEvent.ACTION_POINTER_UP ||
                    action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
    
            final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
            final int skipIndex = pointerUp ? event.getActionIndex() : -1;
    
            // Determine focal point
            float sumX = 0, sumY = 0;
            final int div = pointerUp ? count - 1 : count;
            final float focusX;
            final float focusY;
            if (inAnchoredScaleMode()) {
                // In anchored scale mode, the focal pt is always where the double tap
                // or button down gesture started
                focusX = mAnchoredScaleStartX;
                focusY = mAnchoredScaleStartY;
                if (event.getY() < focusY) {
                    mEventBeforeOrAboveStartingGestureEvent = true;
                } else {
                    mEventBeforeOrAboveStartingGestureEvent = false;
                }
            } else {
                for (int i = 0; i < count; i++) {
                    if (skipIndex == i) continue;
                    sumX += event.getX(i);
                    sumY += event.getY(i);
                }
    
                focusX = sumX / div;
                focusY = sumY / div;
            }
    

    configChanged

    • 布尔类型量,标志着一个操作的完成或者结束(手指离开,手指按下)
            final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
                    action == MotionEvent.ACTION_POINTER_UP ||
                    action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
    

    pointerUp

    • 布尔类型量,用于判断当前动作,是否为手指离开(抬起动作)
            final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
    

    skipIndex

    • 标记量,在是手指离开的情况下,标记离开手指
    • 在后面计算新的焦点代码中,跳过该手指的标记点坐标,进行计算
            final int skipIndex = pointerUp ? event.getActionIndex() : -1;
    

    初始化计算所需临时变量

            // Determine focal point
            float sumX = 0, sumY = 0;
            // 如果是抬起手指,则当前手指数减1,否则不变
            final int div = pointerUp ? count - 1 : count;
            final float focusX;
            final float focusY;
    

    判断是否为锚定比例模式

    • 是的话直接将点击时记下的点,作为焦点
    • 不是的话,把所有点累加求和,除以总个数,计算平均值
            if (inAnchoredScaleMode()) {
                // In anchored scale mode, the focal pt is always where the double tap
                // or button down gesture started
                // 在锚定比例模式中,焦点pt始终是双击的位置,或按下手势开始
                focusX = mAnchoredScaleStartX;
                focusY = mAnchoredScaleStartY;
                if (event.getY() < focusY) {
                    mEventBeforeOrAboveStartingGestureEvent = true;
                } else {
                    mEventBeforeOrAboveStartingGestureEvent = false;
                }
            } else {
                for (int i = 0; i < count; i++) {
                    if (skipIndex == i) continue;
                    sumX += event.getX(i);
                    sumY += event.getY(i);
                }
    
                focusX = sumX / div;
                focusY = sumY / div;
            }
    

    算缩放比例

    • 计算缩放比例也很简单,就是计算各个手指到焦点的平均距离,在用户手指移动后用新的平均距离除以旧的平均距离,并以此计算得出缩放比例。
            // Determine average deviation from focal point @Google translate 
            float devSumX = 0, devSumY = 0;
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) continue;
    
                // Convert the resulting diameter into a radius.
                devSumX += Math.abs(event.getX(i) - focusX);
                devSumY += Math.abs(event.getY(i) - focusY);
            }
            final float devX = devSumX / div;
            final float devY = devSumY / div;
    
            // Span is the average distance between touch points through the focal point;
            // i.e. the diameter of the circle with a radius of the average deviation from
            // the focal point.
            final float spanX = devX * 2;
            final float spanY = devY * 2;
            final float span;
            if (inAnchoredScaleMode()) {
                span = spanY;
            } else {
                span = (float) Math.hypot(spanX, spanY);
            }
    

    计算平均偏差

    • 确定焦点的平均偏差
            float devSumX = 0, devSumY = 0;
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) continue;
    
                // Convert the resulting diameter into a radius.
                devSumX += Math.abs(event.getX(i) - focusX);
                devSumY += Math.abs(event.getY(i) - focusY);
            }
            final float devX = devSumX / div;
            final float devY = devSumY / div;
    

    计算缩放比例

    • 跨度是通过焦点的触摸点之间的平均距离;
    • 即圆的直径,其半径为平均偏差
    • 这里的 Math.hypot(spanX, spanY) 方法,相当于 sqrt(xx + yy)
            final float spanX = devX * 2;
            final float spanY = devY * 2;
            final float span;
            if (inAnchoredScaleMode()) {
                span = spanY;
            } else {
                span = (float) Math.hypot(spanX, spanY);
            }
    

    结束缩放事件

    • @Google Translate:根据需要调度开始/结束事件。
    • 如果配置发生更改,请通过开始通知应用重置其当前状态
    • 一个新的比例事件流。
    • 这里就不做太多描述,主要就是:
    • 判断是不是所有手指都离开了屏幕
    • 如果是,那么索命这个缩放进程结束了
    • 则保存当前缩放的数据
    • 调用 onScaleEnd 方法,结束当前操作
            final boolean wasInProgress = mInProgress;
            mFocusX = focusX;
            mFocusY = focusY;
            if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
                mListener.onScaleEnd(this);
                mInProgress = false;
                mInitialSpan = span;
            }
            if (configChanged) {
                mPrevSpanX = mCurrSpanX = spanX;
                mPrevSpanY = mCurrSpanY = spanY;
                mInitialSpan = mPrevSpan = mCurrSpan = span;
            }
    

    触发 onScaleBegin 开始缩放

    • 当手指移动的距离超过一定数值(数值大小由系统定义)后,会触发 onScaleBegin 方法
    • 如果用户在 onScaleBegin 方法里面返回了 true,则接受事件后,就会重置缩放相关数值,并且开始积累缩放因子。
            final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
            if (!mInProgress && span >= minSpan &&
                    (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
                mPrevSpanX = mCurrSpanX = spanX;
                mPrevSpanY = mCurrSpanY = spanY;
                mPrevSpan = mCurrSpan = span;
                mPrevTime = mCurrTime;
                mInProgress = mListener.onScaleBegin(this);
            }
    

    通知用户进行缩放处理

    • @ Google Translate: 处理动作;焦点和跨度/比例因子正在发生变化。
    • 这块代码的功能主要就是通知用户(编程者)
    • 根据这些数据进行缩放
            if (action == MotionEvent.ACTION_MOVE) {
                mCurrSpanX = spanX;
                mCurrSpanY = spanY;
                mCurrSpan = span;
    
                boolean updatePrev = true;
    
                if (mInProgress) {
                    updatePrev = mListener.onScale(this);
                }
    
                if (updatePrev) {
                    mPrevSpanX = mCurrSpanX;
                    mPrevSpanY = mCurrSpanY;
                    mPrevSpan = mCurrSpan;
                    mPrevTime = mCurrTime;
                }
            }
    

    updatePrev

    • 这个用于接收用户的返回值
    • 只要我们放回 true ,系统就会保存当前数据
    • 重新获取并计算新的数据和比例
    • 系统默认返回 false 然后进行下一次事件的计算
                if (mInProgress) {
                    updatePrev = mListener.onScale(this);
                }
    
                if (updatePrev) {
                    mPrevSpanX = mCurrSpanX;
                    mPrevSpanY = mCurrSpanY;
                    mPrevSpan = mCurrSpan;
                    mPrevTime = mCurrTime;
                }
    

    结语

    我要讲的所有内容,到这里就完全结束了

    由于源码是按照我自己的理解来讲的,所以难免会有一些出入

    希望大家能在评论区中帮我指出,谢谢~ 🙏

    相关文章

      网友评论

        本文标题:缩放手势 ScaleGestureDetector 源码解析,

        本文链接:https://www.haomeiwen.com/subject/iftecctx.html