Android 自定义View学习(十二)——自定义View的分类

学习资料:


一般自定义一个View有4种思路:

  1. 直接继承View
  2. 直接继承ViewGroup
  3. 继承现有的View,例如ImageViweSurfaceView
  4. 继承现有的ViewGroup,例如LinearLayout

推荐优先考虑3或者4。原因也比较明显,无论哪一种方式,需要注意的事项都不少。采用3或者4的话,现有控件的一些特性可以直接拿来用,而且也更加容易做适配


1.自定义View注意事项 <p>

之前有一个误区,总觉得自定义View就得是直接继承View,然后自己实现各种效果


1.1 让View支持 wrap_content <p>

在之前的学习中,重写onMeasure()方法的目的就是为了支持wrap_content。原因在学习ViewViewGroup中,也进行了尝试说明,View是的大小是要受自身和父容器ViewGroup影响。想要让wrap_content拥有包裹内容的特性,就需要重写ViewViewGrouponMeausre()方法,针对要加载内容或者childView的宽高来设置自定义View的宽高。这也是为啥推荐方式3或者4的原因之一


1.2 根据需要,考虑Padding和Margin <p>

  1. 直接继承的ViewMargin是不用考虑的,之前的学习提到过,这个属性被封装进了LayoutParamas由父容器ViewGroup来处理。
    Padding属性需要根据需求在onMeasure()onDraw()方法中处理
  2. 直接继承的ViewGroup,在onMeadsure()方法中,ViewGroup自身的Margin属性是直接支持的。需要考虑的是封装进了LayoutParamas中的childViewMargin属性信息。利用childView拿到的LayoutParams中的Margin属性信息,在测量时就对childView的宽高进行处理。在onLayout()确定childView四个顶点位置时,将Margin属性信息也做处理。Padding属性的处理类似Margin,也需要在onMeausreonLayout()方法中,都进行处理

1.3 尽量不要在View中创建Handler <p>

View自身提供了post系列方法,里面封装的有Handler。除非需要明确使用Handlder来发送消息

注意:
post(new Runnable(){....})中的一系列操作依然是在主线程,最好不要进行耗时的操作。而执行网络请求会直接造成应用崩溃,给出android.os.NetworkOnMainThreadException异常


1.4 View中有线程后者动画,考虑关闭时机 <p>

Android 自定义View学习(九)——Bezier贝塞尔曲线学习中,处理过一次这样的情况

主要使用到一个onDetachedFromWindow()的方法。在View所在的Activity退出或者当前View自身被remove点时,ViewonDetachedFromWindow()方法便会被回调。在这个方法内,将正在运行的动画或者线程关闭掉,能够减少内存泄露的出现


1.5 View有嵌套时,考虑点击事件和滑动冲突

继承之一些可以滑动的ViewGroup后,常需要考虑滑动冲突。View的事件体系,下篇开始学习。

以上都是看Android开发艺术探索后做的总结


2. 直接继承View

虽然之前学习过程中,用过了十几次了,但每次都是按照固定套路来,并没有进行一些思考。

  1. 构造方法又都有啥啥区别?
  2. View有没有生命周期? 有
  3. ViewActivity的生命周期有啥联系没有? 有

这里之前搞错了。说ViewActivity的生命周期没有联系。View依赖于Activity,不可能没有联系。感谢,Android之路留言指出错误。但,具体啥联系,暂时不打算学习,想留在后面具体学习Activity时,再来补充


直接继承View后,代码:

public class LifecycleView extends View {
    private final String TAG = "LifecycleView";
    public LifecycleView(Context context) {
        super(context);
        Log.e(TAG,"&&&第1个构造方法");
    }

    public LifecycleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(TAG,"&&&第2个构造方法");
        for(int i=0;i<attrs.getAttributeCount();i++){
            Log.e(TAG, "&&&第2个构造方法"+attrs.getAttributeName(i)+"--->"+attrs.getAttributeValue(i));
        }
    }

    public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.e(TAG,"&&&第3个构造方法");
    }

    public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.e(TAG,"&&&第4个构造方法");
    }
}

打印出的Log信息:

Log信息

根据Log信息,很明显得出,当在xml布局文件中声明了LifecycleView,系统就会调用第2个构造方法,并且通过参数attrs可以拿到布局中,LifecycleView中的标签属性。布局中控件的属性就封装在AttributeSet

第1个构造方法是直接进行new LifecycleView(mContext)时,会被调用,但很少这样用

结论1:当在布局中使用自定义View时,调用第2个构造方法

可第3和第4构造方干嘛用的?


2.1 使用自定义属性 <p>

第3个构造方法,需要一个declare-styleable自定义属性

关于自定义属性可以看看鸿洋大神的Android 深入理解Android中的自定义属性

先在style.xnl布局文件中声明一个

 <declare-styleable name="CustomProperty">
    <attr name="number" format="integer" />
    <attr name="text" format="string" />
</declare-styleable>

定义好自定义属性后,LifecycleView中使用:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto">

    <com.szlk.customview.custom.LifecycleView
        custom:text="英勇青铜5"
        custom:number="521"
        android:layout_width="100dp"
        android:layout_height="100dp" />
</RelativeLayout>

修改LifecycleView代码:

public class LifecycleView extends View {
    private final String TAG = "LifecycleView";
    public LifecycleView(Context context) {
        super(context);
        Log.e(TAG,"&&&第1个构造方法");
    }

    public LifecycleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(TAG,"&&&第2个构造方法");
        for(int i=0;i<attrs.getAttributeCount();i++){
            Log.e(TAG, "&&&&&&第2个构造方法"+attrs.getAttributeName(i)+"--->"+attrs.getAttributeValue(i));
        }
    }

    public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.e(TAG,"&&&第3个构造方法");
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomProperty);
        String text = ta.getString(R.styleable.CustomProperty_text);
        int textAttr = ta.getInteger(R.styleable.CustomProperty_number, -1);
        Log.e(TAG,"&&&第3个构造方法"+text+"-----"+textAttr);
        ta.recycle();
    }

    public LifecycleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.e(TAG,"&&&第4个构造方法");
    }
}

加入自定义属性后的Log信息

本以为会调用第3个构造方法,Log信息却显示,调用的依然是第2个构造方法,但此时已经拿到了自定义的属性

结论2:在布局文件中使用自定义属性后,仍然调用的第2个构造方法


在别的博客看到,想要调用到第3和4个构造方法,只有在第2个构造方法使用this进行调用

再次对代码进行修改:

//构造方法1
this(context,null);
//构造方法2
this(context, attrs,0);
//构造方法3
this(context, attrs, defStyleAttr,0);
第2个构造方法调用第3和4个

此时就调用了第4和第3个构造方法,但最终还是会调用第2个构造方法

至于为啥这样设计构造方法,虽然看了几篇博客,我暂时依然没有想明白。难道是一开始系统默认会给一个属性,利用第3和第4个构造方法可以更改系统默认属性?


2.2 关于四个参数 <p>

第一个和第二个参数都比较容易理解。

  • 第3个参数int defStyleAttr ,当前Theme中的包含的一个指向style的引用

    默认值为0,是一个属性资源。没有使用第2个参数给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值。传入0表示不向该defStyleAttr中查找默认值,只要在主题中对这个属性赋值,该View就会自动应用这个属性的值。

  • 第4个参数int defStyleRes,也就是在布局文件中使用的style="@style/..."id

    只有在第三个参数defStyleAttr为0,或者主题中没有找到这个defStyleAttr属性的赋值时,才可以启用。
    1.defStyleRes: 指向一个style引用.
    2.defStyleRes的优先级低于defStyleAttr.


这里,看得有些懵,先记住结论

在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值

以上摘抄Android自定义View构造函数详解

这里还找了另外两篇:
Android View 四个构造函数详解
Android中自定义样式与View的构造函数中的第三个参数defStyle的意义

这里实在是没有搞清楚优先级,先挖坑了 : )


2.3 View的类似生命周期的方法 <p>

虽然View也有好多类似Activity那种会在不同时期进行调用的方法,但实际上View的生命周期就3个阶段:

  1. 处理控件动画阶段
  2. 处理测量的阶段
  3. 处理绘制的阶段

重写了几个方法:

public class LifecycleView extends View {
    private final String TAG = "LifecycleView";

    public LifecycleView(Context context) {
        this(context, null);
        Log.e(TAG, "&&&第1个构造方法");
    }

    public LifecycleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(TAG, "&&&第2个构造方法");
    }



    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Log.e(TAG, "&&&解析布局文件onFinishInflate()");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e(TAG, "&&&测量onMeasure()");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.e(TAG, "&&&布局onLayout()");
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e(TAG, "&&&控件大小发生改变onSizeChanged()");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG, "&&&绘制onDraw()");
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        Log.e(TAG, "&&&控件焦点改变onFocusChanged()");
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        Log.e(TAG, "&&&获取或者失去焦点onWindowFocusChanged()");
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.e(TAG, "&&&开始附加在屏幕上onAttachedToWindow()");
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
         Log.e(TAG, "&&&与屏幕失去连接onDetachedFromWindow()");
    }

    @Override
    protected void onAnimationStart() {
        super.onAnimationStart();
        Log.e(TAG, "&&&动画开始onAnimationStart()");
    }

    @Override
    protected void onAnimationEnd() {
        super.onAnimationEnd();
        Log.e(TAG, "&&&动画结束onAnimationEnd()");
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        Log.e(TAG, "&&&可见性发生改变onWindowVisibilityChanged()");
    }
}

加载LifecycleView所在的Activity时:

显示LifecycleView

onFinishInflate()这个方法进行解析Viewxml文件,如果使用构造方法1,就不会回调这个方法。测量方法和布局方法会被回调好几次。而onDraw()竟然也回调了两次,一直觉得会被调用一次


点击回退键,结束掉Activity时:

关闭LifecycleView所在的Activity

关闭时倒是比较容易理解


3. 最后 <p>

关于构造方法34具体在哪种情景下会被调用,在哪个具体时机被调用没有搞明白,暂时决定先搁下,下篇打算开始学习事件体系。有知道的,请告诉一下

继承ViewGroup的知识点,在上篇已经有所了解。而继承现有的ViewViewGroup,和直接继承ViewViewGroup大体思路是一致的,甚至还简单一点

View的事件分发,涉及到责任链模式

本人很菜,有错误,请指出

共勉 : )

推荐阅读更多精彩内容