View相关知识学习总结
(一)LayoutInflater原理分析
LayoutInflater主要用于加载布局。通常情况下,加载布局的任务都是在Activity中调用setContentView()
方法来完成的,该方法内部使用LayoutInflater来加载布局。
想要使用LayoutInflater,首先需要获取到LayoutInflater的实例,有两种方法可以获取到:
LayoutInflater layoutInflater = LayoutInflater.from(context);
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
其实第一种就是第二种的简单写法,只是Android给我们做了一下封装而已。得到了LayoutInflater的实例之后就可以调用它的inflate()
方法来加载布局了。
layoutInflater.inflate(resourceId, root); //inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。
这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。
LayoutInflater广泛应用于需要动态添加View的时候,比如用在在ScrollView和ListView中。接下来从源码的角度看一下它是如何工作的:
不管使用哪个inflate()
方法的重载,最终都会辗转调用到LayoutInflater的如下代码中:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
/**
* 获取一个实现此AttributeSet的实例。因为此XmlPullParser是继承自AttributeSet
* 的,所以parser对象可以直接作为一个AttributeSet对象。
**/
final AttributeSet attrs = Xml.asAttributeSet(parser);
mConstructorArgs[0] = mContext;
View result = root;
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
//如果它的根节点是一个merge对象,则必须手动设置此view的父节点,否则抛出异常
//因为由merge创建的xml文件,常常被其他layout所包含
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("merge can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, attrs);
} else {
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
//这里是一个递归
rInflate(parser, temp, attrs);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
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;
}
return result;
}
}
LayoutInflater使用Android提供的pull解析方式来解析布局文件。第29行,调用了createViewFromTag()
方法,并把节点名和参数传了进去,它是用于根据节点名来创建View对象的,在该方法的内部又会去调用createView()
方法,然后使用反射的方式创建出View的实例并返回。
至此,只是创建出了一个根布局的实例,接下来在第38行调用的rInflate()
方法会循环遍历这个根布局下的子元素,代码如下所示:
private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//创建View实例
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs);
//递归结束后,添加到父布局中
viewGroup.addView(view, params);
}
}
parent.onFinishInflate();
}
在第22行同样是createViewFromTag()
方法来创建View的实例,然后还会在第25行递归调用rInflate()
方法来查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中。把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()
过程全部结束。
(二)视图绘制流程
每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()
、onLayout()
和onDraw()
。
onMeasure()
onMeasure()
方法用于测量视图的大小。View系统的绘制流程会从ViewRoot的performTraversals()
方法中开始,在其内部调用View的measure()
方法,该方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
该方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()
方法,这里是真正去测量并设置View大小的地方,默认会调用getDefaultSize()
方法来获取视图的大小,如下所示:
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;
}
这里传入的measureSpec是一直从measure()
方法中传递过来的。然后调用MeasureSpec.getMode()
方法解析出specMode,调用MeasureSpec.getSize()
方法解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()
方法中调用setMeasuredDimension()
方法来设定测量出的大小,这样一次measure过程就结束了。
onLayout()
measure结束后是layout的过程,onLayout()
方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()
方法会在measure结束后继续执行,并调用View的layout()
方法来执行此过程,如下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
该方法接收四个参数,分别代表着左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的,这里把刚才测量出的宽度和高度传到了该方法中。
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
在该方法中,首先会调用setFrame()
方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会在第11行调用onLayout()
方法。View中的onLayout()
方法是一个空方法,因为onLayout()
过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup中的onLayout()
方法是怎么写的吧,代码如下:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()
方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法,然后在内部按照各自的规则对子视图进行布局的。
onDraw()
layout过程结束后,接下来就进入到draw的过程了,onDraw()
方法对视图进行绘制。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()
方法来执行具体的绘制工作。代码如下所示:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
//draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// draw the content
if (!dirtyOpaque) onDraw(canvas);
// draw the children
dispatchDraw(canvas);
// draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
从第9行代码开始,先是对视图的背景进行绘制,首先需要得到一个mBGDrawable对象,可以在XML中通过android:background属性设置图片或颜色,也可以在代码中通过setBackgroundColor()
、setBackgroundResource()
等方法进行赋值。然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()
方法来完成背景的绘制工作。
接下来在第34行对视图的内容进行绘制,这里调用了onDraw()
方法,该方法是一个空方法,因为每个视图的内容部分各不相同,这部分的功能交给子类实现。
然后对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了(View中的dispatchDraw()
方法是一个空方法,而ViewGroup的dispatchDraw()
方法中就会有具体的绘制代码)。
最后是对视图的滚动条进行绘制。
由此可以发现,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。TextView、ImageView等类的源码都有重写onDraw()
这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()
方法中,供给每个视图使用。
(三)自定义控件
自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。
自绘控件所展现的内容全部都是我们自己绘制出来的,绘制的代码是写在onDraw()
方法中的。主要步骤如下:
Step 1 写一个类继承View类或者View的子类。
Step 2 写一个构造函数,用于获取属性和参数。
Step 3 重写onMeasure(int,int)方法。(该方法可重写可不重写,具体看需求)
Step 4 重写onDraw(Canvas canvas)方法。
组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起。
继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。
(四)ListView
ListVeiw功能的实现,需要以下三个元素:
(1)ListView中的每一列的View。
(2)填入View的数据或者图片等。
(3)连接数据与ListView的适配器。
ListView的实现是典型的适配器模式。适配器是一个连接数据和AdapterView(ListView等)的桥梁,通过它能有效地实现数据与AdapterView的分离设置,使AdapterView与数据的绑定更加简便,修改更加方便。
ArrayAdapter
ArrayAdapter用来绑定一个数组,支持泛型操作,可以实现简单的ListView的数据绑定。默认情况下,ArrayAdapter绑定每个对象的toString值到layout中预先定义的TextView控件上。通常我们使用android.R.layout.simple_list_item_1
,实现带有一个TextView的ListView,使用android.R.layout.simple_list_item_checked
实现带选择框的ListView,使用android.R.layout.simple_list_item_multiple_choice
实现带CheckBox的ListView,使用android.R.layout.simple_list_item_single_choice
实现带RadioButton的ListView,注意使用后三个资源时,是多选还是单选要通过setChoiceMode()
方法来指定。使用步骤如下:
(1)定义一个数组来存放ListView中item的内容。
(2)通过实现ArrayAdapter的构造函数来创建一个ArrayAdapter的对象。
(3)通过ListView的setAdapter()
方法绑定ArrayAdapter。
SimpleAdapter
可以通过SimpleAdapter自定义ListView中的item的内容,比如图片、多选框等。 使用simpleAdapter的数据一般都是用HashMap构成的列表,列表的每一个节点对应ListView的每一行。通过SimpleAdapter的构造函数,将HashMap的每个键的数据映射到布局文件中对应控件上。使用步骤如下:
(1)根据需要定义ListView每行所实现的布局。
(2)定义一个HashMap构成的列表,将数据以键值对的方式存放在里面。
(3)构造SimpleAdapter对象。
(4)将LsitView绑定到SimpleAdapter上。
BaseAdapter
在ListView中使用SimpleAdapter添加一个按钮到item上,会发现按钮无法获得焦点,点击操作被ListView的item所覆盖,为了使按钮实现单独的操作,可以使用BaseAdapter。BaseAdapter是一个抽象类,它的灵活性就在于使用时需要重写很多方法。当系统开始绘制ListView的时候,首先调用getCount()
方法,返回ListView的长度。然后调用getView()
方法根据长度逐一绘制ListView的每一行,在这个函数里面首先获得一个View(或ViewGroup),然后再实例化并设置各个组件及其数据内容并显示它。而getItem()
和getItemId()
则在需要处理和取得Adapter中的数据时调用。
ListView优化
使用convertView和viewHolder
/*
* 新建一个类继承BaseAdapter,实现视图与数据的绑定
*/
private class MyAdapter extends BaseAdapter {
private LayoutInflater mInflater;// 得到一个LayoutInfalter对象用来导入布局
/* 构造函数 */
public MyAdapter(Context context) {
this.mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return getDate().size();// 返回数组的长度
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(final int position, View convertView,
ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item, null);
holder = new ViewHolder();
holder.title = (TextView) convertView
.findViewById(R.id.ItemTitle);
holder.text = (TextView) convertView
.findViewById(R.id.ItemText);
holder.bt = (Button) convertView.findViewById(R.id.Button);
convertView.setTag(holder);// 绑定ViewHolder对象
} else {
holder = (ViewHolder) convertView.getTag();// 取出ViewHolder对象
}
holder.title.setText(getDate().get(position).get("ItemTitle")
.toString());
holder.text.setText(getDate().get(position).get("ItemText")
.toString());
holder.bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.v("MyListViewBase", "你点击了按钮" + position);
}
});
return convertView;
}
}
当启动Activity呈现第一屏ListView的时候,convertView为null。当用户向下滚动ListView时,上面的条目变为不可见,下面出现新的条目。这时候convertView不再为null,而是创建了一系列的convertView的值。当又往下滚一屏的时候会复用第一屏的已绘制的view。也就是说convertView相当于一个缓存,开始为null,当有条目变为不可见,它缓存了它的实例,后面再出来的条目只需要更新数据就可以了,这样大大节省系统资料的开销。
public final class ViewHolder {
public TextView title;
public TextView text;
public Button bt;
}
当convertView为空时,用setTag()
方法为每个View绑定一个存放控件的ViewHolder对象。当convertView不为空,重复利用已经创建的view,使用getTag()
方法获取绑定的ViewHolder对象,这样就避免了重复使用findViewById所造成的开销。
View相关知识学习总结的更多相关文章
- Android View相关知识问答
Android View相关核心知识问答 Activity Window View之间的三角关系 你真的了解View的坐标吗? 在渲染前获取 View 的宽高 5种手势工具类 浅析Android的窗口
- 客户端相关知识学习(十二)之iOS H5交互Webview实现localStorage数据存储
前言 最近有一个需求是和在app中前端本地存储相关的,所以恶补了一下相关知识 webView开启支持H5 LocalStorage存储 有些时候我们发现写的本地存储没有起作用,那是因为默认WebVie ...
- 客户端相关知识学习(十一)之Android H5交互Webview实现localStorage数据存储
前言 最近有一个需求是和在app中前端本地存储相关的,所以恶补了一下相关知识 webView开启支持H5 LocalStorage存储 有些时候我们发现写的本地存储没有起作用,那是因为默认WebVie ...
- 客户端相关知识学习(六)之deeplink技术
Deeplink应用描述 Deeplink,简单讲,就是你在手机上点击一个链接之后,可以直接链接到app内部的某个页面,而不是app正常打开时显示的首页.不似web,一个链接就可以直接打开web的内页 ...
- Python 多线程相关知识学习
多线程类似于同时执行多个不同程序,多线程运行有如下优点: 使用线程可以把占据长时间的程序中的任务放到后台去处理. 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进 ...
- 服务端相关知识学习(三)Zookeeper的配置
前面两篇文章介绍了Zookeeper是什么和可以干什么,那么接下来我们就实际的接触一下Zookeeper这个东西,看看具体如何使用,有个大体的感受,后面再描述某些地方的时候也能在大脑中有具体的印象.本 ...
- 客户端相关知识学习(三)之Android原生与H5交互的实现
Android原生与H5交互的实现 H5调用原生的方式 方式可能有多种,根据开发经验,接触过两种方式. 方法一:Android向H5注入全局js对象,也就是H5调Android 1.首先对WebVie ...
- 客户端相关知识学习(二)之h5与原生app交互的原理
前言 现在移动端 web 应用,很多时候都需要与原生 app 进行交互.沟通(运行在 webview中),比如微信的 jssdk,通过 window.wx 对象调用一些原生 app 的功能.所以,这次 ...
- 【OpenStack】network相关知识学习
network 类型 local:通信不跨主机,必须同一网段,主要做单机测试使用: flat:统计可以跨主机,但是需要在同一网段: 每个 flat network 都会独占一个物理网卡 计算节点上 b ...
随机推荐
- 网络基础:NetBIOS
网络基础小补. 利用 NetBIOS 名称与其他计算机通信 网络中的计算机之间必须知道IP地址后才能相互通信.但对人来说IP难以记忆,NetBIOS计算机名称比较容易记忆.当计算机使用 NetBIOS ...
- C#实现网页爬虫
HTTP请求工具类(功能:1.获取网页html:2.下载网络图片:): using System; using System.Collections.Generic; using System.Dra ...
- 基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式
在基于MVC4+EasyUI的Web开发框架里面,大量采用了Jquery的方法,对数据进行请求或者提交,方便页面和服务器后端进行数据的交互处理.本文主要介绍利用Jquery处理数据交互的几种方式,包括 ...
- xpath学习积累
"//comment()":“所有注释节点”
- 对于一些Http远程连接Api安全的看法;
文章来源于 :http://lesg.cn/?p=122 我的个人博客站点 对于一些Http远程连接Api安全的看法: 当不同系统需要互相通信的时候:如果无法用webservice等方式链接的时候另一 ...
- idea IntelliJ IDEA 2016.2破解
从2016-08-06开始全面切到HTTPS访问模式,而IntelliJ IDEA的License server不支持HTTPS访问,所以本博客将IntelliJ IDEA的在线注册码生成页面转到了h ...
- [转]PHP编码规范
注:这是10年前的一篇PHP编码规范,最早发布于清华水木BBS,现在好像都找不到完整的版本了,但至今看起来仍是非常有参考意义.个人会根据经验做一些调整.文中对于命名一段的描述极大的曾启发了个人的编程体 ...
- 详解 Spring 3.0 基于 Annotation 的依赖注入实现(转)
使用 @Repository.@Service.@Controller 和 @Component 将类标识为 Bean Spring 自 2.0 版本开始,陆续引入了一些注解用于简化 Spring 的 ...
- python signal信号
作用:发送和接收异步系统信号 信号是一个操作系统特性,它提供了一个途径可以通知程序发生了一个事件并异步处理这个事件.信号可以由系统本身生成,也可以从一个进程发送到另一个进程. 由于信号会中断程序的正常 ...
- 低信噪比的HTML5优化
百度搜索引擎建议是我们的HTML文件最好不要超过128KB,其实现在对于那些大文件搜索引擎也是很容易就抓取到的,只不过我们是尽量在可能的情况下把我们的网页代码越精简越好,我们要知道搜索引擎抓取网页的时 ...