Android自定义View,你必须知道的几点
为什么我们觉得自定义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,你必须知道的几点相关推荐
- Android自定义View —— TypedArray
在上一篇中Android 自定义View Canvas -- Bitmap写到了TypedArray 这个属性 下面也简单的说一下TypedArray的使用 TypedArray 的作用: 用于从该结 ...
- Android 自定义View —— Canvas
上一篇在android 自定义view Paint 里面 说了几种常见的Point 属性 绘制图形的时候下面总有一个canvas ,Canvas 是是画布 上面可以绘制点,线,正方形,圆,等等,需要和 ...
- android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...
转载:http://blog.csdn.net/xiabing082/article/details/48781489 1. 大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...
- Android自定义View:ViewGroup(三)
自定义ViewGroup本质是什么? 自定义ViewGroup本质上就干一件事--layout. layout 我们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewG ...
- android 自定义图形,Android自定义View之图形图像(模仿360的刷新球自定
概述: 360安全卫士的那个刷新球(姑且叫它刷新球,因为真的不知道叫什么好,不是dota里的刷新球!!),里面像住了水一样,生动可爱,看似简单,写起来不太简单,本例程只是实现了它的部分功能而已,说实话 ...
- android代码实现手机加速功能,Android自定义View实现内存清理加速球效果
Android自定义View实现内存清理加速球效果 发布时间:2020-09-21 22:21:57 来源:脚本之家 阅读:105 作者:程序员的自我反思 前言 用过猎豹清理大师或者相类似的安全软件, ...
- android中仿qq最新版抽屉,Android 自定义View实现抽屉效果
Android 自定义View实现抽屉效果 说明 这个自定义View,没有处理好多点触摸问题 View跟着手指移动,没有采用传统的scrollBy方法,而是通过不停地重新布局子View的方式,来使得子 ...
- Android 自定义 圆环,Android自定义view实现圆环效果实例代码
先上效果图,如果大家感觉不错,请参考实现代码. 重要的是如何实现自定义的view效果 (1)创建类,继承view,重写onDraw和onMesure方法 public class CirclePerc ...
- android自定义抽奖,Android自定义view制作抽奖转盘
本文实例为大家分享了Android自定义view制作抽奖转盘的具体代码,供大家参考,具体内容如下 效果图 TurntableActivity package com.bawei.myapplicati ...
- android view 渐变动画,Android自定义view渐变圆形动画
本文实例为大家分享了Android自定义view渐变圆形动画的具体代码,供大家参考,具体内容如下 直接上效果图 自定义属性 attrs.xml文件 创建一个类 ProgressRing继承自 view ...
最新文章
- POJ - 1321 棋盘问题
- 前端学习(2023)vue之电商管理系统电商系统之通过路由加载订单列表
- h5 android数字键盘,【笔记】移动端H5数字键盘input type=number的处理(IOS和Android)...
- php析构函数使用,php析构函数__destruct()使用方法及实例讲解
- 《南溪的python灵隐笔记》——tqdm的学习笔记
- PAT 乙级 1008. 数组元素循环右移问题 (20) Java版
- 在线图片坐标拾取工具
- python html转图片失真_html dom 转化成图片踩坑记(canvas toDataURL)
- 如何用tomcat发布自己的Java项目
- Python 命令行库的大乱斗 | 凌云时刻
- 出租车轨迹数据地图匹配
- 使用Liquid实现简单的数据交换
- MPUSH消息推送服务器搭建,MPUSH消息推送服務器搭建
- 如何编辑修改PDF文件的内容?
- matlab四叉树分割代码,【测绘专用】 MATLAB 四叉树分割遥感图像
- 堪萨斯州立大学计算机科学,堪萨斯州立大学相当于国内哪所大学?
- 警告: Category is implementing a method which will also be implemented by its primary class
- 小米雷军打出王炸,始料未及的华为余承东一下子懵了
- 你一年就工作一天还想请假......
- java开发实战经典(第二版)P528 14-1