canvas 手势控制

基本介绍

关于 canvas 的基本使用,可以参考以下两个网站:

Android Canvas绘图详解(图文) - 泡在网上的日子

Android中Canvas绘图基础详解(附源码下载) - CSDN博客

这里主要讲解如何将 canvas 实际运用到我们的项目中。

手势控制

canvas 没有提供有关手势缩放的功能,但我们可以利用 onTouchListener 来监测手势,并根据手势的不同对扫描图作不同处理,比如移动和缩放。首先,让绘制图形的这个类继承一个接口 —— View.OnTouchListener,然后再实现该接口中的 onTouch 方法。

@Override
// 实现接口 View.OnTouchListener 的 onTouch 方法
public boolean onTouch(View v, MotionEvent event) {
    // ...
    return false;
}

只要有手指触碰到绘制的图形,就会触发 onTouch 方法,因此我们只要可以监测到触碰到图形的手指正在进行什么动作,就可以对图形做相应的处理。比如,如果 onTouch 监测到有一根手指从屏幕的左边滑到了右边,那么说明图形应该向右移,如果 onTouch 监测到有两根手指触碰到了屏幕,并且它们的距离在不断减小,那很显然,图形应该被缩小。可是,手指的动作这么灵活,该怎么监测呢?下面我们就来解决这个问题。

无论是什么动作,手指肯定需要先触碰到屏幕,最后再离开屏幕,这样才能完成一整个动作。Android 提供了一个方法来专门监测这两个动作以及更多的动作:

event.getAction()
<small><i>(event 是 onTouch 方法的第二个参数)</i></small>

getAction() 会返回一个 int 型的值,不同的动作对应着不同的值,比如手指按下对应 0,手指抬起对应 1 等等。当然,这么多动作和值,我们不可能全记得,好在 Android 将不同的值都取了一个名字并保存在 MotionEvent 类中,比如

MotionEvent.ACTION_DOWN = 0
MotionEvent.ACTION_UP = 1
MotionEvent.MOVE = 2
...

既然这么方便,我们就可以通过 switch-case 结构来精准监测不同的动作了,看一下下面的代码:

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
        // 手指按下
        case MotionEvent.ACTION_DOWN:
            // ...针对该动作,对图形作出处理
            break;
        // 最后一根手指抬起
        case MotionEvent.ACTION_UP:
            // ...针对该动作,对图形作出处理
            break;
        // 手指移动
        case MotionEvent.ACTION_MOVE:
            // ...针对该动作,对图形作出处理
            break;
        // ...更多的动作
        default:
            break;
    }
    return false;
}

onTouch方法 通过 event.getAction() 获取到的值,自动判断执行哪一个 case 中的代码,即通过监测不同的动作来对图形作出相应处理。我们的处理主要就是移动和缩放,那么下面分别介绍这两方面该如何处理。

移动

Android 提供了两个方法 event.getX()event.getY(),这两个方法可以获取到当前手指在屏幕上的坐标值,那么只要将当前的坐标值减去之前的坐标值就可以得到手指在 x 和 y 方向分别移动了多少,再让图形移动这么多就可以了。下面是具体步骤:

  1. 我们先在绘制图形类中新增两个 float 型成员变量 xDownyDown,用来分别记录手指当前的 x 坐标和 y 坐标。

  2. onTouch 方法中的 switch-case 结构中的 MotionEvent.ACTION_DOWN case 中,记录下手指刚按下时的坐标:

xDown = event.getX();
yDown = event.getY();

(只有手指刚按下去的一刻才会触发MotionEvent.ACTION_DOWN中的代码)

  1. onTouch 方法中的 switch-case 结构中的 MotionEvent.ACTION_MOVE case 中,动态更新每次手指移动的坐标距离:
xTranslate += (event.getX() - xDown) / xScale;
xDown = event.getX();
yTranslate += (event.getY() - yDown) / yScale;
yDown = event.getY();

稍微解释一下,手指每移动一小距离都会执行以上代码,其中 xTranslateyTranslate 是用来控制图形移动的,初始值是 0,只要它们的值变化了,图形就会移动;xScaleyScale 是用来控制图形缩放的,初始值是 1,只要它们的值变化了,图形就会缩放。拿 xTranslate 来说,手指每移动一小距离,都把当前手指的 x 坐标值减去移动之前的 x 坐标值,然后除以当前缩放的比例,再把这个值赋给 xTranslate,这时图形就会移动相应的距离,并且移动的距离和你手指移动的距离完全相等。需要注意的是,在手指移动的过程中,需要不断的把当前手指的 x 坐标值赋给 xDown,即 xDown = event,getX(),因为 event.getX() 的值始终比 xDown 先变化,这样就能保证它们之间始终有一个微小的差值,这个差值就是图形每次移动的那一点微小的距离,因为距离实在太小,所以整个过程看起来就是连续移动了。简而言之,图形的一整段移动是由无数段微小的移动组成的。

  1. 加上当前手指数目的判断。因为当手指移动时,可能是一根手指也可能是两根手指,如果是两根手指,要实现的功能就是缩放而不是移动了,因此需要加上手指数目的判断,这个很好完成,因为 Android 提供了一个方法来获取手指数目的方法:event.getPointerCounter(),这个方法可以直接返回当前触摸到屏幕的手指数目,然后通过 if 语句加入到 MotionEvent.ACTION_MOVE case 中就可以了,如果返回 1,就执行有关图形移动的代码,如果返回 2,就执行有关图形缩放的代码。

缩放

缩放的原理也很好理解。首先,要实现缩放,一定有两根手指触碰到屏幕,那么,我们可以获取当前两根手指的距离和之前两根手指的距离,然后算出比例,这个比例就是图形应该缩放的比例。比如之前手指间的距离是 1,现在是 2,那么图形应该被放大 \(\frac{2}{1}\) 即 2 倍。

下面来看具体步骤:

  1. 我们先要获取两根手指触碰到屏幕时它们之间的距离。之前提到过,手指的每一个动作都对应着一个 int 型的值,两根手指触碰到屏幕这个动作对应的值是 261。然后我们可以通过 event.getX(0)event.getX(1) 分别获取两根手指的坐标,然后相减即可得到两根手指在 x 轴方向的距离,同样的方法也能得到 y 轴方向的距离,然后这两个距离平方相加即可得到两根手指之间的距离,代码如下:
case 261:
    double xLenDown = Math.abs(event.getX(0) - event.getX(1));
    double yLenDown = Math.abs(event.getY(0) - event.getY(1));
    lenDown = Math.sqrt(xLenDown * xLenDown + yLenDown * yLenDown);
    break;
  1. 每次移动手指,都记录下当前手指间的距离,然后除以上次移动时手指间的距离,再减去 1,就得到了这次移动后图形应该缩放的比例,如果大于 0,图形就会放大,否则就会缩小,并且为了不让图形缩小到消失,加入一条 if 语句,设置最小缩放比例为 0.4。代码如下:
else if (event.getPointerCount() == 2) {
    // 实现扫描图缩放
    double xLenMove = Math.abs(event.getX(0) - event.getX(1));
    double yLenMove = Math.abs(event.getY(0) - event.getY(1));
    double lenMove = Math.sqrt(xLenMove * xLenMove + yLenMove * yLenMove);
    // 动态更新
    // 设置最小缩放比例为 0.4
    if (xScale + (lenMove / lenDown - 1) > 0.4) {
        xScale += (lenMove / lenDown - 1);
        yScale += (lenMove / lenDown - 1);
        lenDown = lenMove;
    }
}

首页折线图和扫描图同步移动和缩放

这个功能的目的是,当折线图或者扫描图任何一者移动或者缩放时,另一者也要移动或缩放同样的距离或程度。其中,另一者只在横轴方向上保持同步移动,并且二者缩放时均以当前图形的中心点为缩放中心。

这个功能分为两个部分,一个是改变折线图的同时改变扫描图,一个是改变扫描图的同时改变折线图,先说简单的。

改变折线图的同时改变扫描图

如果上面的移动和缩放弄清楚了,那么这个功能其实不难实现。关键在于同步改变 xTranslatexScale

FragmentDataMeasure 类中,折线图的实例是 mGraphicaView,那么监控折线图的手势,当出现移动和缩放的手势时,同步更改扫描图中的 xTranslatexScale ,另外在注意一些细节即可。这里就不在赘述了。

改变扫描图的同时改变折线图

这个功能的困难在于,虽然绘制折线图的库 GraphicaView 是以 canvas 为基础封装成的,但对于绘制图形的方法,两者有很大的区别,比如 canvas 在绘制图形时是直接根据给出的像素坐标值确定位置的,这个坐标值是基于屏幕自身的;而 GraphicaView 是根据对应于坐标轴上的坐标值确定位置的,这个坐标值是基于用户自己确定的坐标轴的长度的。要解决这个问题,需要找到折线图和扫描图的一个共同特征作为桥梁,将两种坐标值联系起来。

不过在研究 GraphicaView 库后发现,GraphicaView 类中提供了两个方法,可以分别获取和设置当前屏幕上显示出来的 x 轴的最小和最大坐标,即图中所示的两个位置的坐标

image

有了这个方法,这个功能的实现就应该有思路了。我们先考虑移动时的同步。

移动时同步

我们先考虑一下折线图和扫描图的共同特征是什么,由于两幅图在 x 轴方向上都显示的是扫描的距离,因此这个距离应该是相等的,这个距离就是共同特征。

ScanningService 类中,有一个 xDistance 属性,专门用来记录这个距离,而且,xDistance 的值与折线图中的 x 轴长度是相等的,如图所示:

image

图中折线图的红色箭头之间的距离大致为 0.35,扫描图的绿色箭头之间的距离也大致为 0.35,而 0.35 其实就是 xDistance 的值。

当移动扫描图时,由于我们现在可以获取到手指移动的距离 xDistance(注意这个距离是基于屏幕坐标系的,而不是折线图的坐标系),那么只要知道扫描图的 x 轴方向的总距离 width(基于屏幕坐标系),然后让 xDistance 除以 width,就得到了移动距离占总距离的比例,最后让这个比例乘以 xDistance,就得到了基于折线图坐标系的距离。Android 正好提供了一个方法 canvas.getWidth() 用来获取 x 轴方向的距离,因此三个值都有了,那么折线图移动的距离就可以算出来了,代码如下:

// 同步折线图
public void syncGraphicalView(double xTrans) {
    // 更新折线图
    FragmentDataMeasure.getInstance().mService.getMultipleSeriesRenderer()
                .setXAxisMin(-xTrans);
    FragmentDataMeasure.getInstance().mService.getMultipleSeriesRenderer()
                .setXAxisMax(scanView.getXDistance() - xTrans);
    // 重绘折线图
    FragmentDataMeasure.getInstance().mGraphicalView.repaint();
}

其中 setXAxisMin()setXAxisMax() 是设置折线图 x 轴最小和最大坐标的方法,由于图形向右移,屏幕同样位置的坐标值就会减小,因此参数前带有负号。

接下来考虑缩放时的同步。

缩放时同步

缩放比移动复杂一点。

以下两幅图分别是扫描图缩小前和缩小后的图像

image
image

很明显缩小后,横轴所显示的长度比缩小前更长了,由于缩放中心是图形的中心点,因此左右两边多出的距离应该是相同的,除以二就可以得到两边各自多出的距离,这个距离就是折线图的 x 轴左右两边应该移动的量。

用代码来描述就是如下形式:

(scanView.getXDistance() / scanView.getXScale() - scanView.getXDistance()) / 2

其中,getXScale() 用来获取当前缩放的比例,之后用缩放后的 xDistance 减去缩放前的,然后除以二就得到了折线图 x 轴左侧和右侧各应该移动的距离(左侧坐标减小右侧坐标变大即为放大折线图,反之则为缩小折线图)。

最后我们发现,其实移动和缩放折线图的方法都是通过设置折线图 x 轴左右两侧的坐标实现的,因此可以将移动和缩放的代码加在一起。如下所示:

// 同步折线图
public void syncGraphicalView(double xTrans) {
    // 更新折线图
    FragmentDataMeasure.getInstance().mService.getMultipleSeriesRenderer()
                .setXAxisMin(-xTrans -
                        (scanView.getXDistance() / scanView.getXScale() - scanView.getXDistance()) / 2);
    FragmentDataMeasure.getInstance().mService.getMultipleSeriesRenderer()
                .setXAxisMax(scanView.getXDistance() - xTrans +
                        (scanView.getXDistance() / scanView.getXScale() - scanView.getXDistance()) / 2);
    // 重绘折线图
    FragmentDataMeasure.getInstance().mGraphicalView.repaint();
}

原文地址:Canvas 在 LCJCSys 中的运用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,022评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,499评论 25 707
  • 倚窗而望 雨丝牵线而下 秋意凉然 不觉打了寒颤 端起一杯菊花冰糖茶 苦涩带着甜甜的味道渗入舌下 多日的身体不适 让...
    田萍阅读 322评论 0 7
  • 【谈情说爱专题周刊稿】 西伯利亚的寒风低声嘶吼,哐哐地推搡着店门。眼前一碗金针菇酱卤的豆腐脑,袅袅升腾着热气。纤薄...
    婴儿看世界阅读 757评论 17 16
  • 我和爱人相伴十余载,竟如双生般有了心灵感应。好多次,我正给他拨打电话,电话铃突然响了,吓得我差点把电话摞了,还好我...
    清风徐徐霞笑江湖阅读 387评论 4 6