基本介紹
關(guān)于 canvas 的基本使用,可以參考以下兩個網(wǎng)站:
Android Canvas繪圖詳解(圖文) - 泡在網(wǎng)上的日子
Android中Canvas繪圖基礎(chǔ)詳解(附源碼下載) - CSDN博客
這里主要講解如何將 canvas 實際運用到我們的項目中云头。
手勢控制
canvas 沒有提供有關(guān)手勢縮放的功能绍赛,但我們可以利用 onTouchListener 來監(jiān)測手勢,并根據(jù)手勢的不同對掃描圖作不同處理鸣个,比如移動和縮放羞反。首先,讓繪制圖形的這個類繼承一個接口 —— View.OnTouchListener囤萤,然后再實現(xiàn)該接口中的 onTouch 方法昼窗。
@Override
// 實現(xiàn)接口 View.OnTouchListener 的 onTouch 方法
public boolean onTouch(View v, MotionEvent event) {
// ...
return false;
}
只要有手指觸碰到繪制的圖形,就會觸發(fā) onTouch 方法涛舍,因此我們只要可以監(jiān)測到觸碰到圖形的手指正在進行什么動作澄惊,就可以對圖形做相應的處理。比如,如果 onTouch 監(jiān)測到有一根手指從屏幕的左邊滑到了右邊掸驱,那么說明圖形應該向右移肛搬,如果 onTouch 監(jiān)測到有兩根手指觸碰到了屏幕,并且它們的距離在不斷減小毕贼,那很顯然温赔,圖形應該被縮小∷У叮可是让腹,手指的動作這么靈活,該怎么監(jiān)測呢扣溺?下面我們就來解決這個問題骇窍。
無論是什么動作,手指肯定需要先觸碰到屏幕锥余,最后再離開屏幕腹纳,這樣才能完成一整個動作。Android 提供了一個方法來專門監(jiān)測這兩個動作以及更多的動作:
event.getAction()
<small><i>(event 是 onTouch 方法的第二個參數(shù))</i></small>
getAction()
會返回一個 int
型的值驱犹,不同的動作對應著不同的值嘲恍,比如手指按下對應 0,手指抬起對應 1 等等雄驹。當然佃牛,這么多動作和值,我們不可能全記得医舆,好在 Android 將不同的值都取了一個名字并保存在 MotionEvent 類中俘侠,比如
MotionEvent.ACTION_DOWN = 0
MotionEvent.ACTION_UP = 1
MotionEvent.MOVE = 2
...
既然這么方便,我們就可以通過 switch-case 結(jié)構(gòu)來精準監(jiān)測不同的動作了蔬将,看一下下面的代碼:
@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()
獲取到的值惫东,自動判斷執(zhí)行哪一個 case 中的代碼,即通過監(jiān)測不同的動作來對圖形作出相應處理毙石。我們的處理主要就是移動和縮放廉沮,那么下面分別介紹這兩方面該如何處理。
移動
Android 提供了兩個方法 event.getX()
和 event.getY()
徐矩,這兩個方法可以獲取到當前手指在屏幕上的坐標值滞时,那么只要將當前的坐標值減去之前的坐標值就可以得到手指在 x 和 y 方向分別移動了多少,再讓圖形移動這么多就可以了丧蘸。下面是具體步驟:
我們先在繪制圖形類中新增兩個 float 型成員變量
xDown
和yDown
漂洋,用來分別記錄手指當前的 x 坐標和 y 坐標遥皂。在
onTouch
方法中的 switch-case 結(jié)構(gòu)中的MotionEvent.ACTION_DOWN
case 中,記錄下手指剛按下時的坐標:
xDown = event.getX();
yDown = event.getY();
(只有手指剛按下去的一刻才會觸發(fā)MotionEvent.ACTION_DOWN
中的代碼)
- 在
onTouch
方法中的 switch-case 結(jié)構(gòu)中的MotionEvent.ACTION_MOVE
case 中刽漂,動態(tài)更新每次手指移動的坐標距離:
xTranslate += (event.getX() - xDown) / xScale;
xDown = event.getX();
yTranslate += (event.getY() - yDown) / yScale;
yDown = event.getY();
稍微解釋一下演训,手指每移動一小距離都會執(zhí)行以上代碼,其中 xTranslate
和 yTranslate
是用來控制圖形移動的贝咙,初始值是 0样悟,只要它們的值變化了,圖形就會移動庭猩;xScale
和 yScale
是用來控制圖形縮放的窟她,初始值是 1,只要它們的值變化了蔼水,圖形就會縮放震糖。拿 xTranslate
來說,手指每移動一小距離趴腋,都把當前手指的 x 坐標值減去移動之前的 x 坐標值吊说,然后除以當前縮放的比例,再把這個值賦給 xTranslate
优炬,這時圖形就會移動相應的距離颁井,并且移動的距離和你手指移動的距離完全相等。需要注意的是蠢护,在手指移動的過程中雅宾,需要不斷的把當前手指的 x 坐標值賦給 xDown
,即 xDown = event,getX()
葵硕,因為 event.getX()
的值始終比 xDown
先變化眉抬,這樣就能保證它們之間始終有一個微小的差值,這個差值就是圖形每次移動的那一點微小的距離贬芥,因為距離實在太小吐辙,所以整個過程看起來就是連續(xù)移動了宣决。簡而言之蘸劈,圖形的一整段移動是由無數(shù)段微小的移動組成的。
- 加上當前手指數(shù)目的判斷尊沸。因為當手指移動時威沫,可能是一根手指也可能是兩根手指,如果是兩根手指洼专,要實現(xiàn)的功能就是縮放而不是移動了棒掠,因此需要加上手指數(shù)目的判斷,這個很好完成屁商,因為 Android 提供了一個方法來獲取手指數(shù)目的方法:
event.getPointerCounter()
烟很,這個方法可以直接返回當前觸摸到屏幕的手指數(shù)目,然后通過if
語句加入到MotionEvent.ACTION_MOVE
case 中就可以了,如果返回 1雾袱,就執(zhí)行有關(guān)圖形移動的代碼恤筛,如果返回 2,就執(zhí)行有關(guān)圖形縮放的代碼芹橡。
縮放
縮放的原理也很好理解毒坛。首先,要實現(xiàn)縮放林说,一定有兩根手指觸碰到屏幕煎殷,那么,我們可以獲取當前兩根手指的距離和之前兩根手指的距離腿箩,然后算出比例豪直,這個比例就是圖形應該縮放的比例。比如之前手指間的距離是 1珠移,現(xiàn)在是 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
語句,設(shè)置最小縮放比例為 0.4赵刑。代碼如下:
else if (event.getPointerCount() == 2) {
// 實現(xiàn)掃描圖縮放
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);
// 動態(tài)更新
// 設(shè)置最小縮放比例為 0.4
if (xScale + (lenMove / lenDown - 1) > 0.4) {
xScale += (lenMove / lenDown - 1);
yScale += (lenMove / lenDown - 1);
lenDown = lenMove;
}
}
首頁折線圖和掃描圖同步移動和縮放
這個功能的目的是分衫,當折線圖或者掃描圖任何一者移動或者縮放時,另一者也要移動或縮放同樣的距離或程度般此。其中蚪战,另一者只在橫軸方向上保持同步移動牵现,并且二者縮放時均以當前圖形的中心點為縮放中心。
這個功能分為兩個部分邀桑,一個是改變折線圖的同時改變掃描圖施籍,一個是改變掃描圖的同時改變折線圖,先說簡單的概漱。
改變折線圖的同時改變掃描圖
如果上面的移動和縮放弄清楚了丑慎,那么這個功能其實不難實現(xiàn)。關(guān)鍵在于同步改變 xTranslate
和 xScale
瓤摧。
在 FragmentDataMeasure
類中竿裂,折線圖的實例是 mGraphicaView
,那么監(jiān)控折線圖的手勢照弥,當出現(xiàn)移動和縮放的手勢時腻异,同步更改掃描圖中的 xTranslate
和 xScale
,另外在注意一些細節(jié)即可这揣。這里就不在贅述了悔常。
改變掃描圖的同時改變折線圖
這個功能的困難在于,雖然繪制折線圖的庫 GraphicaView
是以 canvas 為基礎(chǔ)封裝成的给赞,但對于繪制圖形的方法机打,兩者有很大的區(qū)別,比如 canvas 在繪制圖形時是直接根據(jù)給出的像素坐標值確定位置的片迅,這個坐標值是基于屏幕自身的残邀;而 GraphicaView
是根據(jù)對應于坐標軸上的坐標值確定位置的,這個坐標值是基于用戶自己確定的坐標軸的長度的柑蛇。要解決這個問題芥挣,需要找到折線圖和掃描圖的一個共同特征作為橋梁,將兩種坐標值聯(lián)系起來耻台。
不過在研究 GraphicaView
庫后發(fā)現(xiàn)空免,GraphicaView
類中提供了兩個方法苏揣,可以分別獲取和設(shè)置當前屏幕上顯示出來的 x 軸的最小和最大坐標贸人,即圖中所示的兩個位置的坐標
有了這個方法衙猪,這個功能的實現(xiàn)就應該有思路了恰梢。我們先考慮移動時的同步。
移動時同步
我們先考慮一下折線圖和掃描圖的共同特征是什么寻咒,由于兩幅圖在 x 軸方向上都顯示的是掃描的距離仁锯,因此這個距離應該是相等的,這個距離就是共同特征匙姜。
在 ScanningService
類中,有一個 xDistance
屬性冯痢,專門用來記錄這個距離氮昧,而且框杜,xDistance
的值與折線圖中的 x 軸長度是相等的,如圖所示:
圖中折線圖的紅色箭頭之間的距離大致為 0.35袖肥,掃描圖的綠色箭頭之間的距離也大致為 0.35咪辱,而 0.35 其實就是 xDistance
的值。
當移動掃描圖時椎组,由于我們現(xiàn)在可以獲取到手指移動的距離 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()
是設(shè)置折線圖 x 軸最小和最大坐標的方法,由于圖形向右移檬嘀,屏幕同樣位置的坐標值就會減小槽驶,因此參數(shù)前帶有負號。
接下來考慮縮放時的同步鸳兽。
縮放時同步
縮放比移動復雜一點捺檬。
以下兩幅圖分別是掃描圖縮小前和縮小后的圖像
很明顯縮小后,橫軸所顯示的長度比縮小前更長了贸铜,由于縮放中心是圖形的中心點堡纬,因此左右兩邊多出的距離應該是相同的,除以二就可以得到兩邊各自多出的距離蒿秦,這個距離就是折線圖的 x 軸左右兩邊應該移動的量烤镐。
用代碼來描述就是如下形式:
(scanView.getXDistance() / scanView.getXScale() - scanView.getXDistance()) / 2
其中,getXScale()
用來獲取當前縮放的比例棍鳖,之后用縮放后的 xDistance
減去縮放前的炮叶,然后除以二就得到了折線圖 x 軸左側(cè)和右側(cè)各應該移動的距離(左側(cè)坐標減小右側(cè)坐標變大即為放大折線圖,反之則為縮小折線圖)渡处。
最后我們發(fā)現(xiàn)镜悉,其實移動和縮放折線圖的方法都是通過設(shè)置折線圖 x 軸左右兩側(cè)的坐標實現(xiàn)的,因此可以將移動和縮放的代碼加在一起医瘫。如下所示:
// 同步折線圖
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();
}