Android编程权威指南(第二版)学习笔记(二十九)—— 第29章 定制视图与触摸事件

    xiaoxiao2021-03-25  70

    本章主要讲了自定义 View 及其触摸事件的处理,有一定的难度

    GitHub 地址: 完成第29章,未完成挑战 完成第29章挑战1-设备旋转 完成第29章挑战2-双指旋转矩形

    1. 自定义 View(定制视图)

    Android 自带众多优秀的标准视图与组件,但有时为追求独特的应用视觉效果,我们仍需创建定制视图。尽管定制视图种类繁多,但无外乎分为以下两大类别。 - 简单视图。简单视图内部也可以很复杂;之所以归为简单类别,是因为简单视图不包括子视图。而且,简单视图几乎总是会执行定制绘制。 - 聚合视图。聚合视图由其他视图对象组成。聚合视图通常管理着子视图,但不负责执行定制绘制。图形绘制任务都委托给了各个子视图。 创建定制视图所需的三大步骤: 1. 选择超类。对于简单定制视图而言,View 是个空白画布,因此它作为超类最常见。对于聚合定制视图,我们应选择合适的超类布局,比如 FrameLayout。 2. 继承选定的超类,并至少覆盖一个超类构造方法。 3. 覆盖其他关键方法,以定制视图行为。

    1.1 创建一个基本的自定义 View

    public class BoxDrawingView extends View { // 从代码中创建的时候调用 public BoxDrawingView(Context context) { this(context, null); } // 从 xml 文件中 inflate 的时候调用 public BoxDrawingView(Context context, AttributeSet attrs) { super(context, attrs); }

    注意在引用时我们必须使用自定义 View 的全路径类名,这样布局 inflater 才能够找到它。布局 inflater 解析布局 XML 文件,并按视图定义创建 View 实例。如果元素名不是全路径类名,布局 inflater 会转而在 android.view 和 android.widget 包中寻找目标。如果目标视图类放置在其他包中,布局 inflater 将无法找到目标并最终导致应用崩溃。

    1.2 处理触摸事件

    因为我们的自定义 View 是 View 的子类,可以直接覆盖以下 View 方法:

    public boolean onTouchEvent(MotionEvent event)

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

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

    我们的目的就是在一根手指放下的时候记录下放下的位置,移动时随之变化,放开时固定该矩形框。并且之前画的矩形框数据需要记录下来。 所以建立一个实体类用于记录按下的点和放开的点:

    public class Box { private PointF mOrigin; private PointF mCurrent; public Box(PointF origin) { mOrigin = origin; mCurrent = origin; } }

    然后重写 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); return true; }

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

    应用启动时,所有视图都处于无效状态。也就是说,视图还没有绘制到屏幕上。为解决这个问题,Android 调用了顶级 View 视图的 draw()方法。这会引起自上而下的链式调用反应。首先,视图完成自我绘制,然后是子视图的自我绘制,再然后是子视图的子视图的自我绘制,如此调用下去直至继承结构的末端。当继承结构中的所有视图都完成自我绘制后,最顶级 View 视图也就生效了。 为加入这种绘制,可覆盖以下 View 方法: protected void onDraw(Canvas canvas) Canvas 和 Paint 是 Android 系统的两大绘制类。 - Canvas 类拥有我们需要的所有绘制操作。其方法可决定绘在哪里以及绘什么,比如线条、 圆形、字词、矩形等。 - Paint 类决定如何绘制。其方法可指定绘制图形的特征,例如是否填充图形、使用什么字 体绘制、线条是什么颜色等。

    public BoxDrawingView(Context context, AttributeSet attrs) { 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); } }

    3. 挑战练习

    3.1 设备旋转问题

    首先,要给整个视图加上 ID,onSaveInstanceState()以及onRestoreInstanceState()方法才会被调用使用 Bundle 传递需要存储的参数 @Override protected Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // 存储父类需要存储的内容 Parcelable superData = super.onSaveInstanceState(); bundle.putParcelable(KEY_SUPER_DATA, superData); // 存储所有的矩形 bundle.putSerializable(KEY_BOXEN, (ArrayList) mBoxen); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { Bundle bundle = (Bundle) state; // 取出父类的内容 Parcelable superData = bundle.getParcelable(KEY_SUPER_DATA); // 取出存储的矩形 mBoxen = (List<Box>) bundle.getSerializable(KEY_BOXEN); super.onRestoreInstanceState(superData); invalidate(); }

    3.2 旋转矩形框

    在处理多点触控时我们需要用 MotionEvent.getActionMasked() 方法来获取事件 ID,ACTION_POINTER_DOWN指的是屏幕上已经有手指了(无论是几根,最大不超过【多点触控屏的极限 - 1】),另一根手指按下的情况。也就是说此时我们能知道两个手指按下了。

    其次,图形的旋转一般是在绘制的时候旋转画布(canvas),需要的参数有旋转的角度(用度表示)以及旋转中心坐标,在这里我在 Box 类中加入了最开始的角度 mOriginAngle,已旋转后的角度 mRotatedAngle 两个成员变量,以及一个获取中心点坐标的方法。

    public class Box { private PointF mOrigin; private PointF mCurrent; // 此次按下时的角度 private float mOriginAngle; private float mRotatedAngle; // 已旋转的角度 public Box(PointF origin) { mOrigin = origin; mCurrent = origin; mOriginAngle = 0; mRotatedAngle = 0; } /** 省略 Getter 和 Setter **/ // 获取矩形的中心点 public PointF getCenter() { return new PointF( (mCurrent.x + mOrigin.x) / 2, (mCurrent.y + mOrigin.y) / 2); } }

    对不同的触摸情况进行处理:

    @Override public boolean onTouchEvent(MotionEvent event) { PointF current = new PointF(event.getX(), event.getY()); String action = ""; // 省略没有变化的部分 switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: action = "POINTER_DOWN"; if (event.getPointerCount() == 2) { // 首先获取按下时的角度(有一个弧度转角度的过程) // 每次按下的时候将角度存入现在矩形的原始角度 float angle = (float) (Math.atan((event.getY(1) - event.getY(0)) / (event.getX(1) - event.getX(0))) * 180 / Math.PI); mCurrentBox.setOriginAngle(angle); } break; case MotionEvent.ACTION_MOVE: action = "ACTION_MOVE"; if (mCurrentBox != null) { // 如果只有一只手指按下,而且还未曾旋转过的话,就进行大小的缩放 if (event.getPointerCount() == 1 && mCurrentBox.getRotatedAngle() == 0) { mCurrentBox.setCurrent(current); } // 如果按下了两根手指 if (event.getPointerCount() == 2) { // 获取角度 float angle = (float) (Math.atan((event.getY(1) - event.getY(0)) / (event.getX(1) - event.getX(0))) * 180 / Math.PI); Log.i(TAG, "onTouchEvent: angle:" + (angle - mCurrentBox.getOriginAngle())); // 已旋转的角度 = 之前旋转的角度 + 新旋转的角度 // 新旋转的角度 = 本次 move 到的角度 - 手指按下的角度 mCurrentBox.setRotatedAngle(mCurrentBox.getRotatedAngle() + angle - mCurrentBox.getOriginAngle()); // 旋转角度变化后,初始角度也发生变化 mCurrentBox.setOriginAngle(angle); } invalidate(); } break; } return true; }
    转载请注明原文地址: https://ju.6miu.com/read-39509.html

    最新回复(0)