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(...) 方法,在屏幕上绘制红色矩形框。