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:每当有新的触摸事件时,以前的滑动轨迹会被清空。

你可能感兴趣的:(Android 多点触控,绘制滑动轨迹和十字光标)