Android 多点触控,绘制滑动轨迹和十字光标

这个测试项,要捕捉当前有几个触摸点,当前触摸点坐标,滑动事件在x轴、y轴方向的速度等信息,在触摸时跟随触摸点会出现十字光标,绘制出滑动轨迹。

  • 首先绘制出暗色格子背景,采用了自定义View,较为简单,核心代码如下:
     Paint paint;  //画笔
    private int mWidth;
    private int mHeight;
    public Check(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint();
        paint.setColor(getResources().getColor(R.color.deepGray));//设置画笔颜色
        paint.setStrokeJoin(Paint.Join.ROUND);//设置画笔图形接触时笔迹的形状
        paint.setStrokeCap(Paint.Cap.ROUND);//设置画笔离开画板时笔迹的形状
        paint.setStrokeWidth(1);  //设置画笔的宽度
    }

     /**
     * 这个方法可以获得控件的宽高
     * @param canvas
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mWidth = w;
        mHeight = h;
    }
    /**
     * 绘制网格线
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.BLACK);
        int lineStart = 80;
        int space = lineStart;   //长宽间隔
        int vertz = lineStart;
        int hortz = lineStart;
        for (int i = 0; i < 100; i++) {
            canvas.drawLine(0, vertz, mWidth, vertz, paint);
            canvas.drawLine(hortz, 0, hortz, mHeight, paint);
            vertz += space;
            hortz += space;
        }
    }
  • 接下来,因为要在这个背景上画图,我在其上覆盖一层透明ImageView,给该iv设置这个属性:
    android:background="@android:color/transparent"
    接下来的绘制滑动轨迹和十字光标都在这个iv上完成。

  • 接下来遇到了一些坑,都踩了一遍。

    1. 因为这个绘图是发生在一个Fragment里,我的绘图界面要设置全屏,但是该Activity中的其他Fragment则不需要这个设置。于是就在这个Fragment中获取到Window,然后设置全屏标记,然后让根视图MATCH_PARENT。
 getActivity().getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

      //mRootView是BaseFragment中设置的该Fragment的视图
        this.mRootView.setLayoutParams(        
                new FrameLayout.LayoutParams(
                        FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.MATCH_PARENT));
  • 创建bitmap,设置bitmap的高宽时,遇到了问题。
    • 因为在onCreateView中View.getWidth和View.getHeight无法获得一个view的高度和宽度,这是因为View组件布局要在onResume回调后完成。所以现在需要使用getViewTreeObserver().addOnGlobalLayoutListener()来获得宽度或者高度。这是获得一个view的宽度和高度的方法之一。

    • OnGlobalLayoutListener 是ViewTreeObserver的内部类,当一个视图树的布局发生改变时,可以被ViewTreeObserver监听到,这是一个注册监听视图树的观察者(observer),在视图树的全局事件改变时得到通知。ViewTreeObserver不能直接实例化,而是通过getViewTreeObserver()获得。

 mTouchScreenIv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {

                mTouchScreenIvWidth = mTouchScreenIv.getWidth();
                mTouchScreenIvHeight = mTouchScreenIv.getHeight();
                // 创建空白图片
                mBitmap1 = Bitmap.createBitmap(mTouchScreenIvWidth, mTouchScreenIvHeight, Bitmap.Config.ARGB_8888);
                mBitmap2 = Bitmap.createBitmap(mTouchScreenIvWidth, mTouchScreenIvHeight, Bitmap.Config.ARGB_8888);
                // 创建两张画布
                mCanvas1 = new Canvas(mBitmap1); //底层画轨迹的画布
                mCanvas2 = new Canvas(mBitmap2); //上面一层画十字架的画布
                // 创建画笔
                mPaint1 = new Paint();      //画轨迹的画笔
                mPaint2 = new Paint();      //画十字架的画笔
                // 画笔颜色为蓝色
                mPaint1.setColor(getResources().getColor(R.color.lightBlue));
                mPaint2.setColor(getResources().getColor(R.color.lightBlue));
                // 宽度1个像素
                mPaint1.setStrokeWidth(1);
                mPaint2.setStrokeWidth(1);
                // 先将白色背景画上
                mCanvas1.drawBitmap(mBitmap1, new Matrix(), mPaint1);
                mCanvas2.drawBitmap(mBitmap2, new Matrix(), mPaint2);

                mBitmap3 = mergeBitmap(mBitmap1, mBitmap2);//将两张bitmap图合为一张

                mTouchScreenIv.setImageBitmap(mBitmap3);

                //用完要解除监听
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    mTouchScreenIv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            }
        });
  • 把两个bitmap合成一个bitmap
 /**
     * 把两个位图覆盖合成为一个位图,以底层位图的长宽为基准
     * @param backBitmap  在底部的位图
     * @param frontBitmap 盖在上面的位图
     * @return
     */
    public Bitmap mergeBitmap(Bitmap backBitmap, Bitmap frontBitmap) {

        if (backBitmap == null || backBitmap.isRecycled()
                || frontBitmap == null || frontBitmap.isRecycled()) {
            Log.e(TAG, "backBitmap=" + backBitmap + ";frontBitmap=" + frontBitmap);
            return null;
        }
        Bitmap bitmap = backBitmap.copy(Bitmap.Config.ARGB_8888, true);
        Canvas canvas = new Canvas(bitmap);
        Rect baseRect = new Rect(0, 0, backBitmap.getWidth(), backBitmap.getHeight());
        Rect frontRect = new Rect(0, 0, frontBitmap.getWidth(), frontBitmap.getHeight());
        canvas.drawBitmap(frontBitmap, frontRect, baseRect, null);
        return bitmap;
    }
  • 给iv控件设置触摸事件。因为是多点触摸事件,所以记录down的起始点和move时的终止点都需要使用float数组。设置四个大小为10的数组。
  • 添加 触摸事件的速度检测器。
  • 单点触摸事件ACTION_DOWN ,多点触摸事件ACTION_POINTER_DOWN,使用case穿透将两种事件一起监听,遍历触摸事件,获得坐标,进行操作。
@Override
    protected void setListener() {
        mTouchScreenCheck.setOnTouchListener(new View.OnTouchListener() {
            
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {

                //当前DOWN或者UP的是手指的index
                int curPointerIndex = motionEvent.getActionIndex();

                //通过index获得当前手指的id
                int curPointerId = motionEvent.getPointerId(curPointerIndex);

                //添加事件的速度计算器
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
                }
                mVelocityTracker.addMovement(motionEvent);

                int actionMasked = motionEvent.getActionMasked();
                Log.i(TAG, "actionMasked === " + actionMasked);
                switch (actionMasked) {

                    case MotionEvent.ACTION_DOWN:
                    case MotionEvent.ACTION_POINTER_DOWN:

                        mCanvas1.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                        mCanvas2.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

                        mTouchScreenIv.setImageBitmap(mergeBitmap(mBitmap1, mBitmap2));

                        //设置当前有几个触摸点
                        pointerCount = motionEvent.getPointerCount();
                        if (pointerCount > 10) {
                            pointerCount = 10;
                        }

                        mActivePointers.append(curPointerId, curPointerId);
                        
                        //在down事件中的操作
                        DownPoint(motionEvent);
                        
                        mTouchScreenIv.setImageBitmap(mergeBitmap(mBitmap1, mBitmap2));

                        break;

                    case MotionEvent.ACTION_MOVE:

                        //获取当前触摸事件的个数
                        if (motionEvent.getPointerCount() > pointerCount) {
                            pointerCount = motionEvent.getPointerCount();
                        }

                        mTouchScreenTvP.setText("P:" + motionEvent.getPointerCount() + "/" + pointerCount);

                        //清除十字架
                        mCanvas2.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                        
                        //在移动时的操作
                        MovePoint(motionEvent);

                        mTouchScreenIv.setImageBitmap(mergeBitmap(mBitmap1, mBitmap2));

                        break;

                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_POINTER_UP:

                        //计算并显示坐标偏移量,并且设置背景颜色
                        setDxDy(motionEvent);

                        //清除十字架
                        mCanvas2.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

                        //清除这个触摸事件的ID

                        mActivePointers.remove(curPointerId);

                        mTouchScreenTvP.setText("P:" + 0 + "/" + pointerCount);
                        mTouchScreenIv.setImageBitmap(mergeBitmap(mBitmap1, mBitmap2));

                        break;
                }
                return true;
            }
        });
    }
    
    /**
     * 在down事件中的操作
     * @param motionEvent
     */
    private void DownPoint(MotionEvent motionEvent) {

        for (int i = 0; i < motionEvent.getPointerCount(); i++) {

            int pointerId = mActivePointers.get(motionEvent.getPointerId(i));

            try {

                //获取触摸点的X,y坐标
                startXs[pointerId] = motionEvent.getX(pointerId);
                startYs[pointerId] = motionEvent.getY(pointerId);

                finalStartX = startXs[pointerId];
                finalStartY = startYs[pointerId];

                //设置上面的字变化并且背景颜色为白色
                mTouchScreenTvDx.setText("X:" + Math.round(motionEvent.getX(pointerId) * 10) / 10.0);
                mTouchScreenTvDx.setBackgroundColor(Color.WHITE);
                mTouchScreenTvDy.setText("Y:" + Math.round(motionEvent.getY(pointerId) * 10) / 10.0);
                mTouchScreenTvDy.setBackgroundColor(Color.WHITE);

            } catch (IllegalArgumentException e) {
                // 此处捕捉系统bug,以防程序停止
                e.printStackTrace();
            }
            mTouchScreenTvP.setText("P:" + pointerCount + "/" + pointerCount);
            // 在开始和结束坐标间画一个点
            mCanvas1.drawPoint(startXs[pointerId], startYs[pointerId], mPaint1);

            //画十字架
            mCanvas2.drawLine(0, startYs[pointerId], mTouchScreenIvWidth, startYs[pointerId], mPaint2);
            mCanvas2.drawLine(startXs[pointerId], 0, startXs[pointerId], mTouchScreenIvHeight, mPaint2);

        }
    }

    /**
     * 在移动时对点的操作
     * @param motionEvent
     */
    private void MovePoint(MotionEvent motionEvent) {
        for (int i = 0; i < motionEvent.getPointerCount(); i++) {

            int pointerId = mActivePointers.get(motionEvent.getPointerId(i));

            Log.i(TAG, "1111111 move pointerId" + pointerId);
            Log.i(TAG, "1111111 endXS size " + endXs.length);

            try {
                // 获取手移动后的坐标
                endXs[pointerId] = motionEvent.getX(pointerId);
                endYs[pointerId] = motionEvent.getY(pointerId);

                // 在开始和结束坐标间画一条线
                mCanvas1.drawLine(startXs[pointerId], startYs[pointerId], endXs[pointerId], endYs[pointerId], mPaint1);

                //重新画十字架
                mCanvas2.drawLine(0, endYs[pointerId], mTouchScreenIvWidth, endYs[pointerId], mPaint2);
                mCanvas2.drawLine(endXs[pointerId], 0, endXs[pointerId], mTouchScreenIvHeight, mPaint2);

                //设置显示坐标的数字变化并且背景颜色为白色
                mTouchScreenTvDx.setText("X:" + Math.round(endXs[pointerId] * 10) / 10.0);
                mTouchScreenTvDx.setBackgroundColor(Color.WHITE);
                mTouchScreenTvDy.setText("Y:" + Math.round(endYs[pointerId] * 10) / 10.0);
                mTouchScreenTvDy.setBackgroundColor(Color.WHITE);

                //获取当前触摸事件的速度
                mVelocityTracker.computeCurrentVelocity(10);
                mTouchScreenTvXv.setText("Xv:" +
                        Math.round(mVelocityTracker.getXVelocity(0) * 1000) / 1000.0 + "");
                mTouchScreenTvYv.setText("Yv:" +
                        Math.round(mVelocityTracker.getYVelocity(0) * 1000) / 1000.0 + "");

                // 刷新开始坐标
                startXs[pointerId] = (int) motionEvent.getX(pointerId);
                startYs[pointerId] = (int) motionEvent.getY(pointerId);
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                Log.i(TAG, "11111:IllegalArgumentException ");
            }
        }
    }
    /**
     * 计算并显示坐标偏移量,并且设置背景颜色
     * @param motionEvent
     */
    private void setDxDy(MotionEvent motionEvent) {
        float dx = motionEvent.getX() - finalStartX;
        float dy = motionEvent.getY() - finalStartY;
        mTouchScreenTvDx.setText("dX:" + Math.round(dx * 10) / 10.0);

        if (dx > 0.1 || dx < -0.1) {
            mTouchScreenTvDx.setBackgroundColor(Color.RED);
        } else {
            mTouchScreenTvDx.setBackgroundColor(Color.WHITE);
        }

        mTouchScreenTvDy.setText("dY:" + Math.round(dy * 10) / 10.0);

        if (dy > 0.1 || dy < -0.1) {
            mTouchScreenTvDy.setBackgroundColor(Color.RED);
        } else {
            mTouchScreenTvDy.setBackgroundColor(Color.WHITE);
        }
    }
  • ACTION_DOWN :当触摸到第一个点时,被触发
  • ACTION_POINTER_DOWN:当控件上已经有点被触摸,再次有点被触摸时,触发该事件。
  • ACTION_UP 和 ACTION_POINTER_UP 也是类似的,最后一个点抬起时才触发ACTION_UP。
  • 但是ACTION_MOVE没有类似的方法,可以通过遍历触摸事件,获得每一个触摸事件。
  • 在触摸时每一个触摸事件会被分配一个id,通过不同的id获取每一个触摸点的坐标。

遗留bug:每当有新的触摸事件时,以前的滑动轨迹会被清空。

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

推荐阅读更多精彩内容