缩放手势 ScaleGestureDetector 源码解析,这一篇就够了

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

本文首先将简单的介绍下 ScaleGestureDetector 的使用,在重点给你们分析下源码(因为源码方面是我本身的理解,可能有误差,但愿各位大佬能在评论区指出,万分感谢~)编程


ScaleGestureDetector 使用

ScaleGestureDetector 包括一个监听器,以及它全部方法的空实现:app

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

ScaleGestureDetector 方法

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

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

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

这个主要是因为判断手写笔是否按下
因为咱们不多处理手写笔,因此这里不作过多说明ui

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

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

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

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;
           }
       }

### anchoredScaleCancelledspa

  • @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;
            }

结语

我要讲的全部内容,到这里就彻底结束了

因为源码是按照我本身的理解来说的,因此不免会有一些出入

但愿你们能在评论区中帮我指出,谢谢~ 🙏

相关文章
相关标签/搜索