基本介绍
关于 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 方向分别移动了多少,再让图形移动这么多就可以了。下面是具体步骤:
我们先在绘制图形类中新增两个 float 型成员变量
xDown
和yDown
,用来分别记录手指当前的 x 坐标和 y 坐标。在
onTouch
方法中的 switch-case 结构中的MotionEvent.ACTION_DOWN
case 中,记录下手指刚按下时的坐标:
xDown = event.getX();
yDown = event.getY();
(只有手指刚按下去的一刻才会触发MotionEvent.ACTION_DOWN
中的代码)
- 在
onTouch
方法中的 switch-case 结构中的MotionEvent.ACTION_MOVE
case 中,动态更新每次手指移动的坐标距离:
xTranslate += (event.getX() - xDown) / xScale;
xDown = event.getX();
yTranslate += (event.getY() - yDown) / yScale;
yDown = event.getY();
稍微解释一下,手指每移动一小距离都会执行以上代码,其中 xTranslate
和 yTranslate
是用来控制图形移动的,初始值是 0,只要它们的值变化了,图形就会移动;xScale
和 yScale
是用来控制图形缩放的,初始值是 1,只要它们的值变化了,图形就会缩放。拿 xTranslate
来说,手指每移动一小距离,都把当前手指的 x 坐标值减去移动之前的 x 坐标值,然后除以当前缩放的比例,再把这个值赋给 xTranslate
,这时图形就会移动相应的距离,并且移动的距离和你手指移动的距离完全相等。需要注意的是,在手指移动的过程中,需要不断的把当前手指的 x 坐标值赋给 xDown
,即 xDown = event,getX()
,因为 event.getX()
的值始终比 xDown
先变化,这样就能保证它们之间始终有一个微小的差值,这个差值就是图形每次移动的那一点微小的距离,因为距离实在太小,所以整个过程看起来就是连续移动了。简而言之,图形的一整段移动是由无数段微小的移动组成的。
- 加上当前手指数目的判断。因为当手指移动时,可能是一根手指也可能是两根手指,如果是两根手指,要实现的功能就是缩放而不是移动了,因此需要加上手指数目的判断,这个很好完成,因为 Android 提供了一个方法来获取手指数目的方法:
event.getPointerCounter()
,这个方法可以直接返回当前触摸到屏幕的手指数目,然后通过if
语句加入到MotionEvent.ACTION_MOVE
case 中就可以了,如果返回 1,就执行有关图形移动的代码,如果返回 2,就执行有关图形缩放的代码。
缩放
缩放的原理也很好理解。首先,要实现缩放,一定有两根手指触碰到屏幕,那么,我们可以获取当前两根手指的距离和之前两根手指的距离,然后算出比例,这个比例就是图形应该缩放的比例。比如之前手指间的距离是 1,现在是 2,那么图形应该被放大 \(\frac{2}{1}\) 即 2 倍。
下面来看具体步骤:
- 我们先要获取两根手指触碰到屏幕时它们之间的距离。之前提到过,手指的每一个动作都对应着一个
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,就得到了这次移动后图形应该缩放的比例,如果大于 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;
}
}
首页折线图和扫描图同步移动和缩放
这个功能的目的是,当折线图或者扫描图任何一者移动或者缩放时,另一者也要移动或缩放同样的距离或程度。其中,另一者只在横轴方向上保持同步移动,并且二者缩放时均以当前图形的中心点为缩放中心。
这个功能分为两个部分,一个是改变折线图的同时改变扫描图,一个是改变扫描图的同时改变折线图,先说简单的。
改变折线图的同时改变扫描图
如果上面的移动和缩放弄清楚了,那么这个功能其实不难实现。关键在于同步改变 xTranslate
和 xScale
。
在 FragmentDataMeasure
类中,折线图的实例是 mGraphicaView
,那么监控折线图的手势,当出现移动和缩放的手势时,同步更改扫描图中的 xTranslate
和 xScale
,另外在注意一些细节即可。这里就不在赘述了。
改变扫描图的同时改变折线图
这个功能的困难在于,虽然绘制折线图的库 GraphicaView
是以 canvas 为基础封装成的,但对于绘制图形的方法,两者有很大的区别,比如 canvas 在绘制图形时是直接根据给出的像素坐标值确定位置的,这个坐标值是基于屏幕自身的;而 GraphicaView
是根据对应于坐标轴上的坐标值确定位置的,这个坐标值是基于用户自己确定的坐标轴的长度的。要解决这个问题,需要找到折线图和扫描图的一个共同特征作为桥梁,将两种坐标值联系起来。
不过在研究 GraphicaView
库后发现,GraphicaView
类中提供了两个方法,可以分别获取和设置当前屏幕上显示出来的 x 轴的最小和最大坐标,即图中所示的两个位置的坐标
有了这个方法,这个功能的实现就应该有思路了。我们先考虑移动时的同步。
移动时同步
我们先考虑一下折线图和扫描图的共同特征是什么,由于两幅图在 x 轴方向上都显示的是扫描的距离,因此这个距离应该是相等的,这个距离就是共同特征。
在 ScanningService
类中,有一个 xDistance
属性,专门用来记录这个距离,而且,xDistance
的值与折线图中的 x 轴长度是相等的,如图所示:
图中折线图的红色箭头之间的距离大致为 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 轴最小和最大坐标的方法,由于图形向右移,屏幕同样位置的坐标值就会减小,因此参数前带有负号。
接下来考虑缩放时的同步。
缩放时同步
缩放比移动复杂一点。
以下两幅图分别是扫描图缩小前和缩小后的图像
很明显缩小后,横轴所显示的长度比缩小前更长了,由于缩放中心是图形的中心点,因此左右两边多出的距离应该是相同的,除以二就可以得到两边各自多出的距离,这个距离就是折线图的 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();
}