Android自定义控件,故名思议,就是自己定义的控件。Android原生给我们提供了很多控件,像:TextView,EditText,ImageView等。
虽然大多数情况下都能够满足我们的需求,但是有时需要的效果,Android并没有提供,这个时候就需要我们自己来定义一个View了。并且,
Android原生的效果,用的多了,用户天天看,都让用户形成视觉疲劳了,所以现在开发的项目中,大部分都是自定义控件,用到的还是非常多的。

话不多说,大家知道自定义控件的重要性就行了,下面开始我们的第一个自定义控件之验证码的旅途:


一、自定义控件的三种实现方式。

1,自定义组合控件。 2,继承已有系统控件实现自定义控件。 3,单独继承View类来实现自定义控件。     1、2是相对而言较为简单的实现自定义控件的方式,本次案例不讲,我们用第三种方式来实现本次案例的自定义控件。

二、功能实现。

首先大家应该都知道,Android原生控件,最终都是继承自:View类,那么如果我们想写一个控件,那么也学它,写一个类,继承View!具体分步骤可以为:
1,自定义View的属性。 2,自定义类继承View,在View的构造中,获取我们自定义的属性。 3,重写onMesure、onLayout。(有时可能无需重写) 4,重写onDraw。
下面来详细讲解这四步:
第一步,自定义View的属性: 首先在res/values/ 目录下建立一个attrs.xml,在里面定义我们的属性。
                                                    <?xml version="1.0" encoding="utf-8"?>                                       <resources>                                                                                <attr name="titleText" format="string" />                                        <attr name="titleTextColor" format="color" />                                        <attr name="titleTextSize" format="dimension" />                                                                                <declare-styleable name="CustomTitleView">                                        <attr name="titleText" />                                        <attr name="titleTextColor" />                                        <attr name="titleTextSize" />                                        declare-styleable>                                                                               resources>                           
name:为属性名称,format:为取值类型。 format的取值类型一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag; 其中大家不熟的,demension:尺寸值; reference:参考指定Theme中资源ID; fraction:百分数; flag:位或运算。
第二步,自定义View继承View,在View构造中获取自己定义的自定义属性:
                                                    public class CustomTextView extends View {                                                                                private int mTextColor;                                        private String mText;                                        private int mTextSize;                                        private Paint mPaint;                                        private Rect mRect;                                                                                public CustomTextView(Context context) {                                        super(context);                                        }                                                                                public CustomTextView(Context context, AttributeSet attrs) {                                        this(context, attrs, 0);                                        }                                                                                public CustomTextView(Context context, AttributeSet attrs, int defStyle) {                                        super(context, attrs, defStyle);                                        // 获取定义的自定义属性                                        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);                                        mText = typedArray.getString(R.styleable.CustomTitleView_titleText);                                        mTextColor = typedArray.getColor(R.styleable.CustomTitleView_titleTextColor, Color.BLACK);                                        // mTextSize = (int) typedArray.getDimension(R.styleable.CustomTitleView_titleTextSize, 12);                                        mTextSize = typedArray.getDimensionPixelSize(R.styleable.CustomTitleView_titleTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));                                        typedArray.recycle();                                                                                // 获取绘制文本的宽和高                                        mPaint = new Paint();                                        mRect = new Rect();                                        mPaint.setTextSize(mTextSize);                                        mPaint.getTextBounds(mText, 0, mText.length(), mRect);                                        }                                       }                           
继承View后,会提示,让我们重写三种构造方法中的一种,那么这三种构造方法都有什么区别呢? 通过查看文档,了解到:
public View (Context context): 在代码中创建视图的时候这个构造会被调用。 public View (Context context, AttributeSet attrs): 在xml中填充视图并不指定style的时候会被调用。 public View (Context context, AttributeSet attrs, int defStyle):在xml中填充视图并指定style的时候会被调用。

所以说,在重写的时候,大家可以根据使用场景来进行重写,如果不需要在xml中定义,只用代码创建视图,那么就可以只重写一个参数的构造。 我一般写的时候,都是习惯三个全部重写,然后 让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

第三步,重写onMesure、onLayout、onDraw方法:
好了,问题来了,为什么要重写onMesure、onLayout、onDraw方法? onMesure、onLayout为什么开头说有时候可能不重写?
实现这几个方法都有什么用?
onMesure:用于控制View的大小, 重写onMeasure()方法是为了自定义View尺寸的规则, 如果你的自定义View的尺寸是根据父控件行为一致,就不需要重写onMeasure()方法。
onLayout:用于控制子View的位置。onLayout方法是ViewGroup中子View的布局方法,用于放置子View的位置。放置子View很简单,只需在重写 onLayout方法,然后获取子View的实例,调用子View的layout方法实现布局。在实际开发中,一般要配合onMeasure测量方法一起使用。
onDraw:用于绘制View。系统会不停的调用该方法,所以在该方法中,不要去做new等消耗的操作。在本例中,我们只实现该方法和onMesure方法:
                                           @Override                                  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {                                  int widthMode = MeasureSpec.getMode(widthMeasureSpec);                                  int widthSize = MeasureSpec.getSize(widthMeasureSpec);                                  int heightMode = MeasureSpec.getMode(heightMeasureSpec);                                  int heightSize = MeasureSpec.getSize(heightMeasureSpec);                                  int width,height;                                  if(widthMode == MeasureSpec.EXACTLY){                                  width = widthSize;                                  }else{                                  mPaint.setTextSize(mTextSize);                                  mPaint.getTextBounds(mText, 0, mText.length(), mRect);                                  int textWidth = mRect.width();                                  width = getPaddingLeft() + textWidth + getPaddingRight();                                  }                                  if(heightMode == MeasureSpec.EXACTLY){                                  height = heightSize;                                  }else{                                  mPaint.setTextSize(mTextSize);                                  mPaint.getTextBounds(mText, 0, mText.length(), mRect);                                  int textHeight = mRect.height();                                  height = getPaddingLeft() + textHeight + getPaddingRight();                                  }                                  setMeasuredDimension(width, height);                                  }                                                                    @Override                                  protected void onDraw(Canvas canvas) {                                  mPaint.setColor(Color.YELLOW);                                  canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);                                  mPaint.setColor(mTextColor);                                  canvas.drawText(mText, getWidth()/2 - mRect.width()/2, getHeight()/2 + mRect.height()/2, mPaint);                                  }                      
    首先,我们重写onMeasure,发来两个传递过来的参数:widthMeasureSpec和heightMeasureSpec。分别是什么意思呢?     widthMeasureSpec宽详细测量值,heightMeasureSpec:高详细测量值,决定View的大小只需要两个值。也可以把详细测量值理解为视图View想要的大小说明(想要的未必就是最终大小)。     对于详细测量值(measureSpec)需要两样东西来确定它,那就是大小(size)和模式(mode)。而measureSpec,size,mode他们三个的关系,都封装在View类中的一个内部类里,名叫MeasureSpec 
MeasureSpec详解:
    为MeasureSpec类很小,而且设计的很巧妙,所以我贴出了全部的源码并进行了详细的标注。(掌握MeasureSpec的机制后会对整个Measure方法有更深刻的理解。)
                                           /**                                   * MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求                                   * MeasureSpec由size和mode组成。                                   * 三种Mode:                                   * 1.UNSPECIFIED                                   * 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)                                   * (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED                                   * 2.EXACTLY                                   * 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。                                   * (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)                                   * 3.AT_MOST                                   * 子最大可以达到的指定大小                                   * (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)                                   *                                   * MeasureSpecs使用了二进制去减少对象的分配。                                   */                                  public class MeasureSpec {                                   // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位)                                   private static final int MODE_SHIFT = 30;                                                                     // 运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)                                   // (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)                                   private static final int MODE_MASK = 0x3 << MODE_SHIFT;                                                                     // 0向左进位30,就是00 00000000000(00后跟30个0)                                   public static final int UNSPECIFIED = 0 << MODE_SHIFT;                                   // 1向左进位30,就是01 00000000000(01后跟30个0)                                   public static final int EXACTLY = 1 << MODE_SHIFT;                                   // 2向左进位30,就是10 00000000000(10后跟30个0)                                   public static final int AT_MOST = 2 << MODE_SHIFT;                                                                     /**                                   * 根据提供的size和mode得到一个详细的测量结果                                   */                                   // measureSpec = size + mode; (注意:二进制的加法,不是10进制的加法!)                                   // 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值                                   // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100                                   public static int makeMeasureSpec(int size, int mode) {                                   return size + mode;                                   }                                                                     /**                                   * 通过详细测量结果获得mode                                   */                                   // mode = measureSpec & MODE_MASK;                                   // MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。                                   // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值                                   public static int getMode(int measureSpec) {                                   return (measureSpec & MODE_MASK);                                   }                                                                     /**                                   * 通过详细测量结果获得size                                   */                                   // size = measureSpec & ~MODE_MASK;                                   // 原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size                                   public static int getSize(int measureSpec) {                                   return (measureSpec & ~MODE_MASK);                                   }                                                                     /**                                   * 重写的toString方法,打印mode和size的信息,这里省略                                   */                                   public static String toString(int measureSpec) {                                   return null;                                   }                                  }                       
知道了widthMeasureSpec和heightMeasureSpec是什么以后,我们可以来看看源码中的onMeasure方法:
                                                    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {                                         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));                                        }                            
在onMeasure中只调用了setMeasuredDimension()方法,接受两个参数,这两个参数是通过getDefaultSize方法得到的,我们到源码里看看getDefaultSize究竟做了什么:
getDefaultSize():
                                           /**                                   * 作用是返回一个默认的值,如果MeasureSpec没有强制限制的话则使用提供的大小.否则在允许范围内可任意指定大小                                   * 第一个参数size为提供的默认大小,第二个参数为测量的大小                                   */                                   public static int getDefaultSize(int size, int measureSpec) {                                   int result = size;                                   int specMode = MeasureSpec.getMode(measureSpec);                                   int specSize = MeasureSpec.getSize(measureSpec);                                                                     switch (specMode) {                                   // Mode = UNSPECIFIED,AT_MOST时使用提供的默认大小                                   case MeasureSpec.UNSPECIFIED:                                   result = size;                                   break;                                   case MeasureSpec.AT_MOST:                                   // Mode = EXACTLY时使用测量的大小                                   case MeasureSpec.EXACTLY:                                   result = specSize;                                   break;                                   }                                   return result;                                   }                       
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),这里就是获取最小宽度作为默认值,然后再根据具体的测量值和选用的模式来得到widthMeasureSpec。heightMeasureSpec同理。之后将widthMeasureSpec,heightMeasureSpec传入setMeasuredDimension()方法。

 setMeasuredDimension():
                                                    /**                                         * 这个方法必须由onMeasure(int, int)来调用,来存储测量的宽,高值。                                         */                                        protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {                                         mMeasuredWidth = measuredWidth;                                         mMeasuredHeight = measuredHeight;                                                                                 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;                                        }                            
这个方法就是我们重写onMeasure()所要实现的最终目的。它的作用就是存储我们测量好的宽高值。 这下思路清晰了,现在的任务就是计算出准确的measuredWidth和heightMeasureSpec并传递进去,我们所有的测量任务就算完成了。 源码中使用的getDefaultSize()只是简单的测量了宽高值,在实际使用时需要精细、具体的测量。而具体的测量任务就交给我们在子类中重写的onMeasure方法。

在子类中重写的onMeasure:

在测量之前首先要明确一点,需要测量的是一个View(例如TextView),还是一个ViewGroup(例如LinearLayout),还是多个ViewGroup嵌套。如果只有一个View的话我们就测量这一个就可以了,如果有多个View或者ViewGroup嵌套我们就需要循环遍历视图中所有的View。



第四步,在布局中引用自定义的View:
                                                             <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"                                              xmlns:custom="http://schemas.android.com/apk/res/com.example.demo"                                              xmlns:tools="http://schemas.android.com/tools"                                              android:layout_width="match_parent"                                              android:layout_height="match_parent"                                              tools:context="com.example.demo.MainActivity" >                                                                                            <com.example.demo.view.CustomTextView                                              android:layout_width="wrap_content"                                              android:layout_height="wrap_content"                                              android:layout_centerInParent="true"                                              android:padding="10dp"                                              custom:titleText="2048"                                              custom:titleTextColor="#00ff00"                                              custom:titleTextSize="40sp" />                                                                                           RelativeLayout>                                

注意,一定要引入命名空间: xmlns:custom="http://schemas.android.com/apk/res/com.example.demo"
其中com.example.demo,为项目的包名。

到此View就画好了,但是,没有验证码的效果,这个时候,我们可以在构造中,添加一个点击事件,让每点击一次,就去随机显示一个数:
                                                             this.setOnClickListener(new OnClickListener() {                                              @Override                                              public void onClick(View v) {                                              mText = randomText();                                              postInvalidate();                                              }                                              });                                
                                                                                             // 随机生成验证码                                   private String randomText() {                                   Random random = new Random();                                   StringBuilder sb = new StringBuilder();                                   for (int i = 0; i < 4; i++) {                                   int nextInt = random.nextInt(10);                                   sb.append(nextInt);                                   }                                   return sb.toString();                                   }                                                          
最终效果:
时间有限,就写到这里,下次有时间可以添加噪点以及斜线,字的大小也可以随机显示。具体大家可以自由发挥~!
源码下载地址: http://download.csdn.net/detail/airsaid/9412922

更多相关文章

  1. 浅谈Java中Collections.sort对List排序的两种方法
  2. Python list sort方法的具体使用
  3. python list.sort()根据多个关键字排序的方法实现
  4. android上一些方法的区别和用法的注意事项
  5. android实现字体闪烁动画的方法
  6. Android中dispatchDraw分析
  7. android studio Could not find com.android.support.constraint
  8. android ndk编译x264开源(用于android的ffmpeg中进行软编码)
  9. Android四大基本组件介绍与生命周期

随机推荐

  1. 播放音乐时的状态条使用
  2. 升级Android(安卓)Studio 3.6.1后Android
  3. Android(安卓)获得View截屏最优方案
  4. Android数据传递相关内容概述
  5. Android的消息机制,用Android线程间通信的
  6. Android界面布局基本知识简述
  7. Android应用程序窗口(Activity)的运行上下
  8. Android中的资源分析
  9. Android新手入门 FAQ
  10. Android独特的架构:HAL与Dalvik虚拟机