Android开发做到了一定程度,多少都会用到自定义控件,一方面是更加灵活,另一方面在大数据量的情况下自定义控件的效率比写布局文件更高。

    一个相对完善的自定义控件在布局文件中和java代码中都应能灵活设置属性。另外在普通的布局中和AdapterView中都应能正确绘制,这就要求合理设计onMeasure方法,下文中会做比较详细的讲解。

    本文原创,如需转载,请注明转载地址:http://blog.csdn.net/carrey1989/article/details/11757409

    接下来我就一步一步来讲解如何设计和编写一个比较完善的自定义控件。

    首先要来设计好我们要完成的效果,我们今天来实现下图所示的这样一个控件:


用文字来描述一下:我们要定义的控件上方会显示一张图片,我们可以设置这张图片的内容,长宽比,透明度,伸缩模式,以及图片四周的填充空间大小。图片下方会显示一行文字,作为一级标题,我们可以设置文字的内容,大小,颜色,以及文字区域四周的填充空间的大小。一级标题下方显示一行二级标题,具体设置内容和一级标题相同。

    我们不妨先来直接看一下完成后的效果,这样可以更直观的了解要实现的控件的样子。

左图的样子是在常规的布局中自定义控件的样子,右图则是在大数据量的情况下自定义控件作为AdapterView的item的时候绘制出来的样子。

    上面我们大体完成了初步的控件设计,下面我们开始编写代码。


    第一步,我们写好自定义属性,根据我们上面所做的设计,我们的自定义属性涉及到三个方面,分别是图片相关的属性,一级标题相关的属性,二级标题相关的属性。

    按照惯例,我们首先在res/values文件目录下创建一个attrs.xml文件。

    然后我们在attrs.xml文件中完成我们对属性的定义,代码片段如下:


<pre name="code" class="html"><?xml version="1.0" encoding="utf-8"?>
<resources>  <attr name="imageSrc" format="reference"/>  <attr name="imageAspectRatio" format="float"/>  <attr name="imageAlpha" format="float"/>  <attr name="imagePaddingLeft" format="dimension"/>  <attr name="imagePaddingTop" format="dimension"/>  <attr name="imagePaddingRight" format="dimension"/>  <attr name="imagePaddingBottom" format="dimension"/>  <attr name="imageScaleType">  <enum name="fillXY" value="0"/>  <enum name="center" value="1"/>  </attr>  <attr name="titleText" format="string"/>  <attr name="titleTextSize" format="dimension"/>  <attr name="titleTextColor" format="color"/>  <attr name="titlePaddingLeft" format="dimension"/>  <attr name="titlePaddingTop" format="dimension"/>  <attr name="titlePaddingRight" format="dimension"/>  <attr name="titlePaddingBottom" format="dimension"/>  <attr name="subTitleText" format="string"/>  <attr name="subTitleTextSize" format="dimension"/>  <attr name="subTitleTextColor" format="color"/>  <attr name="subTitlePaddingLeft" format="dimension"/>  <attr name="subTitlePaddingTop" format="dimension"/>  <attr name="subTitlePaddingRight" format="dimension"/>  <attr name="subTitlePaddingBottom" format="dimension"/>  <declare-styleable name="CustomView">  <attr name="imageSrc"/>  <attr name="imageAspectRatio" />  <attr name="imageAlpha" />  <attr name="imagePaddingLeft" />  <attr name="imagePaddingTop" />  <attr name="imagePaddingRight" />  <attr name="imagePaddingBottom" />  <attr name="imageScaleType" />  <attr name="titleText" />  <attr name="titleTextSize" />  <attr name="titleTextColor" />  <attr name="titlePaddingLeft" />  <attr name="titlePaddingTop" />  <attr name="titlePaddingRight" />  <attr name="titlePaddingBottom" />  <attr name="subTitleText" />  <attr name="subTitleTextSize" />  <attr name="subTitleTextColor" />  <attr name="subTitlePaddingLeft" />  <attr name="subTitlePaddingTop" />  <attr name="subTitlePaddingRight" />  <attr name="subTitlePaddingBottom" />  </declare-styleable>
</resources>  

这里需要说明几点:<attr>标签的format属性值代表属性的类型,这个类型值一共有10种,分别是:reference,float,color,dimension,boolean,string,enum,integer,fraction,flag

。但是我们作为开发者常用的基本上只有reference,float,color,dimension,boolean,string,enum这7种。在attrs.xml文件中的<declare-styleable>标签的name属性的值,按照惯例我们都是写成自定义控件类的名字。一个同名的<attr>在attrs.xml中只可以定义一次。

    除此之外,上面的代码都是针对前面的设计来定义了各种属性,相信各位同学都能看懂。


    第二步就是编写我们自定义控件的java类了,我们首先将之前做的自定义属性在自定义控件类中做好声明:

/** 图片Bitmap */
private Bitmap imageBitmap;
/** 图片的长宽比 */
private float imageAspectRatio;
/** 图片的透明度 */
private float imageAlpha;
/** 图片的左padding*/
private int imagePaddingLeft;
/** 图片的上padding */
private int imagePaddingTop;
/** 图片的右padding */
private int imagePaddingRight;
/** 图片的下padding */
private int imagePaddingBottom;
/** 图片伸缩模式 */
private int imageScaleType;
/** 图片伸缩模式常量 fillXY */
private static final int SCALE_TYPE_FILLXY = 0;
/** 图片伸缩模式常量 center */
private static final int SCALE_TYPE_CENTER = 1;  /** 标题文本内容 */
private String titleText;
/** 标题文本字体大小 */
private int titleTextSize;
/** 标题文本字体颜色 */
private int titleTextColor;
/** 标题文本区域左padding */
private int titlePaddingLeft;
/** 标题文本区域上padding */
private int titlePaddingTop;
/** 标题文本区域右padding */
private int titlePaddingRight;
/** 标题文本区域下padding */
private int titlePaddingBottom;  /** 子标题文本内容 */
private String subTitleText;
/** 子标题文本字体大小 */
private int subTitleTextSize;
/** 子标题文本字体颜色 */
private int subTitleTextColor;
/** 子标题文本区域左padding */
private int subTitlePaddingLeft;
/** 子标题文本区域上padding */
private int subTitlePaddingTop;
/** 子标题文本区域右padding */
private int subTitlePaddingRight;
/** 子标题文本区域下padding */
private int subTitlePaddingBottom;  /** 控件用的paint */
private Paint paint;
private TextPaint textPaint;
/** 用来界定控件中不同部分的绘制区域 */
private Rect rect;
/** 宽度和高度的最小值 */
private static final int MIN_SIZE = 12;
/** 控件的宽度 */
private int mViewWidth;
/** 控件的高度 */
private int mViewHeight;  

然后我们要在构造方法中,将从布局文件中读取的自定义属性解析出来。

TypedArray a = context.getTheme().obtainStyledAttributes(  attrs, R.styleable.CustomView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {  int attr = a.getIndex(i);  switch (attr) {  case R.styleable.CustomView_imageSrc:  imageBitmap = BitmapFactory.decodeResource(  getResources(), a.getResourceId(attr, 0));  break;  case R.styleable.CustomView_imageAspectRatio:  imageAspectRatio = a.getFloat(attr, 1.0f);//默认长宽相等  break;  case R.styleable.CustomView_imageAlpha:  imageAlpha = a.getFloat(attr, 1.0f);//默认不透明  if (imageAlpha > 1.0f) imageAlpha = 1.0f;  if (imageAlpha < 0.0f) imageAlpha = 0.0f;  break;  case R.styleable.CustomView_imagePaddingLeft:  imagePaddingLeft = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_imagePaddingTop:  imagePaddingTop = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_imagePaddingRight:  imagePaddingRight = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_imagePaddingBottom:  imagePaddingBottom = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_imageScaleType:  imageScaleType = a.getInt(attr, 0);  break;  case R.styleable.CustomView_titleText:  titleText = a.getString(attr);  break;  case R.styleable.CustomView_titleTextSize:  titleTextSize = a.getDimensionPixelSize(  attr, (int) TypedValue.applyDimension(  TypedValue.COMPLEX_UNIT_SP, 25, getResources().getDisplayMetrics()));//默认标题字体大小25sp  break;  case R.styleable.CustomView_titleTextColor:  titleTextColor = a.getColor(attr, 0x00000000);//默认黑色字体  break;  case R.styleable.CustomView_titlePaddingLeft:  titlePaddingLeft = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_titlePaddingTop:  titlePaddingTop = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_titlePaddingRight:  titlePaddingRight = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_titlePaddingBottom:  titlePaddingBottom = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_subTitleText:  subTitleText = a.getString(attr);  break;  case R.styleable.CustomView_subTitleTextSize:  subTitleTextSize = a.getDimensionPixelSize(attr,   (int) TypedValue.applyDimension(  20, TypedValue.COMPLEX_UNIT_SP, getResources().getDisplayMetrics()));//默认子标题字体大小20sp  break;  case R.styleable.CustomView_subTitleTextColor:  subTitleTextColor = a.getColor(attr, 0x00000000);  break;  case R.styleable.CustomView_subTitlePaddingLeft:  subTitlePaddingLeft = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_subTitlePaddingTop:  subTitlePaddingTop = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_subTitlePaddingRight:  subTitlePaddingRight = a.getDimensionPixelSize(attr, 0);  break;  case R.styleable.CustomView_subTitlePaddingBottom:  subTitlePaddingBottom = a.getDimensionPixelSize(attr, 0);  break;  }
}
a.recycle();

 这里需要说明几点,TypedArray对象在使用完毕后一定要调用recycle()方法。我之前曾在一篇文章中总结过在java代码中进行px与dip(dp)、px与sp单位值的转换实际上,android中也提供了单位转换的函数,我们也可以使用TypedValue.applyDimension(int unit, float value, DisplayMetrics metrics)方法来进行单位的互换,其中,第一个参数是你想要得到的单位,第二个参数是你想得到的单位的数值,比如:我要得到一个25sp,那么我就用TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 25,getResources().getDisplayMetrics()),返回的就是25sp对应的px数值了。


    接下来我们要开始设计onMeasure方法,再设计onMeasure之前我们简单了解几个概念。

    MeasureSpec的三种模式:

    EXACTLY:表示我们设置了MATCH_PARENT或者一个准确的数值,含义是父布局要给子布局一个确切的大小。

    AT_MOST:表示子布局将被限制在一个最大值之内,通常是子布局设置了wrap_content。

    UNSPECIFIED:表示子布局想要多大就可以要多大,通常出现在AdapterView中item的heightMode中。

    了解了上面几个概念,我们就可以开始设计onMeasure了,具体代码如下:

@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;  int height;  if (widthMode == MeasureSpec.EXACTLY) {  width = widthSize;  } else {  int desired = getPaddingLeft() + getPaddingRight() +   imagePaddingLeft + imagePaddingRight;  desired += (imageBitmap != null) ? imageBitmap.getWidth() : 0;  width = Math.max(MIN_SIZE, desired);  if (widthMode == MeasureSpec.AT_MOST) {  width = Math.min(desired, widthSize);  }  }  if (heightMode == MeasureSpec.EXACTLY) {  height = heightSize;  } else {  int rawWidth = width - getPaddingLeft() - getPaddingRight();  int desired = (int) (getPaddingTop() + getPaddingBottom() + imageAspectRatio * rawWidth);  if (titleText != null) {  paint.setTextSize(titleTextSize);  FontMetrics fm = paint.getFontMetrics();  int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  desired += (textHeight + titlePaddingTop + titlePaddingBottom);  }  if (subTitleText != null) {  paint.setTextSize(subTitleTextSize);  FontMetrics fm = paint.getFontMetrics();  int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  desired += (textHeight + subTitlePaddingTop + subTitlePaddingBottom);  }  height = Math.max(MIN_SIZE, desired);  if (heightMode == MeasureSpec.AT_MOST) {  height = Math.min(desired, heightSize);  }  }  setMeasuredDimension(width, height);
}  

思路是这样的:我们首先判断是不是EXACTLY模式,如果是,那就可以直接设置值了,如果不是,我们先按照UNSPECIFIED模式处理,让子布局得到自己想要的最大值,然后判断是否是AT_MOST模式,来做最后的限制。


    完成onMeasure过程之后,我们需要开始onDraw的设计,在onDraw中我们需要考虑各个部分设置的padding值,然后对应做出坐标的处理,整体的思路是从下向上绘制。具体的代码如下:


@Override
protected void onDraw(Canvas canvas) {  rect.left = getPaddingLeft();  rect.top = getPaddingTop();  rect.right = mViewWidth - getPaddingRight();  rect.bottom = mViewHeight - getPaddingBottom();  paint.setAlpha(255);  if (subTitleText != null) {  paint.setTextSize(subTitleTextSize);  paint.setColor(subTitleTextColor);  paint.setTextAlign(Paint.Align.LEFT);  FontMetrics fm = paint.getFontMetrics();  int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  int left = getPaddingLeft() + subTitlePaddingLeft;  int right = mViewWidth - getPaddingRight() - subTitlePaddingRight;  int bottom = mViewHeight - getPaddingBottom() - subTitlePaddingBottom;  String msg = TextUtils.ellipsize(subTitleText, textPaint, right - left, TextUtils.TruncateAt.END).toString();  float textWidth = paint.measureText(msg);  float x = textWidth < (right - left) ? left + (right - left - textWidth) / 2 : left;  canvas.drawText(msg, x, bottom - fm.descent, paint);  rect.bottom -= (textHeight + subTitlePaddingTop + subTitlePaddingBottom);  }  if (titleText != null) {  paint.setTextSize(titleTextSize);  paint.setColor(titleTextColor);  paint.setTextAlign(Paint.Align.LEFT);  FontMetrics fm = paint.getFontMetrics();  int textHeight = (int) Math.ceil(fm.descent - fm.ascent);  float left = getPaddingLeft() + titlePaddingLeft;  float right = mViewWidth - getPaddingRight() - titlePaddingRight;  float bottom = rect.bottom - titlePaddingBottom;  String msg = TextUtils.ellipsize(titleText, textPaint, right - left, TextUtils.TruncateAt.END).toString();  float textWidth = paint.measureText(msg);  float x = textWidth < right - left ? left + (right - left - textWidth) / 2 : left;  canvas.drawText(msg, x, bottom - fm.descent, paint);  rect.bottom -= (textHeight + titlePaddingTop + titlePaddingBottom);  }  if (imageBitmap != null) {  paint.setAlpha((int) (255 * imageAlpha));  rect.left += imagePaddingLeft;  rect.top += imagePaddingTop;  rect.right -= imagePaddingRight;  rect.bottom -= imagePaddingBottom;  if (imageScaleType == SCALE_TYPE_FILLXY) {  canvas.drawBitmap(imageBitmap, null, rect, paint);  } else if (imageScaleType == SCALE_TYPE_CENTER) {  int bw = imageBitmap.getWidth();  int bh = imageBitmap.getHeight();  if (bw < rect.right - rect.left) {  int delta = (rect.right - rect.left - bw) / 2;  rect.left += delta;  rect.right -= delta;  }  if (bh < rect.bottom - rect.top) {  int delta = (rect.bottom - rect.top - bh) / 2;  rect.top += delta;  rect.bottom -= delta;  }  canvas.drawBitmap(imageBitmap, null, rect, paint);  }  }
} 

当做完这一步的时候,我们的自定义控件已经能够在布局文件中进行使用了,但是我们还不能在AdapterView中用我们设计的布局文件,因为AdapterView中每一个item属性都是在java代码中动态设置的,因此我们就需要给我们的自定义控件开放属性设置的接口,我们这里暂时只开放了设置图片和文字内容的接口。

public void setImageBitmap(Bitmap bitmap) {  imageBitmap = bitmap;  requestLayout();  invalidate();
}  public void setTitleText(String text) {  titleText = text;  requestLayout();  invalidate();
}  public void setSubTitleText(String text) {  subTitleText = text;  requestLayout();  invalidate();
}  

做到这一步的时候,这个自定义控件基本就算完成了,后续的工作就是一些完善和修补了。

    接下来就是自定义控件的使用了,在布局文件中使用自定义控件的时候我们需要额外做一点工作,如下:

<RelativeLayout   xmlns:android="http://schemas.android.com/apk/res/android"  xmlns:carrey="http://schemas.android.com/apk/res/com.carrey.customview"  xmlns:tools="http://schemas.android.com/tools"  android:layout_width="match_parent"  android:layout_height="match_parent"  tools:context=".MainActivity" >  <com.carrey.customview.customview.CustomView   android:id="@+id/customview"  android:layout_width="200dp"  android:layout_height="200dp"  android:layout_centerInParent="true"  android:background="#FFD700"  carrey:imageSrc="@drawable/clock"  carrey:imageAspectRatio="1.0"  carrey:imageAlpha="0.5"  carrey:imagePaddingLeft="5dp"  carrey:imagePaddingTop="5dp"  carrey:imagePaddingRight="5dp"  carrey:imagePaddingBottom="5dp"  carrey:imageScaleType="center"  carrey:titleText="这是一级标题"  carrey:titleTextSize="30sp"  carrey:titleTextColor="#1E90FF"  carrey:titlePaddingLeft="4dp"  carrey:titlePaddingTop="4dp"  carrey:titlePaddingRight="4dp"  carrey:titlePaddingBottom="4dp"  carrey:subTitleText="这是二级子标题"  carrey:subTitleTextSize="20sp"  carrey:subTitleTextColor="#00FF7F"  carrey:subTitlePaddingLeft="3dp"  carrey:subTitlePaddingTop="3dp"  carrey:subTitlePaddingRight="3dp"  carrey:subTitlePaddingBottom="3dp"/>  <Button   android:id="@+id/button"  android:layout_width="match_parent"  android:layout_height="wrap_content"  android:text="next page"/>
</RelativeLayout>

我们需要添加一行xmlns: carrey ="http://schemas.android.com/apk/res/ com.carrey.customview ",其中 carrey 是一个前缀,你可以随意设置, com.carrey.customview 是我们的应用的包名,如果拿不准的可以打开 Manifest文件,在<manifest>节点中找到package属性值即可。
对于在AdapterView中的使用方法就和我们正常使用一个常用控件的方法是一样的,这里就不赘述了, 如果说到了这里还有一些不明白的地方,可以下载我下面提供的源码,然后对照着博客的思路来看,或者给我留言进行交流。

Android UI 之一步步教你自定义控件(自定义属性、合理设计onMeasure、合理设计onDraw等)相关推荐

  1. Android UI编程之自定义控件初步(下)——CustomEditText

    概述: 基于对上一篇博客<Android UI编程之自定义控件初步(上)--ImageButton>的学习,我们对自定义控件也有了一个初步的认识.那现在我们可以再试着对EditText进行 ...

  2. Android UI编程之自定义控件初步(上)——ImageButton

    概述: 我想我们在使用一些App的时候,应该不会出现一些"裸控件"的吧.除非是一些系统中的软件,那是为了保持风格的一致性,做出的一些权衡.我这里并非是在指责Android原生的控件 ...

  3. android 布局可大可小,UI设计教程之:ios与android ui适配(将IOS UI转换成Android经验畅谈)...

    内容提要:这是UI设计系列教程之ios与android ui适配经验畅谈.文章作者介绍了自己将IOS UI转换成Android经验,包括:不要直接转换.了解单位和组件缩放格式.屏幕尺寸DP和像素的换算 ...

  4. 使用XML声明自定义Android UI元素

    如何使用XML声明Android UI元素? #1楼 Google似乎更新了其开发者页面,并在那里添加了各种培训. 其中一个处理自定义视图的创建,可以在这里找到 #2楼 除了大多数投票的答案. obt ...

  5. android UI

    A015-布局之LinearLayout good 巫山老妖 Android UI开发: 横向ListView(HorizontalListView)及一个简单相册的完整实现 (附源码下载) Andr ...

  6. Android初级教程初谈自定义view自定义属性

    有些时候,自己要在布局文件中重复书写大量的代码来定义一个布局.这是最基本的使用,当然要掌握:但是有些场景都去对应的布局里面写对应的属性,就显得很无力.会发现,系统自带的控件无法满足我们的要求,这个时候 ...

  7. 各种Android UI开源框架 开源库

    各种Android UI开源框架 开源库 转 https://blog.csdn.net/zhangdi_gdk2016/article/details/84643668 自己总结的Android开源 ...

  8. 腾讯 android ui,腾讯开源的Android UI框架——QMUI Android

    各位同学,早上好,我是你们的老朋友D_clock爱吃葱花,前些天忙着发版本,最近也在看各种各样的新知识,有好多东西想写啊啊啊啊啊.嗯,先冷静捋一下,卖个关子.扯回正题,今天继续为大家推荐一个Githu ...

  9. GitHub标星5.6K,2020腾讯又一力作开源的Android UI框架——QMUI Android

    各位同学,早上好,我是你们的老朋友D_clock爱吃葱花,前些天忙着发版本,最近也在看各种各样的新知识,有好多东西想写啊啊啊啊啊.嗯,先冷静捋一下,卖个关子.扯回正题,今天继续为大家推荐一个Githu ...

最新文章

  1. android 关闭jack_Android7.0 配置JACK支持多用户同时编译
  2. GDCM:gdcm::DirectionCosines的测试程序
  3. AnnotationConfigApplicationContext 通过调用注解Bean 定义读取器
  4. Oracle行列转换小结
  5. linux 消息队列_Linux进程间通信第六讲 标准IPC之消息队列
  6. Linux 设备驱动模型中的class(类)
  7. 【LeetCode笔记】470. 用Rand7()实现Rand10()(Java、概率)
  8. devops 文化_开放文化如何建立DevOps肌肉记忆
  9. C语言学习笔记---指针和数组
  10. AE中加载个人数据库GDB方法探讨
  11. CentOS6_mini_安装后基本配置
  12. 计算机类实训室建设公司,计算机专业实训室建设方案修改
  13. many to many mysql_mysql “Too many connections” 解决办法
  14. Quartus II 与ModelSim-Altera联合仿真PLL
  15. 华旭 身份证读卡器测试软件_华旭HX-FDX3S读卡器驱动+身份证验证软件
  16. html5实现对试题图片批改效果,类似盖章效果
  17. 基于PeopleSoft的EHR系统建设路线及其团队建设
  18. PYMOL-note
  19. java环境_linu安装jdk
  20. 企业管理必须具备的8大要素!

热门文章

  1. 【基于时间特征交互和引导细化的遥感变化检测 】2022TGRS
  2. 快递单号中的派件时效该怎么查?
  3. Postgres copy命令导入导出数据
  4. 通过机器学习预测电网平均总负荷
  5. 如何获取dgv中所显示的全部数据
  6. sqlmap工具使用手册
  7. HTML css实现翻页效果,一个纯CSS3实现的酷炫翻书效果
  8. 好用的API接口测试工具有什么推荐吗?
  9. AVD Manager创建模拟器
  10. GIC Partitioning