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


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等类似方法刷新界面。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 手势识别器是附加到视图的对象,将低级别事件处理代码转换为更高级别的操作,它允许视图以控件执行的方式响应操作。 手势...
    坤坤同学阅读 4,073评论 0 9
  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,478评论 1 11
  • 今天天气晴 适合出去走走停停晒晒太阳 去亲吻最后一片还未凋落的银杏 适合去问候一个陌生的人 不问他的名字 我们从今...
    和尚的书阅读 247评论 2 1
  • 今天谈谈豆瓣产品形态的变化,我每天在互联网上,花在豆瓣的时间应该是最多的,所以对豆瓣产品更新迭代所带来的用户体验的...
    最牛部落阅读 372评论 1 0
  • 「飞猪说币」2018年的10倍币:IPFS原理介绍之三 飞猪尽量用自己的语言来做基础原理介绍,也许不是最精确,...
    飞不起来的猪简称飞猪阅读 333评论 3 2