1.缩放和拖拽的思路分析:
1.处理chartView中的onTouchEvent方法
2.根据down,move,up事件修改矩阵信息
3.刷新chartView
2.源码分析:
以BarChart为例,手势相关的处理都在BarLineChartBase这个类中
2.1 在BarLineChartBase 初始化touchListener
mChartTouchListener = new BarLineChartTouchListener(this,mViewPortHandler.getMatrixTouch(),3f);
2.2 在Chart的onTouchEvent中将事件处理交给mChartTouchListener
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if(!mTouchEnable || mChartTouchListener == null || mData == null){
return false;
}
return mChartTouchListener.onTouch(this,event);
}
2.3 mChartTouchListener 中的事件处理
首先我们要先看一下 ChartTouchListener 这个父类
ChartTouchListener 继承了 GestureDetector.SimpleOnGestureListener,实现了View.OnTouchListener。
内部持有一个 GestureDetector 手势监听实体,这个实体是在构造方法中初始化的
public ChartTouchListener(T chart) {
this.mChart = chart;
mGestureDetector = new GestureDetector(chart.getContext(), this);
}
接着我们再看它的一个子类 BarLineChartTouchListener, 先看一下它的构造方法:
public BarLineChartTouchListener(BarLineChartBase>> chart
, Matrix touchMatrix, float dragTriggerDistance) {
super(chart);
//持有了ViewportHandler的touchatrix,通过这个来操作数据的坐标变化
this.mMatrix = touchMatrix;
//最小拖拽触发距离
this.mDragTriggerDist = dragTriggerDistance;
//最小缩放触发距离
this.mMinScalePointerDistance = Utils.convertDpToPixel(3.5f);
}
接下来重点来了,开始分析BarLineChartTouchListener 的Touch事件,我们按事件逐个分析
2.3.1 down事件之前的处理
public boolean onTouch(View view, MotionEvent event) {
//初始化一个速度跟踪器,用于拖拽的惯性处理
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
//将事件交给速度跟踪器
mVelocityTracker.addMovement(event);
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
//如果当前的touchState是NONE的话才将事件交给手势识别器mGestureDetector
if (mTouchMode == NONE) {
mGestureDetector.onTouchEvent(event);
}
//如果既不能缩放也不能拖拽就直接return
if (!mChart.isDragEnabled() && (!mChart.isScaleXEnabled() && !mChart.isScaleYEnabled()))
return true;
...
}
其中,touchState是一个枚举,定义如下
public enum ChartGesture {
NONE, DRAG, X_ZOOM, Y_ZOOM, PINCH_ZOOM, ROTATE, SINGLE_TAP, DOUBLE_TAP, LONG_PRESS, FLING
}
/**
* 上次的gesture类型
*/
protected ChartGesture mLastGesture = ChartGesture.NONE;
//touch states
protected static final int NONE = 0; //这个主要是单击,双击事件
protected static final int DRAG = 1;//拖拽事件,包含在move事件中
protected static final int X_ZOOM = 2;//x轴方向的缩放,包含在move事件中
protected static final int Y_ZOOM = 3;//y轴方向的缩放,包含在move事件中
protected static final int PINCH_ZOOM = 4;//包含x,y轴之间的缩放
protected static final int POST_ZOOM = 5;
protected static final int ROTATE = 6;
//当前的touch state
protected int mTouchMode = NONE;
在这里你可能会有疑问,为啥只在NONE的时候才将event交给手势识别器处理呢?
因为手势识别器只处理了单击和双击事件,这....
2.3.2 单击事件
上面讲了单击事件直接交给了mGestureDetector处理,我们直接看SimpleOnGestureListener的onSingleTapUp方法
//单击事件中处理了点击之后的高亮显示和marker的显示,可以先略过这里,后面会详细讲highlight的实现
@Override
public boolean onSingleTapUp(MotionEvent e) {
mLastGesture = ChartGesture.SINGLE_TAP;
OnChartGestureListener l = mChart.getOnChartGestureListener();
if (l != null) {
l.onChartSingleTapped(e);
}
if (!mChart.isHighlightPerTapEnabled()) {
return false;
}
Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY());
performHighlight(h, e);
return super.onSingleTapUp(e);
}
2.3.3 双击事件(缩放)
还是直接看SimpleOnGestureListener的onDoubleTap方法
@Override
public boolean onDoubleTap(MotionEvent e) {
mLastGesture = ChartGesture.DOUBLE_TAP;
OnChartGestureListener l = mChart.getOnChartGestureListener();
if (l != null) {
l.onChartDoubleTapped(e);
}
//双击缩放
if (mChart.isDoubleTapToZoomEnabled() && mChart.getData().getEntryCount() > 0) {
//更正缩放中心点
MPPointF trans = getTrans(e.getX(), e.getY());
//按中心点缩放
mChart.zoom(mChart.isScaleXEnabled() ? 1.1f : 1f, mChart.isScaleYEnabled() ? 1.1f : 1f, trans.x, trans.y);
MPPointF.recycleInstance(trans);
}
return super.onDoubleTap(e);
}
getTrans这个方法是用来干什么的呢?
public MPPointF getTrans(float x, float y) {
ViewPortHandler vph = mChart.getViewPortHandler();
float xTrans = x - vph.offsetLeft();
float yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom());
return MPPointF.getInstance(xTrans, yTrans);
}
其实先调用getTrans 然后再调用zoom方法就是对矩阵进行先平移再缩放的操作,防止按中心点缩放的时候,X、Y轴的0点不在视野之内了。
如下图所以,如果以黑点为缩放中心,绿框放大后变成蓝框,这时候0点可能在bottom之下了,如果平移后在缩放,就避免了这种情况(如果平移的太多了也没事,每次缩放都会做边界检测的)。大家可以修改下代码自己测试下。
接下来我们看zoom这个方法,就是对mViewPortHandler中的mMatrixTouch进行缩放操作,然后请求刷新view
public void zoom(float scaleX, float scaleY, float x, float y) {
mViewPortHandler.zoom(scaleX, scaleY, x, -y, mZoomMatrixBuffer);
mViewPortHandler.refresh(mZoomMatrixBuffer, this, false);
// Range might have changed, which means that Y-axis labels
// could have changed in size, affecting Y-axis size.
// So we need to recalculate offsets.
calculateOffsets();
postInvalidate();
}
2.4 Down事件
单点和多点的down事件中记录了第一次落下去的点等初始信息
2.5 Move事件
//拖拽
if (mTouchMode == DRAG) {
mChart.disableScroll();
float x = mChart.isDragXEnabled() ? event.getX() - mTouchStartPoint.x : 0.f;
float y = mChart.isDragYEnabled() ? event.getY() - mTouchStartPoint.y : 0.f;
performDrag(event, x, y);
}
//缩放
else if (mTouchMode == X_ZOOM || mTouchMode == Y_ZOOM || mTouchMode == PINCH_ZOOM) {
mChart.disableScroll();
if (mChart.isScaleXEnabled() || mChart.isScaleYEnabled()) {
performZoom(event);
}
}
//刚进入move事件时,mTouchMode == NONE,进入此代码块判断具体是什么事件
else if (mTouchMode == NONE && Math.abs(distance(event.getX(), mTouchStartPoint.x, event.getY(),
mTouchStartPoint.y)) > mDragTriggerDist) {
if (mChart.isDragEnabled()) {
//如果已经缩放到最小或者没有拖拽位移就弹出higelight 否则就什么都不做(因为就是你想拖动也滑不动了啊)
boolean shouldPan = !mChart.isFullyZoomedOut() ||
!mChart.hasNoDragOffset();
if (shouldPan) {
float distanceX = Math.abs(event.getX() - mTouchStartPoint.x);
float distanceY = Math.abs(event.getY() - mTouchStartPoint.y);
// Disable dragging in a direction that's disallowed
if ((mChart.isDragXEnabled() || distanceY >= distanceX) &&
(mChart.isDragYEnabled() || distanceY <= distanceX)) {
mLastGesture = ChartGesture.DRAG;
mTouchMode = DRAG;
}
} else {
//如果缩放比例是1,就进行拖拽highlight
if (mChart.isHighlightPerDragEnabled()) {
mLastGesture = ChartGesture.DRAG;
if (mChart.isHighlightPerDragEnabled())
performHighlightDrag(event);
}
}
}
}
我们先来看一下performDrag方法
private void performDrag(MotionEvent event, float distanceX, float distanceY) {
mLastGesture = ChartGesture.DRAG;
//1.将保存的操作赋给mMatrix
mMatrix.set(mSavedMatrix);
OnChartGestureListener l = mChart.getOnChartGestureListener();
//2.对mMatrix进行平移操作
mMatrix.postTranslate(distanceX, distanceY);
if (l != null)
l.onChartTranslate(event, distanceX, distanceY);
}
performZoom方法这里就不做介绍了,核心操作和onDoubleTap是类似的。
无论是performDrag 和 performZoom方法都是对矩阵mMatrix进行了操作,那么是如何刷新界面的呢?
在onTouchEvent的最后会刷新界面,代码如下:
@Override
public boolean onTouch(View view, MotionEvent event) {
//操作矩阵
...
/**
* 刷新界面
*/
mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, true);
return true;
}
2.5 UP事件
up事件中,我们主要看一下惯性滑动
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
final int pointerId = event.getPointerId(0);
//获取瞬时速度
velocityTracker.computeCurrentVelocity(1000, Utils.getMaximumFlingVelocity());
final float velocityX = velocityTracker.getXVelocity(pointerId);
final float velocityY = velocityTracker.getYVelocity(pointerId);
//惯性滑动
if (Math.abs(velocityX) > Utils.getMinimumFlingVelocity() ||
Math.abs(velocityY) > Utils.getMinimumFlingVelocity()) {
if (mTouchMode == DRAG && mChart.isDragDecelerationEnabled()) {
stopDeceleration();
mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis();
mDecelerationCurrentPoint.x = event.getX();
mDecelerationCurrentPoint.y = event.getY();
mDecelerationVelocity.x = velocityX;
mDecelerationVelocity.y = velocityY;
Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by
// Google
}
}
...
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
endAction(event);
break;
Utils.postInvalidateOnAnimation方法中调用了chart的postInvalidateOnAnimation,进而调用了BarLineChartBase的computeScroll方法
@Override
public void computeScroll() {
if (mChartTouchListener instanceof BarLineChartTouchListener)
((BarLineChartTouchListener) mChartTouchListener).computeScroll();
}
最后调用了mChartTouchListener的computeScroll方法
public void computeScroll(){
//滑动的终止条件
if(mDecelerationVelocity.x == 0.f && mDecelerationVelocity.y == 0.f){
return;
}
final long currentTime = AnimationUtils.currentAnimationTimeMillis();
mDecelerationVelocity.x *= mChart.getDragDecelerationFrictionCoef();
mDecelerationVelocity.y *= mChart.getDragDecelerationFrictionCoef();
//1.计算当前时间与point up的时间差,除以1000ms,整个惯性时间是1s,注释要用float类型,不然int直接是0,滑不动了就。
final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f;
//2.计算本次移动的距离,每次加上mDecelerationCurrentPoint记录的坐标,就是移动后的坐标,然后手动创建一个MotionEvent
float distanceX = mDecelerationVelocity.x * timeInterval;
float distanceY = mDecelerationVelocity.y * timeInterval;
mDecelerationCurrentPoint.x += distanceX;
mDecelerationCurrentPoint.y += distanceY;
MotionEvent event = MotionEvent.obtain(currentTime,currentTime,MotionEvent.ACTION_MOVE,
mDecelerationCurrentPoint.x+distanceX,mDecelerationCurrentPoint.y + distanceY,0);
//计算总共的偏移量,而不是每次的偏移量,因为在performDrag中会每次重置mMatrix到mSavedMatrix
float dragDistanceX = mChart.isDragXEnabled() ? mDecelerationCurrentPoint.x - mTouchStartPoint.x : 0.f;
float dragDistanceY = mChart.isDragYEnabled() ? mDecelerationCurrentPoint.y - mTouchStartPoint.y : 0.f;
performDrag(event, dragDistanceX, dragDistanceY);
event.recycle();
// 注意此处不要刷新,因为要用postinvalidate
mMatrix = mChart.getViewPortHandler().refresh(mMatrix,mChart,false);
mDecelerationLastTime = currentTime;
if (Math.abs(mDecelerationVelocity.x) >= 0.01 || Math.abs(mDecelerationVelocity.y) >= 0.01)
Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google
else {
//滑动之后,y轴可显示的rang的范围可能改变了,这时候需要重新计算
// Range might have changed, which means that Y-axis labels
// could have changed in size, affecting Y-axis size.
// So we need to recalculate offsets.
mChart.calculateOffsets();
mChart.postInvalidate();
stopDeceleration();
}
}
3.总结
我们上面讲了拖拽,滑动,惯性滑动,缩放事件,本质上都是操作ViewPortHander的mMatrxTouch矩阵,然后调用chartView的invalidate等类似方法刷新界面。