Android 编程技巧之 ----- 自定义 View 踩坑总结

来源:互联网 发布:红猫网络加速器 编辑:程序博客网 时间:2024/05/16 06:31

  • 前言
  • 踩坑一 构造函数调用时机
  • 踩坑二 嵌套调用构造函数
    • 知识延伸 推荐链接
  • 踩坑三 清单属性
    • 知识延伸 推荐链接
  • 踩坑四 自定义 View 更新 UI
    • 知识延伸 推荐链接
  • 踩坑五 触摸屏事件传递机制
  • 结语

前言

一直以来对 Android 中的自定义 View 这一块都抱有恐惧之心, 潜意识中认为自定义 View 肯定是融合了各式各样的高深公式原理啥的(牛逼炫酷的 View 确实用到了很多高深的数学知识), 所以这方面一直都得不到提高, 渐渐也就成了自己的软肋, 现在这种市场环境, 再不对自己狠点估计就要饿死了.

嗯, 下定决心要把它捡起来啃, 所以就慢慢开始挖坑跳坑填坑了.
跳的一些坑, 有些是因为无意中手抖, 有些是为了自己测试, 而剩下的那些真的就是自己脑子短路了.

以下仅作为个人学习简记, 原理性的源码分析类的好文章网上很多, 可自行搜素, 本文也会给出推荐链接, 而本文只贴出一些自定义 View 需要注意的知识结论.


踩坑一 ~ 构造函数调用时机

注意 : 很多时候, 项目中都是直接在 XML 中设定自定义 View 的各种属性, 位置和样式等, 这种情况下, Android 在解析 XML 布局文件时会自动帮我们调用相应的 View 的构造函数 .

还记得第一次尝试自定义 View 时, 新建类后, 继承自 View 类, Android Studio 提示未生成构造函数, 自动修复补全后, 弹窗可选四个构造函数, 据看过的源码, 选择了前三个构造函数 :

    public CustomView(Context context) {        super(context);    }    public CustomView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }

因为不熟悉几种构造函数什么情况下会被调用, 没分析源码, 看着上面那种阵仗, 直接就把各种 Paint , 各种 Shader 的初始化操作放在第三个构造函数中了, 最后编译出来的结果就是, 一打开包含这个自定义 View 的界面就 crash 掉. 最初找原因找大半天, 一直以为这些构造函数会被嵌套调用到才对, 还真是太自以为是了呢.

其实在 XML 布局文件中引入自定义 View 时调用的是第二个构造函数. 源码原理分析网上可以自己搜索查看, 至于构造函数调用时机, 这里给出结论 :

  1. 在代码中直接 new 一个 CustomView 对象的时候,会调用第一个构造函数;

  2. 在 XML 布局文件中引入 CustomView 的时候,会调用第二个构造函数; (不论是否含自定义属性)

  3. 第三个构造函数一般是自己在构造函数中主动嵌套调用的.

出现这种情况 (无嵌套调用就把初始化操作置于第三个构造函数), 如果用 Android Studio 的 Log 来追踪 Exception 是没办法追踪到的 (压根就没 try-catch ), 其实最简单的方法就是可以直接进入 XML 布局文件看动态界面预览图, 既然各种 Paint, 各种 Shader 都没有初始化, 若 onDraw() 方法中用到了它们, 此时就会渲染失败, 弹出错误信息, 这里就可以根据错误堆栈去查找代码错误. 比如下图 :

XML布局文件抛异常


踩坑二 ~ 嵌套调用构造函数

自定义 View 的构造函数的一般写法是嵌套调用形式, 如下 :

    public CustomView(Context context) {        this(context, null);    }    public CustomView(Context context, AttributeSet attrs) {        this(context, null, 0);    }    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        //initialization...    }

嗯, 当完成自定义 View 的所有功能运行起来后, 进到包含这个自定义 View 的界面, 会发现它又 crash 了, 又 crash 了 ! 细心的朋友肯定已经发现不对劲的地方了, 仔细看, 嵌套调用 第二个 构造函数那里的 第二个 参数, 竟然把它写成了 null, 简直不能再有病了 ! 说实在的, 当时自己是为了贪图方便, 直接复制的第一个构造函数的内容, 然而到最后却忘了修改参数. 这也算是对自己的一次教训吧, 结论 :

  1. 书写代码时, 尽量少用复制粘贴, 虽然不想承认, 但是有些微小的 bug 确是这种动作导致的;

  2. 如果有复制粘贴的强迫症, 一定要细心加小心加耐心, 养成复制完后一个一个参数检查的习惯;

  3. 复制粘贴多了记得优化代码结构. (嗯, 这是废话)

知识延伸, 推荐链接

自定义 View 的构造函数详解推荐链接 :

Android自定义View构造函数详解, 作者 : 低调小一.


踩坑三 ~ 清单属性

说实话, 这种低能的错误不太想说, 但是写出来, 会让自己印象更加深刻, 顺便长长记性也是好的.

好不容易弄完了个自定义 View, 搞了个 Activity, 弄了个 Layout 文件, 心急火燎地就想看看炫酷的效果, 结果又 crash 了, 还以为自己又在自定义 View 哪一步出错了, 这里检那里查, 又是大半天过去, 并未发现哪里有错误.

很明显, 漏了最后也是最重要的一步, 清单文件中声明 Activity, 其实类似这种没有声明组件, 没有声明相关权限啥的, 有时候报出的错误会非常诡异, 这种没有声明组件是直接 crash 掉, 目前暂不清楚有什么可以帮忙追踪这种小错误的方法. 结论 :

  • 声明组件, 权限或者功能时, 最好是一旦创建类或者一旦觉得需要, 就先去清单文件写上, 再回过头开发, 这是一种好习惯.

知识延伸, 推荐链接

Android 清单属性详解链接 :

Android Developer 官网 (科学上网)

Android Developer 国内官网 (直接访问)


踩坑四 ~ 自定义 View 更新 UI

注意 : Android 中的 UI 是线程不安全的, Activity 在回调 onResume 生命周期函数后就不再允许非 UI 线程更新 UI 了.

众所周知, 在自定义 View 过程中, Android 主要为我们提供了两个更新 UI 的函数, invalidate() 和 postInvalidate() :

  1. 前者主要用于主线程, 即 UI 线程更新 UI时调用;

  2. 后者主要用于子线程, 即非 UI 线程更新 UI 时调用.

记忆方法, post 可以理解为推送的意思, 从子线程推送到 UI 线程要求更新 UI (其实还是主线程在更新 UI), 而 UI 线程不需要这种推送, 直接更新, 即 invalidate().

想必都知道了, 踩的坑就是在子线程调用了 invalidate() 方法, 再次导致 App 崩溃了, 这种错误也是比较头疼的, 暂不清楚如何追踪这种错误. 结论 :

  • 自定义 View 需要更新 UI 时, 先确定代码上下文所处线程, 再确定所选用方法.

知识延伸, 推荐链接

自定义 View 原理源码解析, 含上述两个方法的详解 :

Android应用层View绘制流程与源码分析, 作者 : 工匠若水.


踩坑五 ~ 触摸屏事件传递机制

注意 : 一般自定义 View 需要关心的触摸事件从 Activity 到 ViewGroup 到 View 一级级传下来, 可拦截, 可消费.

本来想实现手指从屏幕左边缘往右滑 finish 掉 Activity 的功能, 将触摸监听器设置给了 Layout 布局文件最外层的 ViewGroup(宽高都为 match_parent), 该布局文件中包含了一个 ListView (宽高都为 match_parent, 未添加触摸监听器 ), 一个固定宽高的自定义 View , 然后在 ViewGroup 的监听回调方法 onTouch() 中实现上述逻辑.

App 运行起来后, 发现无论怎么滑动, 都不见 Activity 被 finish() 掉, 再三检查滑动逻辑, 发现并无错误, 最后发现是触摸事件并没有提供给该 ViewGroup 消费(传递到了, 但是没有机会消费).

这里先给出触摸屏事件传递的结论, 结论引用自 :

引用作者 工匠若水.

Android触摸屏事件派发机制详解与源码分析一(View篇)

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

Android触摸屏事件派发机制详解与源码分析三(Activity篇)

对于 ViewGroup 来说,

  1. Android 事件派发是先传递到最顶级的 ViewGroup, 再由ViewGroup 递归传递到 View 的;

  2. 在 ViewGroup 中可以通过 onInterceptTouchEvent 方法对事件传递进行拦截, onInterceptTouchEvent 方法返回 true 代表不允许事件继续向子 View 传递, 返回 false 代表不对事件进行拦截, 默认返回false;

  3. 子 View 中如果将传递的事件消费掉, ViewGroup 中将无法接收到任何事件.


对于 View 来说,

  1. 触摸控件 (View) 首先执行 dispatchTouchEvent 方法;

  2. 在 dispatchTouchEvent 方法中先执行 onTouch 方法,后执行 onClick 方法; (onClick方法在onTouchEvent中执行)

  3. 如果控件 (View) 的 onTouch 返回 false 或者 mOnTouchListener 为 null (控件没有设置 setOnTouchListener 方法) 或者控件不是 enabled 的情况下会调用 onTouchEvent, 而 dispatchTouchEvent 的返回值与 onTouchEvent 的返回值一样;

  4. 如果控件不是 enabled 的设置了 onTouch 方法也不会执行, 只能通过重写控件的 onTouchEvent 方法处理, dispatchTouchEvent 的返回值与 onTouchEvent 的返回值一样;

  5. 如果控件 (View) 是 enabled 且 onTouch 返回 true 的情况下, dispatchTouchEvent 直接返回 true, 不会调用 onTouchEvent 方法;

  6. 当 dispatchTouchEvent 在进行事件分发的时候,只有前一个 action 返回 true, 才会触发下一个 action (也就是说, dispatchTouchEvent 返回 true 才会进行下一次 action 派发).

根据以上结论, 看看 ListView 源码 (API 25), 分析是否触摸事件被 ListView 消费掉了, 经过查找, 并未发现 ListView 有显式的 dispatchTouchEvent 方法和 onTouchEvent 方法, 但是看到了 ListView 继承自 AbsListView, 进入 AbsListView 源码, 没有发现 dispatchTouchEvent 方法, 却发现了 onTouchEvent 方法, 如下 : (只关注 return 值)

    @Override    public boolean onTouchEvent(MotionEvent ev) {        if (!isEnabled()) {            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return isClickable() || isLongClickable();        }        ......        if (mIsDetaching || !isAttachedToWindow()) {            // Something isn't right.            // Since we rely on being attached to get data set change notifications,            // don't risk doing anything where we might try to resync and find things            // in a bogus state.            return false;        }        ......        if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {            return true;        }        ......        return true;    }

回到自己的案例, 根据以上注释, 发现写的布局文件中的 ListView 并未设置 enabled 属性值, 默认为 true, 同时也是跟 window 连接着, 而非 detached 状态, 滑动时并未快速滚动, 所以是返回的最后一个 true, 果然, 触摸事件是被 ListView 消费掉了, 当然对于父 ViewGroup 就算实现了触摸监听逻辑, 也没机会消费触摸事件了.

至于解决办法, 相信有 工匠若水 大神的原理与结论结合指导, 此问题并不难解决, 就留给读者自己实现吧.


结语

全文到此就结束了, 非常感谢各位大神的好文章, 正是因为有了你们无私的奉献, 才会有更多的小白走得更快 (比如我, 哈哈), 本文只是提及了一些很小儿科的错误, 谨作为自己 Android 学习路上的一个小小的总结, 难免有理解不到位的地方, 欢迎指正, 谢谢阅读 !

最后, 再推荐一些个人认为也是非常不错的系列文章 :

自定义View原理&应用系列, 作者 : Carson_Ho

1 0