1.定制视图

  Android自带众多优秀的标准视图与组件,但有时为追求独特的应用视觉效果,我们仍需创建定制视图。

  定制视图分为两大类别:

  •   简单视图: 简单视图内部也可以很复杂,之所以归为简单类别,是因为简单视图不包括子视图,而且简单视图几乎总是会执行定制绘制。
  •   聚合视图:聚合视图由其他视图对象组成,聚合视图通常管理着子视图,但不负责执行定制绘制,图形绘制任务都委托给了各个子视图。

  

  创建定制视图的所需的三大步骤:

  •   选择超类。对于简单定制视图而言,View是个空白画布,因此它作为超类最常见,对于聚合定制视图,我们应选择合适的超类布局,比如FrameLayout.
  •   继承选定的超类,并至少覆盖一个超类构造方法。
  •   覆盖其他关键方法,以定制视图行为。

1.1 创建一个简单的视图类

public class BoxDrawingView extends View {
    // Used when creating the view in code
    public BoxDrawingView(Context context) {
        this(context, null);
    }

    // Used when inflating the view from XML


    public BoxDrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

  从布局文件中实例化的视图可收到一个AttributeSet实例,该实例包含了XML布局文件中指定的XML属性。即使不打算使用构造方法,按习惯做法也应添加他们。

    有了定制视图类,可以在布局文件里面引用它。

<com.bignerdranch.android.draganddraw.BoxDrawingView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

  在引用时必须使用自定义View的全路径名,这样布局inflater才能够找到它,布局文件inflater解析布局XML文件,并按视图定义创建View实例,如果元素名不是全路径名,布局inflater

会转而在android.view和android.widget包中寻找目标,如果目标视图放置在其他包中,布局inflater将无法找到目标并最终导致应用崩溃。

  因此,对于android.view和android.widger包以外的定制视图类,必须指定它们的全路径名。

 

1.2 处理触摸事件

监听触摸事件的一种方式是使用以下view方法,设置一个触摸事件监听器:

public void setOnTouchListener(View.OnTouchListener l)

不过我们的定制视图是View的子类,因此可走捷径直接覆盖以下View方法:

public boolean onTouchEvent(MotionEvent event)

该方法接收一个MotionEvent类实例,MotionEvent类可用来描述包括位置和动作的触摸事件。动作用于描述事件所处的阶段。

动作常量   动作描述
ACTION_DOWN 手指触摸到屏幕
ACTION_MOVE 手指在屏幕上移动
ACTION_UP    手指离开屏幕
ACTION_CANCEL   父视图拦截了触摸事件

 

 

 

 

 

在onTouchEvent()实现方法中,可使用以下MotionEvent方法查看动作值:

public final int getAction()

 

我们的目的就是在一根手指放下的时候记录下放下的位置,移动时随之变化,放开时固定该矩形框。并且之前画的矩形框数据需要记录下来。 

建立一个实体类,用于表示一个矩形框的定义数据。用来保存原始坐标点(手指的初始位置)和当前坐标点(手指的当前位置):

public class Box {
    private PointF mOrigin;
    private PointF mCurrent;

    public Box(PointF origin) {
        mOrigin = origin;
        mCurrent = origin;
    }
   //get、set略 }

然后重写onTouchEvent()方法并进行相应操作:

private Box mCurrentBox;
private List<Box> mBoxen = new ArrayList<>();

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 每次有触摸事件都记录下现在的坐标
    PointF current = new PointF(event.getX(), event.getY());
    String action = "";

    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            action = "ACTION_DOWN";
            // 每次按下的时候在列表中中新增一个 Box
            mCurrentBox = new Box(current);
            mBoxen.add(mCurrentBox);
            break;
        case MotionEvent.ACTION_MOVE:
            action = "ACTION_MOVE";
            if (mCurrentBox != null) {
            // 移动的时候都要重绘
                mCurrentBox.setCurrent(current);
                invalidate();
            }
            break;
        case MotionEvent.ACTION_UP:
            // 抬起的时候不再指向最新的 Box
            action = "ACTION_UP";
            mCurrentBox = null;
            break;
        case MotionEvent.ACTION_CANCEL:
            action = "ACTION_CANCEL";
            mCurrentBox = null;
            break;
    }

    Log.i(TAG, action + " at x=" + current.x +
           ", y=" + current.y);z

    return true;
}

在取消触摸事件或用户手指离开屏幕时,应清空mCurrentBox以结束屏幕绘制,以完成的Box会安全的存储在数组中。

invalidate()方法会强制BoxDrawingView重新绘制自己,这样在拖拽时就能实时看到矩形框。

 

2 onDraw()方法内的图形绘制

  应用启动时,所有视图都处于无效状态(视图还没有绘制到屏幕上),为解决这个问题,Android调用了顶级View视图的draw()方法,这会引起自上而下的链式调用反映。

首先,视图完成自我绘制,然后是子视图的自我绘制,再然后是子视图的子视图的自我绘制,如此调用下去直至继承结构的末端。当继承结构中的所有视图都完成自我绘制后,最顶级

View 视图也就生效了。

  为加入这种绘制,可覆盖以下 View 方法:
    protected void onDraw(Canvas canvas);

  在onTouchEvent()方法中响应ACTION_MOVE动作时,我们调用invalidate()方法再次让BoxDrawingView处于失效状态,这迫使他重新完成自我绘制,并再次调用onDraw()方法。

  

  Canvas和Paint是Android系统的两大绘制类。

  •   Canvas类拥有我们需要的所有绘制操作,其方法可决定在哪里以及绘什么,比如线条、圆形、字词、矩形等。
  •   Paint类决定如何绘制,其方法可指定绘制图形的特征,例如是否填充图形、使用什么字体绘制、线条是什么颜色等

  

 public BoxDrawingView(Context context, AttributeSet attrs){ //AttributeSet实例包含了XML布局文件中指定的XML属性。
        super(context,attrs);

        mBoxPaint = new Paint();
        mBoxPaint.setColor(0x22ff0000);

        mBackgroundPaint = new Paint();
        mBackgroundPaint.setColor(0xfff8efe0);
    }

 @Override
    protected void onDraw(Canvas canvas){
        //先画出背景
        canvas.drawPaint(mBackgroundPaint);

        //画出每个绘制过的矩形
        for(Box box : mBoxen){
            float left = Math.min(box.getOrigin().x, box.getCurrent().x);
            float right = Math.max(box.getOrigin().x, box.getCurrent().x);
            float top = Math.min(box.getOrigin().y, box.getCurrent().y);
            float bottom = Math.max(box.getOrigin().y,box.getCurrent().y);

            canvas.drawRect(left, top ,right ,bottom, mBoxPaint);
        }

    }

 

 以上代码的第一部分简单直接:使用米白背景paint,填充canvas以衬托矩形框。然后,针对矩形框数组中的每一个矩形框,据其两点坐标,确定矩形框上下左右的位置。绘制时,左端和顶端的值作为最小值,右端和底端的值作为最大值。完成位置坐标值计算后,调用 Canvas.drawRect(...) 方法,在屏幕上绘制红色矩形框。