代碼基于4.1.2
導讀:
View的重要性不言而喻,想起有時動态添加一個View時,不知為什麼寬高總是不能随心所欲,是不是感覺虛了不少?是以把View的原理了然于胸之後就不用虛了。我會盡量在每行代碼上加注釋,并在要講解的方法源碼上面對其參數進行說明,方法下面進行總結,希望這樣可以友善閱讀和了解。如有不對的地方,還請大家多多指正。
要開始研究View就不得不從建立View來開始,那麼請聽我說,從前……
建立出一個View執行個體的步驟如下
LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = layoutInflater.inflate(R.layout.main,null);//第二個參數為此View的父控件,下面會有詳解
//或者
LayoutInflater layoutInflater = LayoutInflater.from(context);
View view = layoutInflater.inflate(R.layout.main,null);//第二個參數為此View的父控件,下面會有詳解
兩種原理都是一樣,請根據個人喜好任選其一。
我們先做一個小Demo讓一個簡單的View加載到頁面裡,然後在一步一步的分析。
Activity代碼
public class ViewDemoActivity extends Activity {
private LinearLayout linearLayout;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.viewdemo);
//建立一個View
LayoutInflater layoutInflater = LayoutInflater.from(this);
View view = layoutInflater.inflate(R.layout.button,null);
//添加到Activity的layout中,這部分将在下一章節裡講解
linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId);
linearLayout.addView(view);
}
}
Activity中layout布局,隻是添加了一個id
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewdemoId"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
在layout檔案夾中添加xml檔案(例如m_button.xml),隻是設定一個button
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dip"
android:layout_height="wrap_content"
android:text="Button">
</Button>
效果圖,上面button明明設定高是300dip 為什麼顯示出來的是系統預設值呢?下面有詳解
使用者調用的inflater()都會被跳轉到下面這個inflater()方法,除非直接調用這種inflater(),有三個參數:
● resource(int),布局檔案資源。
● root(ViewGroup),所建立View的父布局,設定為null的話,就是告訴系統此View不知道放在哪個父控件上一切都聽系統的安排,由于沒有老大罩着是以我們上面設定android:layout_height的權利會被剝奪掉,在建立的時候會被系統重新生成一個預設值,是以如果想自己繪制寬高的話需把父控件添加上。
● attachToRoot(boolean),是否要把建立出來的View載入到上一個參數(父布局)中,如果設定為false了就算指定父布局也不會引用,相當于開關。
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
//把布局檔案加載到XmlResourceParser解析器中
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
//下面有詳解
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
通過上面的講解我們知道了,建立View如果想讓自己設定的寬高有效就要給他指定一個父布局,并attachToRoot參數設定為true或者不設定。那是不是這樣就可以了呢,我們來做個Demo測試一下。
其他代碼不變,隻在Activity中為建立的View指定父布局
public class ViewDemoActivity extends Activity {
private LinearLayout linearLayout;
private View view;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.viewdemo);
linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId);
LayoutInflater layoutInflater = LayoutInflater.from(this);
view = layoutInflater.inflate(R.layout.button,linearLayout);
linearLayout.addView(view);
}
}
3、2、1 運作…
哎呀?崩潰了… 我們看看什麼錯
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
意思是說它已經有一個父布局了,在調用addView()時重複添加父布局,怎麼會重複添加父布局呢?好吧,隻好繼續往裡面看看源代碼是怎麼寫的了。
我們知道上面那個inflate(),隻是将布局檔案加載到解析器中然後把解析器傳給下面這個最終版inflate()。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
//這裡裝載的就是此View運作所在對象的Context
mConstructorArgs[0] = mContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//擷取目前節點的标簽
final String name = parser.getName();
//判斷目前标簽是否為"merge",如果是"merge",直接調用rInflate()來生成View
if (TAG_MERGE.equals(name)) {
//merge建立出來的View是需要宿主的,必須是在ViewGroup下面且attachToRoot參數為true。
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//生成View,下面有詳解 ↓
rInflate(parser, root, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 建立一個目前xml中的根View
View temp;
//判斷目前标簽是不是"blink"
if (TAG_1995.equals(name)) {
//如果是"blink"則生成一個有閃爍效果的View,此View每隔0.5s會invalidate一次 ↓
temp = new BlinkLayout(mContext, attrs);
} else {
//如果不是"blink" ↓
temp = createViewFromTag(root, name, attrs);
}
ViewGroup.LayoutParams params = null;
//如果建立的View有父布局
if (root != null) {
// Create layout params that match root, if supplied
// 得到父布局的layoutParams
params = root.generateLayoutParams(attrs);
//如果inflate第三個參數為false則temp用父布局的layoutParams
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp
// 建立temp下面所有的子view
rInflate(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
// 如果有父布局且第三個參數不手動設定為false,無需手動調用addView(),父布局自動添加所要添加的View,最後傳回的是父布局
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
// 如果沒有父布局或者有父布局但是第三個參數手動設定為false則傳回所建立的View,系統不會自動調用addView()
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;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
return result;
}
}
此方法有2個分支
一、在23行中判斷根節點是不是merge,如果是則在第30行直接處理目前節點的子節點,目的是為了減少UI層級(不熟悉merge屬性的請google),如果不是則進入第二個分支。
二、在36行中判斷目前标簽是不是blink,這個對了解影響不大下面再講,45-55行是配置LayoutParams,然後是59行建立View。接下來重點來了,第64行判斷如果有父布局且attachToRoot參數為true那麼系統會自動添加到父布局中,這…下知道了為什麼我在建立完View之後再調用addView()方法後出報出重複添加的錯誤了。OK,那我把addView()去掉試試。
代碼如下
public class ViewDemoActivity extends Activity {
private LinearLayout linearLayout;
private View view;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.viewdemo);
linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId);
LayoutInflater layoutInflater = LayoutInflater.from(this);
view = layoutInflater.inflate(R.layout.button,linearLayout);
// linearLayout.addView(view);
}
}
4、3、2、1 運作…
終于成功了。。
由此可知,動态添加View如果想控制其寬高的話就要指定其父布局,且不要自己調用addView()哦。
接下來我們先來看上面用到的BlinkLayout類和createViewFromTag方法,因為在接下來的rInflate()方法中主要用到的就是這他們倆。
首先說BlinkLayout這個類很有意思,它可以使你的控件不斷閃爍。我們來做一個例子
我們隻需在xml檔案中操作即可
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewdemoId"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:textSize="25sp"
android:layout_height="wrap_content"
android:text="Hello Marco"/>
<blink android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
android:textSize="25sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Marco"/>
</blink>
</LinearLayout>
效果圖(字型是我在java裡面手動改變的)
我們一般用不到blink,其源碼如下。
private static class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;
private boolean mBlink;
//限制dispatchDraw()隻繪制一次元件
private boolean mBlinkState;
private final Handler mHandler;
public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
mBlinkState = !mBlinkState;
makeBlink();
}
//請求重新繪制本視圖
invalidate();
return true;
}
return false;
}
});
}
//重新繪制的入口方法
private void makeBlink() {
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}
//預設在第一次onDraw之前調用
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mBlink = true;
mBlinkState = true;
makeBlink();
}
//在銷毀View時調用
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mBlink = false;
mBlinkState = true;
mHandler.removeMessages(MESSAGE_BLINK);
}
//繪制容器元件時會調用 在onDraw之後
@Override
protected void dispatchDraw(Canvas canvas) {
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}
}
下面我們來講createViewFromTag()方法
View createViewFromTag(View parent, String name, AttributeSet attrs) {
//如果标簽為view
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
View view;
//如果有預先定義Factory則會初始化成我們預先設定好的view
if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
else view = null;
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}
if (view == null) {
if (-1 == name.indexOf('.')) {
//如果标簽中沒有"."這個符号則說明是系統的view
view = onCreateView(parent, name, attrs);
} else {
//有"."則說明是自定義元件
view = createView(name, null, attrs);
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
}
}
這個方法主要就是用到了兩個方法onCreateView()、createView().
1.判斷是否有Factory,如果有則傳回加載的Factory。
2.如果沒有Factory,則判斷要建立的View是系統自帶的還是我們自己建立的。
3.如果是系統自帶的view則會調用onCreateView(),然後自動在字首上添加”android.view.”例如”android.view.ImageView”。
4.如果是自定義元件則會調用createView(),然後在字首上添加例如”com.example.mImageView”。
下面是onCreateView()最終調用方法的源代碼,非常簡潔
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
接下來是createView()方法的源代碼,這個是真正建立View的方法
public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException {
//緩存構造對象,指向的是一個HashMap,其中緩存着使用過的UI控件
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//當緩存構造對象為空時,通過類名來建立一個類
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//mFilter 是一個 Filter對象,為一個過濾器
if (mFilter != null && clazz != null) {
//來通過 mFilter 是否同意建立由class對象所描述的UI控件
boolean allowed = mFilter.onLoadClass(clazz);
//如果 mFilter 不同意建立此UI控件則抛出異常
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
//建立UI元件的構造對象
constructor = clazz.getConstructor(mConstructorSignature);
//添加入HashMap中,供可重複利用
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
//檢測mFilterMap中是否有對應的映射,沒有就報錯
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
//建立view兩要素,一個是Context,另一個是AttributeSet缺一不可。
Object[] args = mConstructorArgs;
args[1] = attrs;
//反射出的view
final View view = constructor.newInstance(args);
//判斷映射出來的view是否為ViewStub的執行個體,ViewStub就是可以不消耗資源就可以隐藏UI的元件
if (view instanceof ViewStub) {
// always use ourselves when inflating ViewStub later
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(this);
}
//啊
return view;
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
}
}
此方法大緻步驟如下
1.擷取對應的緩存對象(緩存的目的就是防止例如多處用到TextView而建立多次TextView這種浪費資源的事。)
2.沒有就建立
3.有的話看是否是ViewStub的執行個體,是的話設定其LayoutInflater.
4.傳回View
建立View的大體架構我們已經分析完成了,具體如何從建立View的細節将在後面章節解析。
接下來我們來看今天的最後一個方法rInflate()
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
//周遊parent每一行
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
//找到START_TAG節點後開始解析
if (type != XmlPullParser.START_TAG) {
continue;
}
//擷取節點名稱
final String name = parser.getName();
//接下來就是根據各個節點來建立對應的view了
if (TAG_REQUEST_FOCUS.equals(name)) {
//TAG_REQUEST_FOCUS節點
parseRequestFocus(parser, parent);
} else if (TAG_INCLUDE.equals(name)) {
//TAG_INCLUDE節點
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 if (TAG_1995.equals(name)) {
//上面有講過
final View view = new BlinkLayout(mContext, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
} else {
//自定義節點
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
//目前View建立完成之後會通知其父類。
if (finishInflate) parent.onFinishInflate();
}
如果上面的内容了解的差不多了,那麼再來看這個方法就好受多了,此方法的思路其實就是利用遞歸來将其所有的view建立出來。
總結:
這一子產品最重要的一點就是,在動态添加View時,如果想設定寬高的話一定要指定它的父View,并且不要寫addView()方法,因為指定父View的話,源碼會自動調用addView()方法的;