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

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


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 源码解析,这一篇就够了的更多相关文章

  1. jQuery2.x源码解析(DOM操作篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) jQuery这个类库最为核心重要的功能就是DOM ...

  2. Apollo源码解析看一文就够

    对于配置中心我们先抛出问号三连,什么是配置中心?为什么要用配置中心?配置中心怎么用? 笔者说说自己理解的配置中心,个人观点的十六字 消息存储 消息推送 环境隔离 灰度发布 今天我们先来看Apollo配 ...

  3. dubbo源码解析-spi(一)

    前言 虽然标题是dubbo源码解析,但是本篇并不会出现dubbo的源码,本篇和之前的dubbo源码解析-简单原理.与spring融合一样,为dubbo源码解析专题的知识预热篇. 插播面试题 你是否了解 ...

  4. Spring源码解析系列汇总

    相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...

  5. Mybatis源码解析3——核心类SqlSessionFactory,看完我悟了

    这是昨晚的武汉,晚上九点钟拍的,疫情又一次来袭,曾经熙熙攘攘的夜市也变得冷冷清清,但比前几周要好很多了.希望大家都能保护好自己,保护好身边的人,生活不可能像你想象的那么好,但也不会像你想象的那么糟. ...

  6. ViewPagerindicator 源码解析

        ViewPagerindicator 源码解析   1. 功能介绍 1.1 ViewPagerIndicator ViewPagerIndicator用于各种基于AndroidSupportL ...

  7. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  8. [原创]android开源项目源码解析(一)----CircleImageView的源码解析

    CircleImageView的代码很简洁,因此先将此工程作为源码解析系列的第一篇文章. 解析说明都在代码里了. /* * Copyright 2014 - 2015 Henning Dodenhof ...

  9. vue UI库iview源码解析(2)

    上篇问题 在上篇<iview源码解析(1)>中的index.js 入口文件的源码中有一段代码有点疑惑: /** * 在浏览器环境下默认加载组件 */ // auto install if ...

随机推荐

  1. Vim 写 iOS App

    Vim 写 iOS App 我们都知道 Vim 和 Emacs 都是文本编辑器中的上古神器,你也许用 ctags,cscopes 配合 Vim 完成过大型 C 或者 C++ 的开发,你也许配合过其他插 ...

  2. wpf控件开发基础(3) -属性系统(2)

    原文:wpf控件开发基础(3) -属性系统(2) 上篇说明了属性存在的一系列问题. 属性默认值,可以保证属性的有效性. 属性验证有效性,可以对输入的属性进行校验 属性强制回调, 即不管属性有无发生变化 ...

  3. 新世界PT850/PT853检查用友机对接T1交易宝

    新世界PT850/PT853检查用友机对接T1交易宝 这是一个老话题.简单.条形码号码模式是没有必要说,我要说的是,,启用条码扫描.显示有关信息. 并能够产生:条码 , 数量,价格格 这样做的目的.将 ...

  4. Method of Seamless Integration and Independent Evolution of Information-Centric Networking via Software Defined Networking

    A method of transferring data between a software defined network (SDN) and an information-centric ne ...

  5. XMLHttpRequest 请求java部署的webservice 跨域问题

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <hea ...

  6. Windows下程序打包发布时的小技巧(使用Dependency Walker侦测不理想,改用VS自带的dumpbin则万无一失,还可查看dll导出的函数)

    Windows下开发的应用程序在发布时,需要将其依赖的一些动态链接库一起打进安装包里面去.这个时候,快速确定这个程序到底依赖哪些动态链接库变得非常重要.很久以前写过一篇关于Qt程序安装包制作的博客,里 ...

  7. blockchain_eth客户端安装 & geth使用 &批量转账(二)

    回顾一下,前面我们讲到启动geth geth --rpc --datadir "F:/geth/Geth/" --light console 2>console.log 这一 ...

  8. linux没有 conio.h解决的方式

    conio.h不是C标准库中的头文件,在ISO和POSIX标准中均未定义. conio是Console Input/Output(控制台输入输出)的简写,当中定义了通过控制台进行数据输入和数据输出的函 ...

  9. HTTP协议入门(一)- 版本

    当我们在浏览器的地址栏输入URL后,信息会被发送到WEB服务器,服务器得到响应,将数据传输回来,展示到WEB页面上,这其中的传输方法就是HTTP协议. 一.HTTP 0.9 发布于1991年,是首个H ...

  10. Python杂谈: __init__.py的作用

    我们经常在python的模块目录中会看到 "__init__.py"  这个文件,那么它到底有什么作用呢? 1. 标识该目录是一个python的模块包(module package ...