美文网首页Android技能手册安卓UI
一.自定义View学习笔记(持续更新中。。。。)

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

作者: kim_liu | 来源:发表于2017-12-18 15:56 被阅读27次

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

    http://blog.csdn.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.组合已有控件实现自定义控件

    旋转菜单效果图:

    旋转菜单效果图

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

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

    1.转出动画:

    转出动画

    2.转入动画:

    转入动画 分析

    1.旋转中心点:

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

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

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

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

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

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

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

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

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

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

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

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

    8.bug修复:

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

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

    屏蔽 启用

    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));

                                  }

                 }

    可以看到打印的结果:

    可以看到,当我们直接在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);这句代码获取自定义属性,通过对源码的追踪我们发现:

    最终调用了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) 如图:

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

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

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

    dimension:尺寸值 如设置宽高

    fraction:百分数

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

    flag:位或运算

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

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

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

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

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

    3.重写onMeasure方法测量宽高

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

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

    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中的一个静态内部类,是由尺寸和模式组合而成的一个值,用来描述父控件对子控件尺寸的约束,看看他的部分源码,一共有三种模式,然后提供了合成和分解的方法:

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

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

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

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

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

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




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

    1.绘制界面内容

    2.响应触摸事件

    3.接口监听

    1.绘制界面内容

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

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

    自定义开关

    在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);//这句代码执行时,外部(界面上该控件的该接口的监听被调用)

    }

    相关文章

      网友评论

        本文标题:一.自定义View学习笔记(持续更新中。。。。)

        本文链接:https://www.haomeiwen.com/subject/kvcmwxtx.html