Android自定义控件开发系列(一)——第一次动手做自定义控件

  转载自:"_程序猿大人_"http://blog.csdn.net/a_running_wolf

 Android系统提供的控件多种多样,以至于很多初学者经常忘了还有这样那样的控件没用过甚至没听过。尽管如此,但是系统控件大多比较死板,而且不够美观,很多多样化的显示或是交互方式都没法完成。每每遇到这种情况,就需要我们来开发我们自己的控件了——所谓的“自定义控件”。接下来我们就一步一步扎扎实实的从头开始Android自定义控件的开发。

        废话少说,开始吧:

        一、实现自定义控件的3种主要方式

        (1)修改已有控件——继承已有控件,重写其显示、响应等;

        (2)组合已有控件——将已有的系统控件组合成一个独特的控件(接下来的示例中就是这种演示);

        (3)开发全新的控件——一般继承View或SurfaceView。他们都提供一个Canvas(画布)和一系列的画的方法,还有Paint(画笔)。使用它们去创建一个自定义的UI。你可以重写事件,包括屏幕接触或者按键按下等等,用来提供与用户交互。这种方式比较高阶,需要熟悉View的工作原理并熟悉其各个回调方法。

        二、为自定义控件增加属性的两种方法:

        (1)在自定义类中定义属性字段

         本文通过开发一个可以显示文字和图片组合的控件来加以说明。

       通过构造函数中引入的AttributeSet 去查找XML布局的属性名称,然后找到它对应引用的资源ID去找值:

[java] view plain copy
  1. public class MyView1 extends View {  
  2.     private String mtext;  
  3.     private int msrc;  
  4.   
  5.     public MyView1(Context context) {  
  6.         super(context);  
  7.     }  
  8.   
  9.     public MyView1(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.   
  12.         //方法attrs.getAttributeResourceValue(String nameSpace, String attriName, int default)的nameSpace参数传null即可————  
  13.         int textId = attrs.getAttributeResourceValue(null"Text"0);  
  14.         int imgId = attrs.getAttributeResourceValue(null"Img"0);  
  15.   
  16.         //根据传来的id找出字符串,比如示例代码中传入的是@stirng/hello_world  
  17.         //这里也可以直接在xml文件中设置字符串的参数但是获取属性值的方法就要相应变成  
  18.         //mtext = attrs.getAttributeValue(null, "Text")直接获取传入的字符串  
  19.         mtext = getResources().getText(textId).toString();  
  20.         msrc = imgId;  
  21.     }  
  22.   
  23.     @Override  
  24.     protected void onDraw(Canvas canvas) {  
  25.         Paint paint = new Paint();  
  26.         paint.setColor(Color.RED);  
  27.         paint.setTextSize(30);  
  28.   
  29.         InputStream is = getResources().openRawResource(msrc);  
  30.         Bitmap bitmap = BitmapFactory.decodeStream(is);  
  31.         int imgWid = bitmap.getWidth();  
  32.         int imgHei = bitmap.getHeight();  
  33.         canvas.drawBitmap(bitmap, 00, paint);  
  34.         canvas.drawText(mtext,imgWid/3, imgHei/2, paint);  
  35.     }  
  36. }  

        然后在布局文件中使用我们自定义的控件(控件名要用包名+类名):

[html] view plain copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent">  
  4.   
  5.     <com.wangj.cusview.MyView1  
  6.         android:layout_width="wrap_content"  
  7.         android:layout_height="wrap_content"  
  8.         Img="@drawable/xx"  
  9.         Text="@string/hello_world" />  
  10. </RelativeLayout>  

         运行一下如下图:

        

        到这里,可能会有人问,布局文件里自定义控件的属性设置直接用Img="@drawable/xx"和Text="@string/hello_world",怎么不像系统控件的属性那样的写法android:layout_height="wrap_content"呢?这里就需要回顾一下上边我们在上边MyView1类中用attrs.getAttributeResourceValue(String nameSpace, String attriName, int default)方法时的nameSpace参数传了个null,没有使用命名空间

        ----------------------------------------------好了,上边这种方法完成了,下边的和上边的一样,只是更规范而已----------------------------------------------

        什么是命名空间呢?呵呵,你就先理解成一个归属吧,就像你们学校有好多个叫“王小二”的同学,校长要叫王二小同学来办公室聊聊,这是就要指出是“哪个班的王二小”了,命名空间就相当于这个班级的作用。我们看布局文件,根节点属性中都会有xmlns:android="http://schemas.android.com/apk/res/android"这一句,这就是声明一个命名空间,“命名空间的名字”就叫android,接下来就可以用android:layout_height="wrap_content"这种写法了。

        接下来我们也将上边的布局文件改成这种方式(我们也自己给一个命名空间)(这里需要说明一下,有人认为系统组件的命名空间必须要用“android”,其实不是的。“android”只是命名空间的名称,“http://schemas.android.com/apk/res/android”才是命名空间,你把xmlns:android="http://schemas.android.com/apk/res/android"改成xmlns:abcd="http://schemas.android.com/apk/res/android",在系统组件属性用abcd:layout_width也是没有问题的):   

[html] view plain copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:myview="http://blog.csdn.net/a_running_wolf"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.     <!--这里第二行xmlns:my="http://blog.csdn.net/a_running_wolf"就是自己定义的命名空间-->  
  6.     <!--***注意这里的http://blog.csdn.net/a_running_wolf要和自定义类中的nameSpace一致,否则属性找不出来的-->  
  7.   
  8.     <com.wangj.cusview.MyView1  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         myview:Img="@drawable/xx"  
  12.         myview:Text="@string/hello_world" />  <!--上边定义了命名空间,这里就可以像系统控件的属性一样的写法了-->  
  13.   
  14. </RelativeLayout>  

        相应的,在MyView1中就应该使用我们声明的这个命名空间了,直接看代码(这个类应该先于布局文件创建,不然在布局文件中怎么会有自己的控件可以用呢?这里放在后边主要是让大家重点看“命名空间”)

[java] view plain copy
  1. public class MyView1 extends View {  
  2.     //要和布局文件中声明的命名空间一致  
  3.     private final String nameSpace = "http://blog.csdn.net/a_running_wolf";  
  4.     private String mtext;  
  5.     private int msrc;  
  6.   
  7.     public MyView1(Context context) {  
  8.         super(context);  
  9.     }  
  10.   
  11.     public MyView1(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.         int textId = attrs.getAttributeResourceValue(nameSpace, "Text"0);   //参数nameSpace用我们的命名空间  
  14.         int imgId = attrs.getAttributeResourceValue(nameSpace, "Img"0);  
  15.         mtext = getResources().getText(textId).toString();  
  16.         msrc = imgId;  
  17.     }  
  18.   
  19.     //onDraw方法和上边的一样  
  20.     .......  
  21.  }  
        改造完毕,运行一下和上边一致:

        

        显然这种方式能实现相应的效果,但是效果很不好,文字显示在图片的什么地方、如果文字很多图片很小呢、如果我要改文字位置还要手动改drawText的位置参数,那么我们来看更规范的做法——往下看。

        (2)通过XML资源文件为自定义控件注册属性(和Android系统提供属性的方式一致)

        通过XML资源文件为控件注册属性,并使用命名空间的方式就是Android系统控件的属性提供方法,所以这种方式是推荐的自定义控件开发方法。首先在value目录下创建attrs.xml文件(有的话就不用创建了,直接往进写),写入如下的属性字段和相应的值(当然这里是根据自己的需要写,如下只是示例):

[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <declare-styleable name="MyView2">  
  4.         <attr name="Text" format="reference|string" />  
  5.         <attr name="Oriental">  
  6.             <enum name="Herizontal" value="1" />  
  7.             <enum name="Vertical" value="0" />  
  8.         </attr>  
  9.         <attr name="Img" format="reference|integer" />  
  10.     </declare-styleable>  
  11. </resources>  

        创建好attrs.xml后就可以用这些属性字段了(前提是你先创建好MyView2,否则控件啊!可以先创建一个空类,构造方法具体实现先不写,等后边在具体实现):

[html] view plain copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:TV="http://schemas.android.com/apk/res-auto"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:padding="@dimen/activity_vertical_margin">  
  6.     <!-- 有的博客上说这里自己的命名空间要用“http://schemas.android.com/apk/包名”,但是我在AndroidStudio1.3.2、Android5.0时  
  7.     用“http://schemas.android.com/apk/res-auto”更智能一点 -->  
  8.   
  9.     <com.wangj.cusview.MyView2  
  10.         android:layout_width="wrap_content"  
  11.         android:layout_height="wrap_content"  
  12.         TV:Text="@string/app_name"  
  13.         TV:Img="@drawable/xx"  
  14.         TV:Oriental="Herizontal"/>  
  15. </RelativeLayout>  

       附上MyView2的实现类: 

[java] view plain copy
  1. public class MyView2 extends LinearLayout {   //为了简单,这里继承LinearLayout  
  2.     public MyView2(Context context) {  
  3.         super(context);  
  4.     }  
  5.   
  6.     public MyView2(Context context, AttributeSet attrs) {  
  7.         super(context, attrs);  
  8.         int resourceId = -1;  
  9.           
  10.         //获取布局文件中对应控件的属性  
  11.         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView2);  
  12.         ImageView iv = new ImageView(context);  
  13.         TextView tv = new TextView(context);  
  14.         int N = typedArray.getIndexCount();  
  15.         for (int i = 0; i < N; i++){  
  16.             int attr = typedArray.getIndex(i);  
  17.             switch (attr){  
  18.                 case R.styleable.MyView2_Text:  
  19.                     resourceId = typedArray.getResourceId(R.styleable.MyView2_Text, 0);  
  20.                     tv.setText(resourceId > 0 ? getResources().getText(resourceId) : typedArray.getString(R.styleable.MyView2_Text));  
  21.                     break;  
  22.                 case R.styleable.MyView2_Img:  
  23.                     resourceId = typedArray.getResourceId(R.styleable.MyView2_Img, 0);  
  24.                     iv.setImageResource(resourceId > 0 ? resourceId : R.mipmap.ic_launcher);  
  25.                     break;  
  26.                 case R.styleable.MyView2_Oriental:  
  27.                     resourceId = typedArray.getInt(R.styleable.MyView2_Oriental, 0);  
  28.                     this.setOrientation(resourceId == 1 ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL );  
  29.                     break;  
  30.             }  
  31.         }  
  32.   
  33.         //记住要将TextView和ImageView都addView,否则什么都显示不了  
  34.         addView(iv);  
  35.         addView(tv);  
  36.         typedArray.recycle();   //释放资源  
  37.     }  
  38.   
  39.     @Override  
  40.     protected void onDraw(Canvas canvas) {  
  41.         super.onDraw(canvas);  
  42.     }  
  43. }  

      MainActivity中没有做任何逻辑。直接运行,看效果:


        这里看到这个红色框中就是我们的MyView2控件(不包括红色框)。肯定有人问,这里看起来这个控件就和一个ImageView和一个TextView水平线性排列就一个样子啊?其实是的,但是区别在于——这里是一个控件,而不是两个控件——这里是为了简单演示,就简单的让MyView2继承了LineraLayout。根据实际的需求,我们可已让它继承RelativeLayout......各种布局,再创建自己的属性根据属性来做出漂亮的控件;可以继承已有的系统控件,重写其构造方法或是覆盖其onDraw方法来扩展出自己的个性化控件

        这些都是自定义控件开发最基础的知识,接下来我们将逐步深入,开发更高级和更美观的自定义控件,我也是在边学边写,如有不足希望大家指出,一起讨论,一起进步,也希望大家继续关注我的文章。

展开阅读全文

没有更多推荐了,返回首页