一.自定义View学习笔记(持续更新中。。。。)

       自定义View的内容很多,原本只是想写一篇博客,现在觉得我需要新建一个自定义View的文集了,这一篇主要是讲什么是自定义View,以及自定义View中的自定义属性,其中的几个方法。接下来几篇会写几个自定义控件。此文主要参考:

http://blog.****.net/xmxkf/article/details/51490283

【openXu的博客】


注意:

1.在ListView中,条目中如果有Button之类带点击效果的控件,那么必须要处理一下,不然它会抢走ListView的焦点,使ListView的ItemOnclick事件不生效。解决方法:在条目布局根节点声明一个属性:descendantFocusability,指的是:该条目内部子控件获取焦点的方式

它可以指定三个值,分别是:

1)afterDescendants  在条目获取之后,子控件才获取焦点

2)beforeDescendants 在条目获取焦点之前,子控件获取焦点

3)blocksDescendants 以区块的方式获取焦点,只有在点击到子控件所在的区块,子控件才会获取焦点。


2.PagerAdapter中需要子类重写的4个方法:



3.自定义控件的绘制是在界面打开之后的onCreate()方法之后绘制。

4.在自定义控件中调用系统方法:invalidate() 方法,会调用onDraw方法,使整个控件重新绘制,界面更新。

5.在自定义View中获取上下文,一般使用getContext()



自定义View分为三种:

1.组合已有控件实现自定义控件

2.完全自定义控件

3.继承已有控件,扩展其功能



1.组合已有控件实现自定义控件

旋转菜单效果图:


一.自定义View学习笔记(持续更新中。。。。)
旋转菜单效果图

点击home键和menu键分别让外层的相对布局转入和转出。

点击menu键,为第三层布局添加动画。第三层布局显示,将第三层转出去,第三层布局隐藏,转进来。使用补间动画

1.转出动画:

一.自定义View学习笔记(持续更新中。。。。)
转出动画

2.转入动画:

一.自定义View学习笔记(持续更新中。。。。)
转入动画
一.自定义View学习笔记(持续更新中。。。。)
分析

1.旋转中心点:

相对于自己旋转,控件的长宽在坐标轴上均为1,中心点如上图红色点所示,点坐标是(0.5f,1f)

2.逆时针旋转,角度递减。顺时针旋转,角度递增。

3.点击home键,旋转第二层,点击menu键,旋转第三层。

在旋转时需要判断是转入,还是转出。如果布局显示,则转出,布局隐藏则转入。

4.点击home键时需要判断,第三层是否显示在屏幕上,如果第三层显示,先把第三层转出去。

这种情况,第二层布局需要添加延时执行动画,否则第二层第三层布局会同时转出。设置延时执行动画代码:

raOutAnimation.setStartOffset(100);//设置动画启动延时

在第二层旋转动画前加一个判断:如下:


一.自定义View学习笔记(持续更新中。。。。)

5.设置动画执行延时,是否延时,要看外面那一级的菜单是否显示。设置一个变量,如果外面那层显示,将变量添加200毫秒

如果三级菜单显示,则delay+=200;如果三级菜单没显示,delay仍为0.


一.自定义View学习笔记(持续更新中。。。。)


6.为了避免动画重复执行,设置一个变量,动画本身添加监听,动画开始执行时,将变量值++,动画执行结束,变量值- -,执行动画之前判断,如果变量值>0.则return;


一.自定义View学习笔记(持续更新中。。。。)

7.手机键盘menu键,点击之后,三层布局全部执行转入转出动画。重写onKeyDown方法。


一.自定义View学习笔记(持续更新中。。。。)


8.bug修复:

补间动画缺点,虽然布局转出去了,但是其实控件仍然在原来的位置,点击menu键原来的位置,第三层仍然会执行动画。

解决方法:转出动画时把按钮的点击事件屏蔽掉,转入动画再解除屏蔽。


一.自定义View学习笔记(持续更新中。。。。)
屏蔽


一.自定义View学习笔记(持续更新中。。。。)
启用



2.完全自定义控件:继承自View


绘制完全自定义控件步骤:

1.继承View,覆盖构造方法

2.自定义属性

3.重写onMeasure方法测量宽高

4.重写onDraw方法绘制控件


1.继承View,覆盖构造方法

因为View类不具有无参的构造函数,因此,自定义View需要重写其构造方法,一般重写三个构造方法,但此处,把第四个构造方法也详细的记录一下:

1)带一个参数的构造方法:从代码创建时走此构造方法,用于代码创建。如:newTextView(mContext);

源码中的解释:Simple constructor to use when creating a view from code.

2) 带两个参数的构造方法:在xml中使用时走此构造方法,可用于指定自定义属性。

源码中的解释:Constructor that is called when inflating a view from XML. This is called 

                            when a view is being constructed from an XML file, supplying attributes

                            that were specified in the XML file. This version uses a default style of

                           0, so the only attribute values applied are those in the Context's Theme

                           and the given AttributeSet.

如:这样几行代码,只在带两个参数的构造方法中打印一段话,并且将其attrs也打印出来看看

public SwitchButtonView(Context context, @Nullable AttributeSet attrs){

                  super(context, attrs);

                  Log.i("带两个参数的构造方法", "SwitchButtonView: ");

                   for(int i=0;i<attrs.getAttributeCount();i++){

                                         Log.d("带两个参数的构造方法attrs", attrs.getAttributeName(i)+" : "+attrs.getAttributeValue(i));

                              }

             }

可以看到打印的结果:

一.自定义View学习笔记(持续更新中。。。。)

可以看到,当我们直接在xml文件中使用,在类中绑定时,走的时带两个参数的构造方法,打印出的attrs,正是我们指定的属性,因此,在这个方法中,可以获取用户输入的自定义属性的值。

注意:如果xml中指定了样式,走的仍然是这个构造方法。也就是说,系统默认只会调用Custom View的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数).

3)带三个和四个参数的构造方法:通常是在第二个构造函数中调用,一般用来获取用户的自定义属性。

获取自定义属性的代码:

public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {

       super(context, attrs, defStyleAttr);

      TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);

      String attr1 = ta.getString(R.styleable.MyCustomView_custom_attr1);

      String attr2 = ta.getString(R.styleable.MyCustomView_custom_attr2);

      String attr3 = ta.getString(R.styleable.MyCustomView_custom_attr3);

      String attr4 = ta.getString(R.styleable.MyCustomView_custom_attr4);

      Log.e("customview", "attr1=" + attr1);

       Log.e("customview", "attr2=" + attr2); Log.e("customview", "attr3=" + attr3); Log.e("customview", "attr4=" + attr4);

     ta.recycle();

}

使用TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);这句代码获取自定义属性,通过对源码的追踪我们发现:


一.自定义View学习笔记(持续更新中。。。。)


一.自定义View学习笔记(持续更新中。。。。)


一.自定义View学习笔记(持续更新中。。。。)

最终调用了Theme中的obtainStyledAttributes带有4个参数的构造方法:

1.AttributeSet set: 属性值的集合.

2.int[] attrs:  我们自定义属性集合在R类中生成的int型数组.这个数组中包含了自定义属性的资源ID.

3.int defStyleAttr:

这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defStyleAttr中查找默认值.

4.int defStyleRes: 这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用.

由于一个属性可以在很多地方对其进行赋值,包括: XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:

属性赋值优先级次序表:

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

2.自定义属性

当我们自定义一个View时,可以通过自定义属性来更改View的一些如字体大小,背景图片等属性。那么自定义属性怎么用的呢?

1.首先,在attrs.xml文件中定义一个resource,其中可以填写任何我们想要设置的属性,format的意思是该属性的取值是什么类型(支持的类型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag) 如图:


一.自定义View学习笔记(持续更新中。。。。)

这是我在自定义toolbar中定义的自定义属性,其中rightButtonIcon代表的是右侧按钮图标,类型是reference,意思是引用类型。

format有11中类型,其中无法从字面直接获取其意思的几个详细记录一下:

reference:参考某一资源ID,如图片等的设置

dimension:尺寸值 如设置宽高

fraction:百分数

enum:枚举值 如线性布局中设置方向

一.自定义View学习笔记(持续更新中。。。。)

flag:位或运算


一.自定义View学习笔记(持续更新中。。。。)


注意,在xml文件中使用自定义属性是不要忘记命名空间。

xmlns:openxu="http://schemas.android.com/apk/res-auto"

2.在构造方法中获取用户输入的自定义属性。并在CustomView.java中编写相关方法,用来更改控件的属性。如同我们上面所讲的,在带有三个参数的构造方法中获取用户输入的自定义属性。如下图


一.自定义View学习笔记(持续更新中。。。。)

上图以右侧按钮图标为例,在View中编写setRightButtonIcon()方法。setRightButtonIcon()方法是用来把用户输入的自定义属性设置到右侧按钮上的。


一.自定义View学习笔记(持续更新中。。。。)

这样就完成了给控件设置自定义属性。

3.重写onMeasure方法测量宽高

关于View在官方文档中的解释:

View这个类代表用户界面组件的基本构建块。View在屏幕上占据一个矩形区域,并负责绘制和事件处理。View是用于创建交互式用户界面组件(按钮、文本等)的基础类。它的子类ViewGroup是所有布局的父类,它是一个可以包含其他view或者viewGroup并定义它们的布局属性的看不见的容器。 实现一个自定义View,你通常会覆盖一些framework层在所有view上调用的标准方法。你不需要重写所有这些方法。事实上,你可以只是重写onDraw(android.graphics.Canvas)。

一.自定义View学习笔记(持续更新中。。。。)

Android界面的绘制流程:

View:重写以下方法

onMeasure()(该方法用于指定自己的宽高)---->onDraw()(该方法用于绘制自己的内容)

ViewGroup:重写以下方法

onMeasure()(该方法用于指定自己的宽高,子view的宽高)---->onLayout()(摆放所有的子View)----->onDraw()(绘制内容)

以上的这些方法,均在Activity或者Fragment、View等使用该控件的类中的onResume()方法之后执行。

好了,一个一个来解决:

onMeasure()方法:测量,也就是控制View的大小

测量:我们在写onMeasure方法时,通常会这么写:

 int widthMode = MeasureSpec.getMode(widthMeasureSpec);         

  int heightMode = MeasureSpec.getMode(heightMeasureSpec);         

  int widthSize = MeasureSpec.getSize(widthMeasureSpec);          

 int heightSize = MeasureSpec.getSize(heightMeasureSpec);  

用来获取宽高,那么MeasureSpec究竟是什么呢?跟踪一下源码,发现它是View中的一个静态内部类,是由尺寸和模式组合而成的一个值,用来描述父控件对子控件尺寸的约束,看看他的部分源码,一共有三种模式,然后提供了合成和分解的方法:


一.自定义View学习笔记(持续更新中。。。。)

可以看到,其中有三种约束,UNSPECIFIED  EXACTLY  AT_MOST

一.自定义View学习笔记(持续更新中。。。。)

当控件宽高设置为match_parent或者是具体宽高值的时候,模式为EXACTILY。

当控件宽高设置为warp_content时,模式为AT_MOST。

那么举个例子,来重写onMeasure方法:


一.自定义View学习笔记(持续更新中。。。。)

http://blog.****.net/xmxkf/article/details/51490283

一.自定义View学习笔记(持续更新中。。。。)

onMeasure()方法中调用了setMeasuredDimension(宽,高)方法,该方法时用来设置自定义控件的宽高,




完全自定义控件例子:自定义开关

1.绘制界面内容

2.响应触摸事件

3.接口监听

1.绘制界面内容

1)定义ToggleView继承View,重写View的三个构造方法

2).绘制界面内容,界面由两张图片组成,前景和背景。如下图:


一.自定义View学习笔记(持续更新中。。。。)
自定义开关

在ToggleView类中,设置三个方法:

1.setToggleBackground(int background):设置开关背景

toggleBackground = BitmapFactory.decodeResource(getResources(), background);把用户传入的background转化成bitmap,通过onDraw方法画到控件上

2.setToggleForeground(int foreground):设置开关前景

toggleForeground = BitmapFactory.decodeResource(getResources(), foreground);同上

3.setToggleStatus(Boolean open):设置开关状态。通过用户输入的boolean值设置开关状态

3).重写onMeasure方法,设置控件的宽高

设置控件宽高,与背景图片一样宽、高:setMeasuredDimension(toggleBackground.getWidth(), toggleBackground.getHeight());

4).重写onDraw方法,把图片绘制到控件上,绘制的内容都会显示到控件上

//1.绘制背景

canvas.drawBitmap(toggleBackground,0,0,paint);//中间两个参数是距离控件原点(左上角坐标)的x、y轴距离

//2.根据开关状态绘制前景

if(isopen){

//开

    //获取前景移动距离

    int i =toggleBackground.getWidth() -toggleForeground.getWidth();

    canvas.drawBitmap(toggleForeground,i,0,paint);

}else{

//关

    canvas.drawBitmap(toggleForeground,0,0,paint);

}

控件内容绘制完成

2.响应触摸事件

重写View的onTouchEvent()方法:返回值改为true,控件才会消费用户的点击事件。

在触摸事件中,获取用户手指的X坐标,通过更新前景图标左上角的X坐标来更新控件。在onTouchEvent调用invalidate()方法,每次触摸都会重新绘制。在手指抬起时,根据前景图的坐标判断开关是什么状态。

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

//按下

            /**

            * 按下时,将isTouchMode改为true,更改currentX

*/

            currentX = event.getX();

            isTouchMode =true;

                        break;

        case MotionEvent.ACTION_MOVE:

                         //移动

            currentX = event.getX();

            /**

            * 移动时,更改currentX,通过调用invalidate()重绘界面

            */

                             break;

        case MotionEvent.ACTION_UP:

                    //抬起

              currentX = event.getX();

            /**

            * 抬起时,将isTouchMode设置为false

*/

               boolean state = false;

                 if(currentX < center){

                         //在中间值左边 关

                              state = false;

                              }else if(currentX > center){

                               state = true;}

                              isopen = state;//把state的值赋值给开关状态

                     isTouchMode =false;

                             break;

    }

              invalidate();//调用该方法,每次触摸时都重新绘制控件

                return true;//必须更改为true,控件才能消费掉用户的触摸事件

}

在onDraw()方法中绘制界面:如果是触摸模式,根据触摸坐标来绘制界面;否则,根据开关状态绘制界面。设置左右边界,不允许前景图片超过边界,

@Override

protected void onDraw(Canvas canvas) {

//1.绘制背景

    canvas.drawBitmap(toggleBackground,0,0,paint);

    //根据用户触摸坐标来绘制画面

    if(isTouchMode){

currentX =currentX -toggleForeground.getWidth()/2.0f;//移动的坐标需要比手指点下的坐标左移半个前景图片大小,这样看起来就是点击在开关中间

        //容错处理:设置左右边界

        float maxLeft =toggleBackground.getWidth() -toggleForeground.getWidth();//开关能移到右侧的最大值

        if(currentX<0){

currentX =0;

        }else if(currentX>maxLeft){

currentX = maxLeft;

        }

canvas.drawBitmap(toggleForeground,currentX,0,paint);

    }else{

//根据开关状态绘制前景

        if(isopen){

//开

            //获取前景移动距离

            int i =toggleBackground.getWidth() -toggleForeground.getWidth();

            canvas.drawBitmap(toggleForeground,i,0,paint);

        }else{

//关

            canvas.drawBitmap(toggleForeground,0,0,paint);

        }}}

3.接口监听

当用户操作自定义控件时,自定义控件内部需要通知外部(界面,程序)我的状态改变了,并把状态的boolean变量传出去。

// 1. 声明接口对象

public interface OnSwitchStateUpdateListener{

// 状态回调, 把当前状态传出去

void onStateUpdate(boolean state);

}

// 2. 添加设置接口对象的方法, 外部进行调用

public void setOnSwitchStateUpdateListener(

OnSwitchStateUpdateListener onSwitchStateUpdateListener) {

this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;

}

// 3. 在合适的位置.执行接口的方法

onSwitchStateUpdateListener.onStateUpdate(state);

// 4. 界面/外部, 收到事件.

sbv.setOnSwitchStatusChangeListener(new SwitchButtonView.OnSwitchStatusChangeListener(){

@Override

    public void onStatusChange(boolean status) {

Toast.makeText(mContext,"开关状态为"+status,Toast.LENGTH_SHORT).show();

    }

});


手指抬起时,如果state的值改变了,那么说明开关状态改变了。因为state的值最后是赋值给了isopen,在赋值之前判断,如果两者不同,就说明开关状态改变,那么在此时执行3中的方法

if(state!=isopen&&onSwitchStatusChangeListener!=null){

onSwitchStatusChangeListener.onStatusChange(state);//这句代码执行时,外部(界面上该控件的该接口的监听被调用)

}