? ? ? ?本文介紹自己寫的一個處理手勢的工具類ViewTransHelper,可以方便的放在自己的圖表中來處理手勢。用內置封裝好的InsetTransView或者OutsetTransHelper更加簡單,解耦,不像第三方庫里面圖表的手勢部分與其它部分耦合。ViewTransHelper適合自定義圖表,或者查看圖片,支持tap、double tap、多點scale、drag、fling。后面有效果圖。
? ? ? ?項目地址:https://github.com/fornana/ViewTransHelper
? ? ? ?開發中碰到圖表的情況還是很多的,但是一般比較尷尬的是,項目里面只需要折線圖或者柱狀圖,需求簡單。如果是餅圖還好,相對簡單一些。折線圖和柱狀圖就不好辦了。用第三方庫例如MPAndroidChart,稍微大了點。而且如果細節部分比較特別,需要根據MPAndroidChart來定制,也不是那么方便的。也就是說,ui設計的圖表類型單一,但是效果上又稍有特色的時候。直接使用MPAndroidChart,第一是大了點,第二是細節部分不好處理。
? ? ? ?這個時候選擇自定義怎么樣?常見的做法是直接套一層HorizontalScrollView,然后一次性把數據全部繪制。兩個問題:一次性繪制所有數據,但是只顯示部分,效率低;手勢單一,不能縮放。所以,要想自定義稍微好點,就必須自己處理手勢,簡單點就只處理左右滑動。難點就只能使用第三方庫了。第三方庫里面的手勢部分往往與其具體圖表關聯太深,不好提取出來放在自己的圖表里使用。所以,寫了ViewTransHelper,還有兩個對ViewTransHelper的封裝,InsetTransView和OutsetTransHelper。它們都與圖表沒有關聯,只處理手勢部分。
? ? ? ?繼續說自定義圖表,首先要面對手勢判斷,然后是圖表繪制,而繪制是需要坐標的,特別是計算滑動縮放以后應該繪制哪部分數據,剩下的怎么封裝數據之類的比較好處理。而且如果只是應付一般的小項目里面的圖表,在不用考慮手勢的情況下處理起來就更簡單了。歸納一下,自定義圖表主要難點:手勢判斷、坐標計算。以下對其進行分析,并給出解決方法。
一、手勢判斷
? ? ? 像圖表里面碰到的這種問題,我們在圖片查看的需求里面也會碰到。查看圖片需要能夠double tap放大,縮放,拖拽。和這里是一致的。這個已經有很好的方法了,例如PhotoView,SubsamplingScaleImageView。當然,將要提到的ViewTranHelper也可以用來做查看圖片的功能。
? ? ? 那么,PhotoView是怎么處理這些手勢的呢?縮放使用ScaleGestureDetector,滑動、double tap放大使用GestureDetector。github上面開源的圖表部分也是這么做的。最初是想將PhotoView中手勢部分直接提取出來放在圖表中使用的。但是ScaleGestureDetector、GestureDetector都是通過設置callback的方式做的,而onTouchEvent是順序型處理,這樣ScaleGestureDetector與GestureDetector夾雜在一起很別扭,細節部分不好控制。所以沒有采用。
? ? ? ?實際上,這個地方不好做的就是tap、double tap、scale。tap、double tap就交給GestureDetector,它也只處理這部分。剩下的就是scale比較難,drag、fling自行處理。至于scale,既然ScaleGestureDetector里面有,那就提取出來,而不是直接使用,就避免了callback。ViewTranHelper的scale部分是從ScaleGestureDetector(android4.4)里面提取出來的。
? ? ? ?之所以從ScaleGestureDetector里面提取,是因為它支持任意多點的控制,考慮了很多細節,官方的更值得信賴。其它的,例如MPAndroidChart的scale是作者自己寫的,對于多點的處理不是很完美。測試一下多點,實際上一次性只有兩個手指的移動對于縮放是有效的,而且多手指按下再放開以后縮放不能進行下去。ScaleGestureDetector在任意多點的縮放,多點按下松開,各種情況下都很好。
? ? ? ?ViewTransHelper是獨立出來處理view手勢的工具類,在它的基礎上可以很簡單的就自定義圖表以及圖片查看功能,集合了GestureDetector與ScaleGestureDetector的手勢部分。ViewTransHelper的輸入是event,輸出是dx、dy,就是滑動偏移的大小,以及sx、sy,就是縮放的大小。
? ? ? ViewTranHelper的使用:
? ? ? ?transHelper = new ViewTransHelper(view,callback);
? ? ? ?transHelper.onTouchEvent(e);
? ? ? ?這里的callback接口方法如下:
? ? ? canDragHorizontal()、canDragVertical()、canScaleHorizontal()、canScaleVertical()指明是否能夠滑動或者縮放。
? ? ? getScaleLevel(),返回值例如1.2f,這意味著double tap以后,放大1.2f。
? ? ? onScale(float sx,float sy,float px,float py),手指縮放時的縮放大小以及縮放中心。
? ? ? onDrag(int dx,int dy),手指滑動時,滑動的距離。
? ? ? onFling(int dx,int dy),手指滑動然后松開,此時如果速度達到要求就移動直到速度減為0,或者達到了邊界點。
? ? ? 就像ViewDragHelper一樣,很簡單的。但是看來還是覺得稍微麻煩還要設置callback。下面提供兩個類對ViewDragHelper進行封裝,然后,就可以不用設置callback就能處理手勢了。
? ? ? ?在ViewTransHelper的callback中,可以想象我們根據輸出的dx、dy、sx、sy去控制canvas上的圖形的繪制。ViewTransHelper只是識別出手勢,怎么移動,怎么縮放圖形就不由它控制了。例如,我去控制某個方形移動,一般會給它的移動加上一個邊界。怎么處理呢?InsetTransHelper和OutsetTransHelper就是做這個工作的,它們對ViewTransHelper進行封裝,使用時設置需要變換的圖形,輸入是從ViewTransHelper傳過來的dx、dy、sx、sy,輸出則是變換后的圖形以及一個matrix。
? ? ? ?這樣如果你要控制canvas中的某個圖形,不再需要callback。只要設置顯示的viewport,圖形的大小,圖形最大最小寬高。然后在onDraw()中,獲取當前變換后的圖形繪制即可。詳細的使用見示例InsetTransView、OutsetTransView。
InsetTransHelper效果如下:
InsetTransHelper的使用:
? ? ? ?transHelper = new InsetTransHelper(view);
? ? ? ?transHelper.setup(viewport,shapeWidth,shapeHeight,minWidth,minHeight);
? ? ? ?transHelper.onTouchEvent(e);
? ? ? 這里viewport是顯示的窗口為rect,指在view的哪里顯示;shapeWidth、shapeHeight是希望控制的圖形shape的寬高,它們起點是viewport的起點;minWidth、minHeight是最小寬高,因為能夠縮放所以不能為0。所有的點坐標都是相對于canvas而言的。如果需要設置初始的shape,調用transHelper.setCurrentShape(rect)即可,rect也是相對于canvas坐標系而言的。
? ? ? ?InsetTransHelper保證不管怎么縮放都使得shape在viewport內部。
OutsetTransHelper的效果如下:
? ? ? ?OutsetTransHelper的使用類似,它保證任意時刻shape都是包裹viewport,任何變換下都如此。
? ? ? ?在圖表中,我們需要顯示的數據如果全部繪制出來就是一個長方形,viewport是顯示的方框,手勢滑動,就會顯示相應的部分。任意時刻這個長方形都將viewport包裹在其中。OutsetTransHelper包裹viewport與圖表中的情形是不是一模一樣。使用OutsetTransHelper以后完全不需要自己處理手勢,滑動縮放,邊界判斷之類的問題。而且它返回一個matrix,根據這個matrix可以計算出當前顯示圖表哪部分。
二、坐標計算
? ? ? 前面提到一次性繪制全部的數據,但是,顯示的只有那么幾個。效率上存在問題,而且本身自定義圖表在繪制的時候坐標計算就不好弄,現在又要做到顯示多少繪制多少,就更麻煩了。MPAndroidChart是怎么做的呢?使用matrix。
? ? ? ?計算機圖形學或者3d數學里面會講到怎樣對局部坐標系中的物體進行各種變換,然后將局部坐標系的物體變換到全局坐標系,再將其變換到viewport窗口部分,然后剪切顯示。它就是通過matrix來做的(不考慮旋轉)。這里所面對的問題是一樣的,只是換成了2d。所以,使用matrix是可以計算出手指各種縮放、移動以后,圖表的哪些數據是可見的。
? ? ? ?matrix來自于哪里呢?就是上面提到的InsetTransHelper、OutsetTransHelper中維護的matrix。
? ? ? 所以,使用ViewTranHelper就可以解決圖表中的兩個難點。剩下的例如各種樣式,各種效果,就比較方便了。后續將會分析如何繪制曲線圖,主要是坐標計算以及OutsetTransHelper的使用,并使用OutsetTransHelper實現簡單的折線圖。