Android 自定义View

来源:互联网 发布:淘宝美工用哪个ps软件 编辑:程序博客网 时间:2024/06/03 15:33

在 Android 系统中,为开发者提供了很多的控件。但如果想要做出绚丽的界面效果仅仅依靠这些系统控件是远远不够的,这个时候就必须通过自定义 View 来实现这些绚丽的效果,自定义 View 是一个综合的技术体系,它涉及 View 的层次结构、事件分发机制和 View 的工作原理等技术细节。本文以综述的形式介绍自定义 View 的分类和须知,旨在帮助大家更好地理解自定义 View。

/** * 本文内容来自 《Android开发艺术探索》 */

一、自定义 View 的分类

1.1 继承 View 重写 onDraw 方法
  这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写 onDraw() 方法。采用这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。

1.2 继承 ViewGroup 派生特殊的 Layout
  这种方法主要用于实现自定义布局,即除了 LinearLayout、RelativeLayout、FrameLayout 这几种系统布局之外,我们重新定义一种新布局,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

1.3 继承特定的 View(比如 TextView)
  这种方法比较常见,一般用于扩展某种已有的 View 功能,比如 TextView,这种方法比较容易实现。这种方法不需要自己支持 wrap_content 和 padding 等。

1.4 继承特定的 ViewGroup(比如 LinearLayout
  这种方法也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量、布局这两个过程。需要注意这种方法和方法2 的区别,一般来说方法2 能实现的效果方法4 也能实现,两者主要差别在于方法2 更接近底层。


二、自定义 View 须知
下面介绍自定义 View 过程中的一些注意事项,这些问题处理不好,会影响 View 的正常使用,而有些则会导致内存泄露等。具体注意事项如下:

2.1 让 View 支持 wrap_content
  这是因为直接继承 View 或者 ViewGroup 的控件,如果不在 onMeasure() 中对 wrap_content 进行特殊处理,那么当外界在布局中使用 wrap_content 时就无法达到预期的效果。

2.2 如果有必要,让 View 支持 padding 
  这是因为直接继承 View 的控件,如果不在 draw() 方法中处理 padding,那么 padding 属性是无法起作用的。另外,直接继承自 ViewGroup 的控件需要在 onMeasure() 和 onLayout() 中考虑 padding 和子元素的 margin 对其造成的影响,不然将导致 padding  和子元素的 margin 失效。

2.3 尽量不要在 View 中使用 Handler
  这是因为 View 内部本身就提供了 post 系列的方法,完全可以替代 Handler 的作用,当然除非你很明确地要使用 Handler 来发送消息。

2.4 View 中如果有线程或者动画,需要及时停止,参考 View 的 onDetachedFromWindow
  如果有线程或者动画需要停止时,那么 onDetachedFromWindow() 是一个很好的时机。当包含此 View 的 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow() 方法会被调用,和此方法对应的是 onAttachedToWindow(),当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow() 方法会被调用。同时,当 View 变得不可见时也需要停止线程和动画,如果不及时处理这种问题,有可能造成内存泄露。

2.5 View 带有滑动嵌套情形时,需要处理好滑动冲突
  如果有滑动冲突的话,那么要合适地处理滑动冲突,否则将严重影响 View 的效果。


附三:获取 View 的宽高问题
比如我们想在 Activity 已启动的时候就做一件任务,但是这件任务需要获取某个 View 的宽/高。而实际上在 onCreate()、onStart()、onResume() 中均无法正确得到某个 View 的宽/高信息,这是因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了 onCreate()、onStart()、onResume() 时某个 View 已经测量完毕了,如果 View 还没有测量完毕,那么获取的宽/高就是 0。关于这个问题,下面提供 4 种解决方法。

3.1 Activity/View 的 onWindowFocusChanged
onWindowFocusChanged() 方法的含义就是:View 已经初始化完毕了,宽/高已经准备好了,这时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged() 会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。核心代码如下:
    public void onWindowFocusChanged(boolean hasFocus) {        super.onWindowFocusChanged(hasWindowFocus);        if (hasFocus) {            int width = view.getMeasuredWidth();            int height = view.getMeasuredHeight();        }    }

3.2 view.post(runnable)
通过 post 可以将一个 runnable 投递到消息列队尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。核心代码如下:
    protected void onStart() {        super.onStart();        view.post(new Runnable() {            @Override            public void run() {                int width = view.getMeasuredWidth();                int height = view.getMeasuredHeight();            }        });    }

3.3 ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout() 方法将被回调,因此这是获取 View 的宽/高一个很好的时机。需要注意的是,伴随着 View 树的状态改变等,onGlobalLayout() 会被多次调用。核心代码如下:
    protected void onStart() {        super.onStart();        ViewTreeObserver observer = view.getViewTreeObserver();        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){            @SuppressWarnings("deprecation")            @Override            public void onGlobalLayout() {                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);                int width = view.getMeasuredWidth();                int height = view.getMeasuredHeight();            }        });    }

3.4 view.measure(int widthMeasureSpec, int heightMeasureSpec) 
通过手动对 View 进行 measure 来得到 View 的宽/高。这种方法比较复杂,这里要分情况处理,根据 View 的 LayoutParams 来分:

match_parent:
直接放弃,无法 measure 出具体的宽/高。因为构造此种 measure 需要知道 parentSize,即父容器的剩余空间,而这个时候无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小。

具体的数值(dp/px):
比如宽/高都是 100px,如下 measure
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);    view.measure(widthMeasureSpec, heightMeasureSpec);

wrap_content:

    int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);    int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);    view.measure(widthMeasureSpec, heightMeasureSpec);
注意到 (1 << 30) - 1,通过分析 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是说最大是 30 个 1(即 2^30 - 1),也就是(1 << 30) - 1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的。

3.5 关于 View 的 measure,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 错误),其次不能保证一定能 measure 出正确的结果。

第一种错误的用法
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);    view.measure(widthMeasureSpec, heightMeasureSpec);

第二种错误的用法

    view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);