5.手势实现解析

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之下了,如果平移后在缩放,就避免了这种情况(如果平移的太多了也没事,每次缩放都会做边界检测的)。大家可以修改下代码自己测试下。


5.手势实现解析_第1张图片
image.png

接下来我们看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等类似方法刷新界面。

你可能感兴趣的:(5.手势实现解析)