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

本章主要讲了自定义 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 设备旋转问题

  1. 首先,要给整个视图加上 ID,onSaveInstanceState()以及onRestoreInstanceState()方法才会被调用
  2. 使用 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 旋转矩形框

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

  2. 其次,图形的旋转一般是在绘制的时候旋转画布(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);
    }

}


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

    ```java
    @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;
    }
    ```
    
---
GitHub Page: [kniost.github.io](http://kniost.github.io)
简书:[http://www.jianshu.com/u/723da691aa42](http://www.jianshu.com/u/723da691aa42)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容