本文介绍自己写的一个处理手势的工具类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实现简单的折线图。