安卓自定义 view 开篇

299 阅读5分钟

本开篇是整个自定义 view 系列的开始,主要探讨安卓屏幕坐标系,颜色以及 MotionEvent 的 getX, getY 和 getRawX, getRawY 的区别。

屏幕坐标系

回想初中数学建立的直角坐标系,如图1-1

图1-1

那在安卓中的屏幕会是什么样子呢? 它应该是这个样子的。如图 1-2

图1-2

根据自己的理解去验证是否符合猜想,就在屏幕中去绘制这样的一个坐标系。在自定义控件过程中,有些坐标想不清楚其实也是和没有深刻理解手机坐标系的而导致的。如图1-3 是我在画布中绘制而成。通过对比可以看出,手机屏幕的 y 轴正半轴向下,这就是和数学坐标系的区别。想要看代码预览代码的点击此处 ,注释很清楚的,当然不懂也是没关系的,本篇只是介绍坐标系,如果是小白可能对代码中使用的 api 不了解,别慌,问题不大。往后看会懂的😊, 你只需要理解屏幕坐标系和数学中的坐标系的区别即可。

图1-3

颜色系统

将颜色之前先说说光的三原色,也就是红、绿、蓝,光学三原色组成显示屏显示颜色。在安卓系开发中颜色无处不再,在自定义 view 中更是经常用到。常用的颜色设置方法如下:

 // 通过十六进制的方式设置
       mPaint.setColor(Color.parseColor("#E2C0D6"));  
       // 没有透明度设置通道
       mPaint.setColor(Color.rgb(100, 100,100));
     // 有透明度, 其中 a 表示 alpha , r  表示red, g 表示 green, b 表示 b
        mPaint.setColor(Color.argb(200,100,100, 100));

值得注意的是,安卓手机屏幕是不可能有透明度的变化的,我们设置的透明度其实是将 R、G、B 与画布的颜色进行混合而的到的。这样达到透明度的效果。有很多这样的图形混合方式,将在后续讲解。

MotionEvent 的 getX,getY 和 getRawX, getRawY

讲这个区别通过例子来讲解,在屏幕上画一个圆,然后当手指按下后进行移动。分析这个自定义 view 的实现,无非就是需要处理按下的手指是否在圆上,以及怎样记录移动的坐标,根据记录的坐标不断更新圆的位置, 效果如下:

滚动小球

首先新建 MotionEventView 继承自 View 重写带有两个参数的构造方法,这是为了在布局中能使用,布局中使用的自定义控件会调用带有两个参数的构造方法。接着初始化画笔。

private void initPaint() {
        // 初始化画笔并设置抗拒址以及抗抖动.
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        // 设置填充.
        mPaint.setStyle(Paint.Style.FILL);
        // 设置画笔的颜色
        //mPaint.setColor(Color.parseColor("#E2C0D6"));
        mPaint.setColor(Color.parseColor("#880000"));
    }

以及记录移动的坐标,初始化时为 view 的中心点坐标,当前的 view 和屏幕宽高一致。

 public MotionEventView(Context context, AttributeSet attrs) {
        super(context, attrs);

        initPaint();
  
       // 记录手指移动的坐标
        mPoint = new PointF();
    }

  @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mWidth = w;
        mHeight = h;

        // 开始初始化为 view 的中心点坐标
        mPoint.set((float) mWidth / 2, (float) mHeight / 2);
    }

注意的是 onSizeChanged 是 view 经过测量后的到确切的大小。其中 oldw,oldh 是当 view 发生该变导致重新绘制,记录的是上一次 view 测量的大小。而 w,h 是 view 最终经过测量确定的大小。

接下来,在 onDraw 方法中绘制圆。

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
  
     // 绘制一个圆, 确定一个圆需要一个圆心坐标和半径,然后以半径旋转一周得到一个圆, 
     // 这里第一个和第二个参数为圆心坐标, 
     // 第三个参数为 半径, 
     // 第四个参数是画笔
        canvas.drawCircle(mPoint.x, mPoint.y, radius, mPaint);
    }

最后就是处理手势移动过程啦!在安卓中要处理手势移动,只需实现 onTouchEvent 方法或者通过 view 的 setOnTouchListener 实现 onTouch 方法也可。处理其中手势按下,移动,抬起 或意外事件终结各个状态。

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN: // 根据名字可以看出,这是手势按下
                // 手指按下时需要检查是否在圆上
                mHasPressed = hasPressed(event);
                if (mHasPressed) {
                    Toast.makeText(getContext(), "点击到圆上", Toast.LENGTH_SHORT).show();
                }

                break;
            case MotionEvent.ACTION_MOVE:
                if (mHasPressed) {
                   // 手势移动的时,将新的坐标赋值给 mPoint
                    mPoint.set(event.getX(), event.getY());
                }
  
                // 调用这个方法会重新绘制,调用 onDraw 方法。
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
               // 手势抬起或意外终结将圆还原到初始位置,以及重置标志位。
                mPoint.set((float) mWidth / 2, (float) mHeight / 2);
                mHasPressed = false;
                invalidate();
                break;
        }

上面检查手势按下的坐标是否在圆上,这也是在初等数学中学到的。也就是按下的点与圆心的距离和半径的差值,如果大于0,在圆外,小于等于0 在圆内。其本质就是勾股定理,看一张图。 只要想象 O 点为圆心坐标, B 点为手势按下的坐标。通过公式就可以轻松求出距离。

图3-1

所以 hasPressed 的计算方法如下:

    /**
     *  检查是否 down 事件在圆上
     * @param event
     * @return
     */
    public boolean hasPressed(MotionEvent event) {
        // 当前按下的位置距离 view 的左上角位置.
        float x = event.getX();
        float y = event.getY();

        // 要判断一个点是否在圆上,根据公式 按下的点和圆心的距离小于等于半径, 实际上就是初中学的勾股定理
       // Math.pow 为求一个数的几次方, 这里是 2 次方
        double distance = Math.sqrt(Math.pow(x - mPoint.x, 2) + Math.pow(y - mPoint.y, 2));
        if (distance <= radius) {
            return true;
        }

        return false;
    }

如果留心看代码的话,可以看到我们使用的是 event.getX(), event.getY() 来获取的按下的坐标。这个点的含义是对于当前的视图, 比如这个就是针对的是我们自定义的 MotionEventView, 而 getRawX(), getRawY() 是以屏幕的坐标系为参考。读者可以自行将 event.getY() 改为 getRawY() 试试看,看看是不是会发现按下的点要在圆上方一点。这是因为 getRawY 的坐标多了一个 ActionBar 的高度。