好就没有看csdn了,发现好多东西可以学了,唉,最近股市的震荡头有点痛,哈哈,搞个学习的心情都没有。然后一直拖到现在简历也没有改好,就还是要看一些东西,怕哪天心血来潮去面试随便一个东西都答不上来那就是有点蛋疼了。当问到自定义控件的时候,或许你知道怎么用,可能说起来也就不是那么回事了啊,所以还是有必要去把一些原理去搞清楚。
一、View
一般自定义控件的话呢,可实现的空间还是很大的,如果你只是想改变原控件的属性那就继承相对的控件就好了,但是我们更多的还是继承Layout、view/viewGroup等,这样的自定义空间会比较大点,所以我们还是讲讲view和viewGroup吧。
View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。这就证明了一点,View代表了用户界面组件的一块可绘制的空间块。每一个View在屏幕上占据一个长方形区域。在这个区域内,这个VIEW对象负责图形绘制和事件处理。View是小控件widgets和ViewGroup的父类。ViewGroup又是Layout的基类。
最简单的自定义就是加载布局,所以不得不说下setContentView(View view, ViewGroup.LayoutParams params)和LayoutInflater.inflate(layoutResID, mContentParent)的区别,
哈哈哈,先来看下我们每加载创建一个activity的onCreate(Bundle savedInstanceState)方法中都会有一个初始布局的加载,然后不难发现我们的setContentView()有3个构造方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
因为这是api22的初始化,所以是actionBar的init,然后不得不知道phoneWindow这个类,PhoneWindow类的setContentView(int layoutResID)方法源码
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
可以看出,首先判断mContentParent是否为null,也就是第一次调运);如果是第一次调用,则调用installDecor()方法,否则判断是否设置FEATURE_CONTENT_TRANSITIONS Window属性(默认false),如果没有就移除该mContentParent内所有的所有子View;接着mLayoutInflater.inflate(layoutResID, mContentParent);将我们的资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中(其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context);)。我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews。
然后我们来看LayoutInflater.inflate()
LayoutInflater lif = LayoutInflater.from(Context context);
LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
这两方法很熟悉吧,接下来看源码
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext;
//定义返回值,初始化为传入的形参root
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果一开始就是END_DOCUMENT,那说明xml文件有问题
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//有了上面判断说明这里type一定是START_TAG,也就是xml文件里的root node
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
//处理merge tag的情况(merge,你懂的,APP的xml性能优化)
//root必须非空且attachToRoot为true,否则抛异常结束(APP使用merge时要注意的地方,
//因为merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容加到某个上层
//ViewGroup中。)
if (root == null || !attachToRoot) {
throw new InflateException("
+ "ViewGroup root and attachToRoot=true");
}
//递归inflate方法调运
rInflate(parser, root, attrs, false, false);
} else {
// Temp is the root view that was found in the xml
//xml文件中的root view,根据tag节点创建view对象
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
//根据root生成合适的LayoutParams实例
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//如果attachToRoot=false就调用view的setLayoutParams方法
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp
//递归inflate剩下的children
rInflate(parser, temp, attrs, true, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
//root非空且attachToRoot=true则将xml文件的root view加到形参提供的root里
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
//返回xml里解析的root view
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
//返回参数root或xml文件里的root view
return result;
}
}
从上面的源码分析我们可以看出inflate方法的参数含义:
inflate(xmlId, null); 只创建temp的View,然后直接返回temp。
inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。
inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。
inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。
到此其实已经可以说明我们上面示例部分执行效果差异的原因了(在此先强调一个Android的概念,下一篇文章我们会对这段话作一解释:我们经常使用View的layout_width和layout_height来设置View的大小,而且一般都可以正常工作,所以有人时常认为这两个属性就是设置View的真实大小一样;然而实际上这些属性是用于设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫作layout_width和layout_height,而不是width和height的原因了。),如下:
mInflater.inflate(R.layout.textview_layout, null)
不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。mInflater.inflate(R.layout.textview_layout, parent)
能正确显示我们设置的宽高是因为我们的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)
不为空。 Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这个params正是root.generateLayoutParams(attrs);得到的。mInflater.inflate(R.layout.textview_layout, null, true)与mInflater.inflate(R.layout.textview_layout, null, false)
不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
//final方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
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;
//通过MeasureSpec解析获取mode与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等于AT_MOST或EXACTLY就返回specSize,也就3种状态,UNSPECIFIED我基本上觉得是不需要管的。通过取得的大小我们就会setMeasuredDimension()然后控件的大小就这样的确定了。
接着是OnLayout():
先看下ViewGroup的layout方法,如下:
@Override
public final void layout(int l, int t, int r, int b) {
......
super
.layout(l, t, r, b);
......
}
public void layout(int l, int t, int r, int b) {
......
//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if
(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回调onLayout
onLayout(changed, l, t, r, b);
......
}
......
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
ViewGroup的onLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自 定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用 onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout 的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
//计算父窗口推荐的子View宽度
final int width = right - left;
//计算父窗口推荐的子View右侧位置
int childRight = width - mPaddingRight;
// Space available for child
//child可使用空间大小
int childSpace = width - paddingLeft - mPaddingRight;
//通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
final int count = getVirtualChildCount();
//获取Gravity属性设置
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//依据majorGravity计算childTop的位置值
switch
(majorGravity) {
case
Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break
;
// mTotalLength contains the padding already
case
Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break
;
case
Gravity.TOP:
default
:
childTop = mPaddingTop;
break
;
}
//重点!!!开始遍历
for
(int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if
(child ==
null
) {
childTop += measureNullChild(i);
}
else
if
(child.getVisibility() != GONE) {
//LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取子View的LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if
(gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//依据不同的absoluteGravity计算childLeft位置
switch
(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case
Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break
;
case
Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break
;
case
Gravity.LEFT:
default
:
childLeft = paddingLeft + lp.leftMargin;
break
;
}
if
(hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//通过垂直排列计算调运child的layout设置child的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void performTraversals() {
......
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
}
绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:
如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
View的绘制是借助onDraw方法传入的Canvas类来进行的。
区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对 ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对 LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
我们首先来了解Dispay。Dispay代表了硬件显示屏幕信息。
通过这些函数可以了解一个屏幕的宽、高及分辨率还有是横屏还坚屏等一些基本情况,透过这些函数,我们开发应用时可以方便的得到当前安装我这个应用的屏幕的大小,以便调整应用使用户得到更好的用户体验。接下来我们看其它三者之间的关系,我想大家虽然看了前面的View的介绍和SDK中关系UI的基本介绍之后还是对Android图形窗口十分困惑,看API也是,有WindowMangaer接口和Window类,但是在说明文档中,并未提到如何用这些。但实际上这里面要去看到Android核心,Android核心底层GDI是SKIA,同chrome是一样的GDI,但是GUI是不一样的。这里面Android实现的是C/S模式。如下图所示
从上图,我们可以理出大致的显示过程如下:
1、ActivityManagerService创建Activity线程,激活一个activity
2、系统调用Instrumentation.newActivity创建一个activity
3、Activity创建后,attach到一个新创建的phonewindow中。这样Activity获取一个唯一的WindowManager服务的实例
4、Activity创建过程中使用setcontentView设置用用户UI,这些VIEW被加入到PhoneWindow的ContentParent中。
5、Activity线程继续执行,当执行到Activity.makeVisible是将根view DecoView加入到WindowManger中,WindowManger实全会为每个DecoView创建对应的ViewRoot
6、每个ViewRoot拥有一个Surface,每个Surface将会调用底层库创建图形绘制的内存空间。这个底层库就是SurfaceFlinger。SurfaceFlinger同时也负责将个View绘制的图形合到一块(按照Z轴)显示到用户屏幕。
7、如果用户直接在Canvas上绘制,实际上它直接操作Surface。但对每个View的变更,它是要通知到ViewRoot,然后ViewRoot获取Canvas。如果绘制完成,surfaceFlinger得到通知,合并Surface成一个Surface到设备屏幕。
从上面的图形输出过程分析,我们可以知道真正显示图形的实际上跟Activity没有关系,完全由WindowManager来决定。WindowManager是一个系统服务,因此可以直接调用这个服务来创建界面,并且更绝的是Dialog、Menu也是有WindowManager来管理的。另外一个我们也可以看到,最底层都是Surface来,因此,常见开发游戏的人都推荐你使用SurfaceView来创建界面。