您的位置:首页 > 其它

当你准备自定义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/

持续更新…

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐