当你准备自定义view的时候
2017-02-23 14:46
357 查看
前言
一直以来 写一些小demo没什么总结性,今天先放个坑,谈谈对自定义的理解 也作为自己的网络笔记。讲讲自己对常用方法的理解,后续会补上相关实际的实例
先分享一些我已经发过的比较简单的自定义view
简易实现Listview滑动删除 (通用任意view)
Android DIY之路 (一) 指定区域多图片合成 放大 缩小 镜像 旋转 等
Android DIY之路 (三) 手绘 仅在限定区域留下痕迹 并再现这一过程
自定义Toast效果,windows层添加view,多个Toast效果
自定义edittext 手机格式 银行卡格式
手势密码实用demo
Android DIY之路 (四)拖拽替换,一个view发送其他所有view绑定即可监听到。拖拽排序的核心
都是日常的积累,大家可以讨论,比如再现手绘过程,都还没写完善。
进入正题
自定义view核心无非3点测量==== 布局==== 绘制
*自定义事件
那么根据这个思路,我们先直接继承一个View啥也不写,打印出可能会用到的方法,扔在Activity里面进行研究。
public class ExView extends View { private String exViewMethodInturn = "exViewMethodInturn----->"; public ExView(Context context) { this(context, null); } public ExView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ExView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public String getExViewMethodInturn() { return exViewMethodInturn; } private void init() { exViewMethodInturn += "init-->"; Log.i("rex", "init"); } //重写常用的方法,进行打印 } <com.justforview.view.ExView android:id="@+id/exView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:background="#DEDEDE" />
调用顺序:
init(构造方法)—–>onFinishInflate
—–>onAttachedToWindow
—–>onMeasure
—–>onSizeChanged
—–>onLayout
—–>onMeasure( -2147482978,-2147482656)
—–>onLayout
—–>onDraw
—–>dispatchDraw
(经过打印,和手机上画面显示,我们发现有些方法被调用了多次,onMeasure 数值什么鬼?,wrap无效的现象。下面我们来一一探讨)
*废话*:官方的方法名,一般起的 都能理解个一二,以后回忆起来也好记,measure测量 哪里绘制,draw诸如此类。(学好英语很重要)
构造方法
一般用作(AttributeSet attrs),自定义属性的获取,和一些初始化, new Paint() 啊 TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.XXXiew); a.getColor啥的; 暂不讨论。1.onFinishInflate
Inflate 很面熟吧 我就记“打气”比如给娃娃充气,使她感到充实,成为一个完整的个体。用一个XML源填充view,加上finish,那就是完事。常用:LayoutInflater.from(getContext()).inflate() / View.inflate()
所以finish了把xml打气成一起view,但要注意的是,每个部位都充气充好,才是真正的完事。
即当View中所有的子控件均被映射成xml后触发
2.onAttachedToWindow
Attach(附在)Window(窗体)上View和Window缠绵在一起(绑定时)就会调用这个函数(那么这个函数的作用 你懂得)
按再打印出生命周期onCreate->onStart->onResume->onAttachedToWindow
Activity的onResume生命周期后就能获取DecorView的LayoutParam,进而可以设置高度和宽度了。根据上面贴出的生命周期图,onResume()后面是onAttachedToWindow(),并且onAttachedToWindow只会调用一次,不会受用户操作行为影响。所以
在onAttachedToWindow中进行窗口尺寸的修改再合适不过了
DecorView的LayoutParams是在ActivityThread的handleResumeActivity中设置的,并且该函数会调用Activity的onResume生命周期,所以在onResume之后可以设置窗体尺寸
3.onMeasure
测量相信多多少会提到,比如获取宽高的时候为0啊。测量也非常重要,那它是干嘛的,怎么测量,自定义的时候有什么作用。包括上述例子中经典现象wrap_content是无效的(填具体数值有效)。widthMeasureSpec, heightMeasureSpec -2147482978又是什么。咋还被多次调用呢?这方法被重写的几率高吗?其实不高。大多需求影响没有那么大(那说个卵…)好的,那就开始说个卵,你不重写它,wrap_content是无效的,简言之,你不告诉它包裹的内容是啥,它就默认干别的去了比如漫不经心的充满布局啥的。TextView 告诉了它包裹的是字,在哪里告诉的 就是onMeasure。可能到这里好像卵用性还不够。
好了,正经的说View在屏幕上显示出来要先经过measure(计算)宽高 和 layout(布局)位置
3.1 onMeasure/setMeasuredDimension/specSize
为方便研究,我们直接在将宽高设置成,200,100px(不设置dp是为了更明显看他的作用,因为数据实际传递是生成的px,as里面可以设置px)
我们先看widthMeasureSpec(heightMeasureSpec类似)是什么
大小+类型
(多余的解释)
一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。
MeasureSpec是通过将一个int(32)的数组成而成的,本质是一个int数,MeasureSpec由两部分组成:SepcMode 和 SpecSize 。其中SpecMode为MeasueSpec的高2位,SpecSize为MeasureSpec的低30位。
{ //经查阅方法转化 int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); <----->MeasureSpec.makeMeasureSpec //得出来的恰好是200 specMode 先不变。 //说明通过layout-width=200经过上面两个方法之后 到了view的onMeasure的经过MeasureSpec形成边界参数widthMeasureSpec传进来 //那么我们除以2 再给super.onmesure试试 int newwidthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec) / 2, MeasureSpec.getMode(widthMeasureSpec)); super.onMeasure(newwidthMeasureSpec, heightMeasureSpec); } //而实际宽度是 super.onMeasure(newwidthMeasureSpec, heightMeasureSpec)中的newwidthMeasureSpec
到这里就解释了一个疑惑xml中layout-width为什么不是width,一如layout-gravity和gravity关系?(当然这只是我的推测)layout-width是告诉父布局,我想要多大的宽高,父布局surper再给你实际大小。貌似很有道理,我们还是看看源码莫意淫。
源码就一个setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
这个估计就是直接告诉爹地我要多大地盘。我们一试。根据他这个getDefaultSize推断这里是实际宽度,而不是一长串什么鬼(控件可获得的空间以及关于这个空间描述的元数据)。
直接 setMeasuredDimension(100,100);注释掉super依然是个100x100正方形.其实不注释也没关系。实际就是两次的setMeasuredDimension取最后一个效果。
那么ok 从layout-width到widthMeasureSpec最后到再到实际效果的setMeasuredDimension。形成最终的width
网上的一个解释是父布局给你一个限制,你可以遵守或者不遵守。
我觉得是当子view还在xml里面的时候告诉了父亲自己想要多大的空间,等到终于有一天,自己被inflate成为view的时候,父亲将这个值还给了子view,子view可以选择遵从父亲留存当时最初的梦想,也可以变心自己去任性setMeasuredDimension。
上段都是废话。核心就是能得到xml里面(或者inflate之前)的值,你可以选择改或者不改(setMeasuredDimension/super.measuer—->setMeasuredDimension)。
如果你不改就会取默认的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
(默认填充父布局?)现在大小我们知道了,再看看最后一个specMode 是什么。
3.2 specMode
specMode 根据数值(上面的方法也可以看到):
1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
2. AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
3. UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
3.3 wrap_content无效的原因
由上面两个方法得出默认是没有处理wrap_content,且默认效果是填充父容器
(详细原因)
在默认实现中,AT_MOST和EXACTLY两种模式都会被设置成specSize。我们也知道AT_MOST对应于wrap_content,而EXACTLY对应于match_parent和具体数值情况。也就说默认情况下wrap_content和match_parent是具有相同的效果的。那有人会问:wrap_content和match_parent具有相同的效果,为什么是填充父容器的效果呢? 下面讲一下View的绘制过程: View的绘制首先起于ViewRootImpl,并且View的三个流程也是通过ViewRootImpl来完成的,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建viewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。之后,View的绘制过程从ViewRootImpl的performTraversals方法开始,它经过measure、layout、draw三个过程最终将一个View绘制出来,由于DecorView是Android的一个页面的顶级View,所以绘制过程首先会从DecorView开始,又因为DecorView是一个ViewGroup,它会遍历绘制所有的子View,我们注意到在protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中会有两个参数,其实这两个参数是在ViewGroup中传入进去的,最初是在DecorView中赋值的,且其值就是屏幕的宽度和高度。.也就是下面的windowSize
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
关于wrap_content无效的可参考博客
https://my.oschina.net/ccqy66/blog/616662
3.4很多次的onmeasure
一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小
4.onSizeChanged
它的方法名已经告诉我们了,这个方法会在这个view的大小发生改变是被系统调用,我们要记住的就是view大小变化,这个方法就被执行就可以了.我们啥也没变它也调用了?0开始变的嘛。参数名int w, int h, int oldw, int oldh。就不再赘述。5.onLayout
该方法是View的放置方法,在View类实现。调用该方法需要传入放置View的矩形空间左上角left、top值和右下角right、bottom值。这四个值是相对于父控件而言的。例如传入的是(10, 10, 100, 100),则该View在距离父控件的左上角位置(10, 10)处显示,显示的大小是宽高是90(参数r,b是相对左上角的),这有点像绝对布局。平常开发所用到RelativeLayout、LinearLayout、FrameLayout…这些都是继承ViewGroup的布局。这些布局的实现都是通过都实现ViewGroup的onLayout方法,只是实现方法不一样而已
// 动态获取子View实例 for (int i = 0, size = getChildCount(); i < size; i++) { View view = getChildAt(i); // 放置子View,宽高都是100 view.layout(l, t, l + 100, t + 100); l += 100 + padding; }
onMeasure在整个界面上需要放置一样东西或拿掉一样东西时会调用。比如addView就是放置,removeview就是拿掉,另外比较特殊的是,child设置为gone会触发onMeasure,但是invisible不会触发onMeasure。一旦执行过onMeasure,往往就会执行onLayout来重新布局
5.onDraw/dispatchDraw ()
由于onMeasure大佬太占位置,写了半天才写到能体现自定义view的精髓的Draw.这也是自定义view最有成就的地方调用流程 :
mView.draw()开始绘制,draw()方法实现的功能如下:
1 、绘制该View的背景
2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)
值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类
函数实现具体的功能。
dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个
地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能
实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
但我曾经写到高亮引导(或者二维码中间扣一块透明里面有用到dispatchDraw )
自定义高亮区域 作用户引导的思路
@Override protected void dispatchDraw(Canvas canvas) { //核心先后顺序 if (rects.size() != 0) { drawRect(canvas);//绘制高亮区域 super.dispatchDraw(canvas);//绘制子view -->如提示文字 箭头 } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制半透明背景 }
*invalidate()方法
说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
一般引起invalidate()操作的函数如下: 1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。 2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。 3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法, 继而绘制该View。 4 、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
*requestLayout()方法
会导致调用measure()过程 和 layout()过程 。说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制
任何视图包括该调用者本身。
一般引起invalidate()操作的函数如下: 1、setVisibility()方法: 当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。 同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。 requestFocus()函数说明: 说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。
下面一篇讲述关于绘制过程比较深刻的微博 虽年代久远,但依然获益匪浅。
http://blog.csdn.net/qinjuning/article/details/7110211/
持续更新…
相关文章推荐
- 自定义一个View用UIcontroller去调用的时候
- 自定义View的时候出错
- ViewController 的loadView、viewDidLoad、viewDidUnload分别是什么时候调用的,在自定义ViewCointroller时在这几个函数中应该做什么工作?
- 当RecycleView跟ScrollView冲突设置自定义LinearLayoutManager的时候出现IllegalArgumentException异常
- 当你使用ScorllView的时候
- 原创:当你准备去面试不知道坐什么公交车的时候,我想你需要使用这个功能
- 自定义View的时候,报错android.view.InflateException: Binary XML file line #0: Binary XML file line #0:
- 自定义View里面的自定义属性的时候报错:Attribute "color" has already been defined
- 自定义导航栏的时候使用UIImagePickerViewControlle Item无法正常显示
- RecyclerView-错误收集:当你刷新RecyclerView程序崩掉的时候(1)
- 2014-11-3Android学习------关于自定义视图View的时候需要调用onMeasure--------GIF动画实现
- 自定义view在activity销毁时候保存数据
- 自定义View知识基础准备(一)
- 自定义View的时候onMeasure()理解
- 自定义TitleView,用来实现开发时候统一标题栏
- ViewController 的loadView、viewDidLoad、viewDidUnload分别是什么时候调用的,在自定义ViewCointroller时在这几个函数中应该做什么工作?
- 自定义view(自定义view的时候,三个构造函数各自的作用)
- ViewController 的loadView、viewDidLoad、viewDidUnload分别是什么时候调用的,在自定义ViewCointroller时在这几个函数中应该做什么工作?
- 一个自定义的 View,支持显示下载进度,完成和结束的时候会有酷酷的动画。
- 自定义view,移动view的时候抖动很厉害的解决方案