canvas 手势控制

作者: LiuHDme | 来源:发表于2017-11-10 19:32 被阅读46次

    基本介绍

    关于 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 中的运用

    相关文章

      网友评论

        本文标题:canvas 手势控制

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