为什么我们觉得自定义View是学习Android的一道坎?
为什么那么多Android大神却认为自定义View又是如此的简单?
为什么google随便定义一个View都是上千行的代码?
以上这些问题,相信学Android的同学或多或少都有过这样的疑问。
那么,看完此文,希望对你们的疑惑有所帮助。

回到主题,自定义View ,需要掌握的几个点是什么呢?
我们先把自定义View细分一下,分为两种
1) 自定义ViewGroup
2) 自定义View

其实ViewGroup最终还是继承之View,当然它内部做了许多操作;继承之ViewGroup的View我们一般称之为容器,而今天我们不讲这方面,后续有机会再讲。
来看看自定义View 需要掌握的几点,主要就是两点

一、重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法。
二、重写 protected void onDraw(Canvas canvas) {}方法

空讲理论很难理解,我们还得用例子来说明,记得我前面来写了一篇 Android 微信6.1 tab栏图标和字体颜色渐变的实现 的博客,里面tab的每个item就是通过自定义View来实现的,那么接下来就通过此例子来说明问题。

我们可以把View理解为一张白纸,而自定义View就是在这张白纸上画上我们自己绘制的图案,可以在绘制任何图案,也可以在白纸的任何位置绘制,那么问题来了,白纸哪里来?图案哪里来?位置如何计算?

a)白纸好说,只要我们继承之View,在onDraw(Canvas canvas)中的canvas就是我们所说的白纸

/*** Created by moon.zhong on 2015/2/13.*/
public class CustomView extends View {public CustomView(Context context) {super(context);}public CustomView(Context context, AttributeSet attrs) {super(context, attrs);}public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onDraw(Canvas canvas) {// canvas 即为白纸super.onDraw(canvas);}
}

b)图案呢?这里的图案就是有图片和文字组成,这个也好说,定义一个Bitmap 成员变量,和一个String的成员变量

private Bitmap mBitmap ;
private String mName ;
mName = "这里直接赋值";
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;

图片可以通过资源文件可以拿到。

c)计算位置
所以最核心的也是我们认为最麻烦的地方就是计算绘制的位置,计算位置就得先测量自身的大小,也就是我们必须掌握的两点中的第一点:需要重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法
先来看一下google写的TextView的onMeasure()方法是如何实现的

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);int width;int height;BoringLayout.Metrics boring = UNKNOWN_BORING;BoringLayout.Metrics hintBoring = UNKNOWN_BORING;if (mTextDir == null) {mTextDir = getTextDirectionHeuristic();}int des = -1;boolean fromexisting = false;if (widthMode == MeasureSpec.EXACTLY) {// Parent has told us how big to be. So be it.width = widthSize;} else {if (mLayout != null && mEllipsize == null) {des = desired(mLayout);}if (des < 0) {boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);if (boring != null) {mBoring = boring;}} else {fromexisting = true;}if (boring == null || boring == UNKNOWN_BORING) {if (des < 0) {des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));}width = des;} else {width = boring.width;}final Drawables dr = mDrawables;if (dr != null) {width = Math.max(width, dr.mDrawableWidthTop);width = Math.max(width, dr.mDrawableWidthBottom);}if (mHint != null) {int hintDes = -1;int hintWidth;if (mHintLayout != null && mEllipsize == null) {hintDes = desired(mHintLayout);}if (hintDes < 0) {hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);if (hintBoring != null) {mHintBoring = hintBoring;}}if (hintBoring == null || hintBoring == UNKNOWN_BORING) {if (hintDes < 0) {hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth(mHint, mTextPaint));}hintWidth = hintDes;} else {hintWidth = hintBoring.width;}if (hintWidth > width) {width = hintWidth;}}width += getCompoundPaddingLeft() + getCompoundPaddingRight();if (mMaxWidthMode == EMS) {width = Math.min(width, mMaxWidth * getLineHeight());} else {width = Math.min(width, mMaxWidth);}if (mMinWidthMode == EMS) {width = Math.max(width, mMinWidth * getLineHeight());} else {width = Math.max(width, mMinWidth);}// Check against our minimum widthwidth = Math.max(width, getSuggestedMinimumWidth());if (widthMode == MeasureSpec.AT_MOST) {width = Math.min(widthSize, width);}}int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();int unpaddedWidth = want;if (mHorizontallyScrolling) want = VERY_WIDE;int hintWant = want;int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();if (mLayout == null) {makeNewLayout(want, hintWant, boring, hintBoring,width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);} else {final boolean layoutChanged = (mLayout.getWidth() != want) ||(hintWidth != hintWant) ||(mLayout.getEllipsizedWidth() !=width - getCompoundPaddingLeft() - getCompoundPaddingRight());final boolean widthChanged = (mHint == null) &&(mEllipsize == null) &&(want > mLayout.getWidth()) &&(mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);if (layoutChanged || maximumChanged) {if (!maximumChanged && widthChanged) {mLayout.increaseWidthTo(want);} else {makeNewLayout(want, hintWant, boring, hintBoring,width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);}} else {// Nothing has changed}}if (heightMode == MeasureSpec.EXACTLY) {// Parent has told us how big to be. So be it.height = heightSize;mDesiredHeightAtMeasure = -1;} else {int desired = getDesiredHeight();height = desired;mDesiredHeightAtMeasure = desired;if (heightMode == MeasureSpec.AT_MOST) {height = Math.min(desired, heightSize);}}int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));}/** We didn't let makeNewLayout() register to bring the cursor into view,* so do it here if there is any possibility that it is needed.*/if (mMovement != null ||mLayout.getWidth() > unpaddedWidth ||mLayout.getHeight() > unpaddedHeight) {registerForPreDraw();} else {scrollTo(0, 0);}setMeasuredDimension(width, height);
}

哇!好长!而且方法中还嵌套方法,如果真要算下来,代码量不会低于500行,看到这么多代码,头都大了,我想这也是我们为什么在学习Android自定义View的时候觉得如此困难的原因。大多数情况下,因为我们是自定义的View,可以说是根据我们的需求定制的View,所以很多里面的功能我们完全没必要,只需要几十行代码就能搞定。看到几十行代码就能搞定,感觉顿时信心倍增(^.^)
在重写这个方法之前,得先了解一个类 MeasureSpec ,如果不了解,没关系,下面就一起来了解一下这个类。先把代码贴出来,膜拜一下
public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK  = 0x3 << MODE_SHIFT;public static final int UNSPECIFIED = 0 << MODE_SHIFT;public static final int EXACTLY     = 1 << MODE_SHIFT;public static final int AT_MOST     = 2 << MODE_SHIFT;public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}
}

这里我把里面一些我认为没必要的代码都去掉了,只留了以上几行代码,这样看起来很清晰,也非常容易理解。
我们先做个转化,把上面几个成员变量转化成二进制这个就不需要转化了,这里代表的只是一个移动的位置,也就是一个单纯的数字
private static final int MODE_SHIFT = 30;
0x3 就是 11 左移30位 ,就是补30个0;
private static final int MODE_MASK  = 1100 0000 0000 0000 0000 0000 0000 0000 ;
00 左移30位
public static final int UNSPECIFIED = 0000 0000 0000 0000 0000 0000 0000 0000 ;
01 左移30位
public static final int EXACTLY     = 0100 0000 0000 0000 0000 0000 0000 0000 ;
10 左移30位
public static final int AT_MOST     = 1000 0000 0000 0000 0000 0000 0000 0000 ;你就会问了,这样写有什么好处呢? 细心的人看了上面这几个方法就明白了,每个方法中都有一个 & 的操作,所以我们接下来看看这集几个方法的含义是什么,先从下往上看,先易后难1、       public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}顾名思义,通过measureSpec这个参数,获取size ,两个都是int类型,怎么通过一个int类型的数获取另一个int类型的数。我们在学习java的时候知道,一个int类型是32位,任何int类型的数都是有32位,比如一个int类型的数值3,它也是占有32位,只是高30位全部为0。google 也是利用这一点,让这个int类型的measureSpec数存了两个信息,一个就是size,保存在int类型的低30位,另一个就是mode,保存在int类型的高2位。前面我们看到了有几个成员变量,UNSPECIFIED,EXACTLY,AT_MOST者就是mode的三种选择,目前也只有这三种选择,所以只需要2位就能实现。2、      ` public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}`这也好理解,获取模式,但这些模式有啥用处呢?1)、EXACTLY 模式: 准确的、精确的;这种模式,是最容易理解和处理的,可以理解为大小固定,比如在定义layout_width的时候,定义为固定大小 10dp,20dp,或者match_parent(此时父控件是固定的)这时候,获取出来的mode就是EXACTLY2)、AT_MOST 模式: 最大的;这种模式稍微难处理些,不过也好理解,就是View的大小最大不能超过父控件,超过了,取父控件的大小,没有,则取自身大小,这种情况一般都是在layout_width设为warp_content时。3)、UNSPECIFIED 模式:不指定大小,这种情况,我们几乎用不上,它是什么意思呢,就是View的大小想要多大,就给多大,不受父View的限制,几个例子就好理解了,ScrollView控件就是。3、        `public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}`这个方法也好理解,封装measureSpec的值,在定义一个View的大小时,我们只是固定了大小,你下次想要获取mode的时候,肯定无法拿到,所以就得自己把模式添加进去,这个方法,在自定义View中,也基本不需要用到,他所使用的场所,是在设置子View的大小的时候需要用到,所以如果是自定义ViewGroup的话,就需要用到。感觉讲了这么多,还是不知道怎么使用,接下来就来重写onMeasure()方法,写完之后,你就明白了,这里把注解下载代码里头。
  @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出来。int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);//View 真正需要显示的大小int width = 0, height = 0;//这里是去测量字体大小measureText();//字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列int contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());// 我们渴望得到的宽度int desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;//重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的layout_widthswitch (widthMode) {//如果是AT_MOST,不能超过父View的宽度case MeasureSpec.AT_MOST:width = Math.min(widthSize, desiredWidth);break;//如果是精确的,好说,是多少,就给多少;case MeasureSpec.EXACTLY:width = widthSize;break;//这种情况,纯属在这里打酱油的,可以不考虑case MeasureSpec.UNSPECIFIED://我是路过的width = desiredWidth;break;}int contentHeight = mBoundText.height() + mIconNormal.getHeight();int desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;switch (heightMode) {case MeasureSpec.AT_MOST:height = Math.min(heightSize, desiredHeight);break;case MeasureSpec.EXACTLY:height = heightSize;break;case MeasureSpec.UNSPECIFIED:height = contentHeight;break;}//最后不要忘记了,调用父类的测量方法setMeasuredDimension(width, height);}
到这里,就算View的大小就已经完成了,自定义View的计算过程和以上方法基本类似。接着就是计算需要显示的图标和字体的位置。这里希望图片和字体垂直排列,并居中显示在View当中,因为当前的View的宽高已经测量好了,接下来的计算也就非常简单了,这里就放在onDraw()方法中计算d)绘制图标和字体
绘制图标,可以用canvas.drawBitmap(Bitmap bitmap, int left, int top ,Paint paint)方法,bitmap 已经有了,如果不需要对图片作特殊处理 paint 可以传入null表示原图原样的绘制在白纸上,所以就差绘制的位置 left ,top前面已经分析过了,需要把图绘制在View的中间,当然这里还需包含字体,所以可以这样计算left 和top。

int left = (mViewWidth - mIconNormal.getWidth())/2 ;
int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;


mViewWidth --->View的宽度,mIconNormal --->图片的宽度, mBoundText.height() --->字体的高度;绘制字体,绘制字体,就比绘制图片稍微麻烦点,因为绘制字体需要用到画笔Paint ,这里定义一个画笔Paint,直接new 一个出来
    mTextPaintNormal = new Paint();//设置字体大小mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));//设置画笔颜色,也就是字体颜色mTextPaintNormal.setColor(mTextColorNormal);//设置抗锯齿mTextPaintNormal.setAntiAlias(true);
        这里也是调用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要绘制的字体内容, mTextPaintNormal画笔,x,y需要绘制的位置
    float x = (mViewWidth - mBoundText.width())/2.0f ;float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;整体来说代码还是相当少的。下面把onDraw的代码也贴出来@Overrideprotected void onDraw(Canvas canvas) {drawBitmap(canvas) ;drawText(canvas) ;}private void drawBitmap(Canvas canvas) {int left = (mViewWidth - mIconNormal.getWidth())/2 ;int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;canvas.drawBitmap(mIconNormal, left, top ,null);}private void drawText(Canvas canvas) {float x = (mViewWidth - mBoundText.width())/2.0f ;float y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;canvas.drawText(mTextValue,x,y, mTextPaintNormal);}

“`
总结:
onMeasure() 方法只要了解了 MeasureSpec 类就不是什么问题,而MeasureSpec 也很简单,onDraw() 方法就需要了解Canvas 类的绘制方法,并且通过简单的Api查询,就基本能实现我们所需的要求。对于自定义View,如果你会重写 测量 和 onDraw 方法,那么就具备了此技能,而如果需要了解更深,自定义有个性,更绚丽的View,就还得深入了解Canvas 、Paint等方法,

源码下载地址,请前往我的另一篇博客 Android 微信6.1 tab栏图标和字体颜色渐变的实现

Android自定义View,你必须知道的几点相关推荐

  1. Android自定义View —— TypedArray

    在上一篇中Android 自定义View Canvas -- Bitmap写到了TypedArray 这个属性 下面也简单的说一下TypedArray的使用 TypedArray 的作用: 用于从该结 ...

  2. Android 自定义View —— Canvas

    上一篇在android 自定义view Paint 里面 说了几种常见的Point 属性 绘制图形的时候下面总有一个canvas ,Canvas 是是画布 上面可以绘制点,线,正方形,圆,等等,需要和 ...

  3. android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  4. Android自定义View:ViewGroup(三)

    自定义ViewGroup本质是什么? 自定义ViewGroup本质上就干一件事--layout. layout 我们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewG ...

  5. android 自定义图形,Android自定义View之图形图像(模仿360的刷新球自定

    概述: 360安全卫士的那个刷新球(姑且叫它刷新球,因为真的不知道叫什么好,不是dota里的刷新球!!),里面像住了水一样,生动可爱,看似简单,写起来不太简单,本例程只是实现了它的部分功能而已,说实话 ...

  6. android代码实现手机加速功能,Android自定义View实现内存清理加速球效果

    Android自定义View实现内存清理加速球效果 发布时间:2020-09-21 22:21:57 来源:脚本之家 阅读:105 作者:程序员的自我反思 前言 用过猎豹清理大师或者相类似的安全软件, ...

  7. android中仿qq最新版抽屉,Android 自定义View实现抽屉效果

    Android 自定义View实现抽屉效果 说明 这个自定义View,没有处理好多点触摸问题 View跟着手指移动,没有采用传统的scrollBy方法,而是通过不停地重新布局子View的方式,来使得子 ...

  8. Android 自定义 圆环,Android自定义view实现圆环效果实例代码

    先上效果图,如果大家感觉不错,请参考实现代码. 重要的是如何实现自定义的view效果 (1)创建类,继承view,重写onDraw和onMesure方法 public class CirclePerc ...

  9. android自定义抽奖,Android自定义view制作抽奖转盘

    本文实例为大家分享了Android自定义view制作抽奖转盘的具体代码,供大家参考,具体内容如下 效果图 TurntableActivity package com.bawei.myapplicati ...

  10. android view 渐变动画,Android自定义view渐变圆形动画

    本文实例为大家分享了Android自定义view渐变圆形动画的具体代码,供大家参考,具体内容如下 直接上效果图 自定义属性 attrs.xml文件 创建一个类 ProgressRing继承自 view ...

最新文章

  1. POJ - 1321 棋盘问题
  2. 前端学习(2023)vue之电商管理系统电商系统之通过路由加载订单列表
  3. h5 android数字键盘,【笔记】移动端H5数字键盘input type=number的处理(IOS和Android)...
  4. php析构函数使用,php析构函数__destruct()使用方法及实例讲解
  5. 《南溪的python灵隐笔记》——tqdm的学习笔记
  6. PAT 乙级 1008. 数组元素循环右移问题 (20) Java版
  7. 在线图片坐标拾取工具
  8. python html转图片失真_html dom 转化成图片踩坑记(canvas toDataURL)
  9. 如何用tomcat发布自己的Java项目
  10. Python 命令行库的大乱斗 | 凌云时刻
  11. 出租车轨迹数据地图匹配
  12. 使用Liquid实现简单的数据交换
  13. MPUSH消息推送服务器搭建,MPUSH消息推送服務器搭建
  14. 如何编辑修改PDF文件的内容?
  15. matlab四叉树分割代码,【测绘专用】 MATLAB 四叉树分割遥感图像
  16. 堪萨斯州立大学计算机科学,堪萨斯州立大学相当于国内哪所大学?
  17. 警告: Category is implementing a method which will also be implemented by its primary class
  18. 小米雷军打出王炸,始料未及的华为余承东一下子懵了
  19. 你一年就工作一天还想请假......
  20. java开发实战经典(第二版)P528 14-1

热门文章

  1. Random类模拟微信发红包
  2. 通过打开android应用市场来定位你的app
  3. 计算机编程可以用air,Adobe AIR是什么软件?Adobe AIR有什么用?
  4. 前端学习记录——offset与client
  5. event_dlist
  6. (毕老师)html视频笔记
  7. Linux的远程桌面管理,密钥登陆,SSH协议,四层防御系统实验详解
  8. B站左程云算法视频高级班01
  9. 618大促,线上线下联动显威力,或在线下引发更激烈竞争
  10. 智慧工厂解决方案赋能企业智慧运营