今天,咱們來搞點事情,自定義一個 LayoutInflate,搞點有意思的東西,實現一個酷炫的動畫。
首先,在自定義 LayoutInflate 以前,咱們要先分析一下 LayoutInflate 的源碼,瞭解了源碼的實現方式,才能定製嘛~~~~
好了,怕大家無聊跑了,先放效果圖出來鎮貼html
好了,效果看完了,node
那就先從LayoutInflate的源碼開始吧。android
##LayoutInflate
先看看官方文檔吧~我英語很差,就不幫你們一句一句翻譯了,反正你們也都知道這個類是幹嗎的。git
仍是提取一下關鍵信息吧。
1.LayoutInflate 能夠將 xml 文件解析成 View 對象。獲取方式有兩種getLayoutInflater()和getSystemService(Class)。程序員
2.若是要建立一個新的 LayoutInflate去解析你本身的 xml,可使用 cloneInContext,而後調用 setFactor()。github
好了,咱們先來回顧一下平時咱們是怎麼把 xml 轉換成 View 的吧。api
咱們給 Activity 設置 佈局 xml 都是調用這個方法,如今咱們就來看看這個方法到底幹了什麼事。bash
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
-----以上是 Activity 的方法,調用了 Window 的 steContentView
----手機上的 window 都是 PhoneWindow,就不饒彎了,直接看 PhoneWindow
----的setContentView方法。
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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
----在構造方法裏面找到了mLayoutInflater 的賦值
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}複製代碼
一樣是調用了LayoutInflate.inflate()方法app
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}複製代碼
咱們項目中全部的 Xml 轉 View 都離不開這三個方法吧,這三個方法最終調用的都仍是 LayoutInflate 的 inflate 方法。ide
咱們再來看看怎麼獲取到 LayoutInflate 的實例。
上面三個xml 解析成 view 的方法都是用LayoutInflate.from(context)來獲取 LayoutInflate 實例的。
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}複製代碼
看到這個代碼有木有以爲很眼熟啊,咱們的 ActivityService、WindowService、NotificationService等等各類 Service 是否是都這樣獲取的。而咱們都知道這些系統服務都是單例的,而且在應用啓動的時候系統爲其初始化的。好了,撤遠了~~
回過頭來,咱們繼續看 LayoutInflate 源碼。
經過 Resources 獲取 xml 解析器XmlResourceParser。
public View inflate(@LayoutRes int resource, @Nullable 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();
}
}複製代碼
XmlResourceParser解析 xml,而且返回 view
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//寫入跟蹤信息,用於 Debug 相關,先不關心這個
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
//用於讀取 xml 節點
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
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!");
}
//獲取類名,好比說 TextView
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//若是標籤是merge
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
//merge做爲頂級節點的時候必須添加的 rootview
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//遞歸方法去掉沒必要要的節點,爲何 merge 能夠優化佈局
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp 是根節點
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//若是不添加到 rootView 切 rootView 不等於空,則生成 LayoutParams
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// 解析子節點
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// 若是要添加到 rootview。。
// to root. Do that now.
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.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (Exception 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); //返回解析結果 return result; } }複製代碼
在這個方法中,判斷了是否使用 merge 優化佈局,而後經過createViewFromTag解析的頂級 xml 節點的 view,而且處理了是否添加解析的佈局到 rootView。調用rInflateChildren方法去解析子 View 而且添加到頂級節點 temp 裏面。最後返回解析結果。
咱們先來看看 createViewFromTag
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//獲取命名空間
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 給 view 設置主題。如今知道爲何colorPrimary等 theme 屬性會影響控件顏色了吧
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
//讓 view 閃爍,能夠參考http://blog.csdn.net/qq_22644219/article/details/69367150
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
優先調用了mFactory2的 oncreateView 方法,建立了 temp View
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
}
}複製代碼
這裏咱們能夠知道,mFactor或者 mFactor 不爲 null,則調用mFactor來建立 View,若是mFactor爲 null 或者mFactor建立是失敗,則最終調用LayoutInflate 的createView方法 來建立 View 的,它傳入了 view 的 parent、name、context、 attrs。
接下來繼續去看子 View 的解析rInflateChildren
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//獲取佈局層級
final int depth = parser.getDepth();
int type;
//沒看懂沒事,咱們不是來糾結 xml 解析的
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();
//requestFocus標籤,http://blog.csdn.net/ouyang_peng/article/details/46957281
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
//tag標籤,只能用於 api21以上,給父view 設置一個 tag
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//include 節點
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//merge 節點
throw new InflateException("<merge /> must be the root element");
} else {
//走了剛剛的那個方法,建立 view 設置 LayoutParams
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
//添加到付 view
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}複製代碼
咱們來整理一下思路吧,調用步驟
1.LayoutInflater 的靜態方法 form 獲取LayoutInflater實力
2.inflate解析 xml 資源
3.inflate 調用createViewFromTag建立了頂級view
4.inflate 調用rInflateChildren 建立全部子 view
5.rInflateChildren遞歸調用rInflate建立全部子 view。
6.rInflate經過調用createViewFromTag真正建立一個 view。
7.createViewFromTag優先使用 mFactory二、mFactory、mPrivateFactory來建立 View,若是建立失敗,則最終調用createView方法來建立。建立的過程當中用了parent,name,context,attrs等參數,而後運用反射的方法,建立出 View,
所以,咱們全部的 View 的構造方法都是被 LayoutInflate 的Factory調用建立出來的。
若是要自定義 LayoutInflate 解析,只須要給調用LayoutInflate的 setFactory設置咱們自定義的 Factory 便可。
可是問題來了,LayoutInflate是系統服務,並且是單例,咱們直接調用LayoutInflate的 setFactory 方法,會影響後期全部 view 的建立。
因此咱們須要用到LayoutInflate的cloneInContext方法clone一個新的 LayoutInflate,而後再設置本身的 Factory。至於LayoutInflate是一個抽象類,cloneInContext是一個抽象方法,咱們根本不用關心,由於咱們直接用系統建立好的LayoutInflate便可。
好了,LayoutInflate的源碼分析完了,接下來咱們來分析動畫了。
##動畫分析
源碼看了好久,咱們再來從新看一遍動畫吧
1.翻頁
2.翻頁的時候天上的雲,地上的建築物移動速度和翻頁速度不同
3.不一樣的背景物移動速度不同,最後一頁背景物上下擴散
4.翻頁的過程當中,人一直在走路
5.最後一頁人要消失。
解決方案:
1.ViewPager
2.給 viewPage設置PageChangeListener,在滾動的時候給各類 背景物體設置setTranslation。
3.不一樣的背景物設置不一樣的setTranslation係數。
4.人物走路用幀動畫便可,在viewPage滑動處於SCROLL_STATE_DRAGGING狀態的時候開啓幀動畫。
5.這個簡單,監聽onPageSelected,而後再設置人爲 View.GONE便可。
解決方案的問題:
粗略數了一下,6個頁面大概有50個左右的背景物。若是要一個一個去獲取 id,而後再根據不一樣的 id,設置不一樣的滑動速度滑動方向,可能你會瘋掉。
所以,咱們須要想一個辦法,去解決這個問題。可能有的童鞋會說,我寫一個自定義 View,設置滑動速度係數屬性就好了呀。這個方法能夠實現,but,你仍是須要一個一個去 findViewbyid。
那麼,咱們是否是能夠給 xml 添加自定義標籤,而後自定義解析。好比說,天上的雲,滑進來的阻尼係數是0.4,滑出去的阻尼係數是0.6,只須要在 xml 裏面設置好這兩個參數,而後咱們再在合適的時使用這兩個參數便可啊。
##自定義LayoutInflater.Factory
咦,怎麼變成自定義LayoutInflater.Factory了,哈哈哈,還記得剛剛LayoutInflater的源碼分析麼,View 的建立所有在createViewFromTag裏面,而createViewFromTag優先使用 Factory 來 建立。而後咱們來看看Factory究竟是幹嗎的。
Hook you can supply that is called when inflating from a LayoutInflater.
You can use this to customize the tag names available in your XML layout files.
這下迷惑都解開了吧,啊哈哈哈哈~~
如今,咱們就來定義這個 Factory
思路很簡單。
1.繼承LayoutInflater.Factory2
2.實現抽象方法onCreateView
3.在onCreateView裏面使用 LayoutInflate 的 createView方法建立View
4.建立成功以後,讀取 view 的 attrs 屬性,做爲 tag 保持到 viewTag。
關鍵代碼以下:
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
//建立一個 View
View view = createViewOrFailQuietly(name, context, attrs);
//實例化完成
if (view != null) {
//獲取自定義屬性,經過標籤關聯到視圖上
setViewTag(view, context, attrs);
//全部帶有自定義屬性的 View 保存起來,供動畫切換的時候調用
mParallaxView.getParallaxViews().add(view);
}
return view;
}複製代碼
建立 view 的方法,這裏注意一下,xml 標籤裏面系統的 view只有類名,自定義 view 是全路徑。如:
private View createViewOrFailQuietly(String name, Context context,
AttributeSet attrs) {
//1.自定義控件標籤名稱帶點,因此建立時不須要前綴
if (name.contains(".")) {
createViewOrFailQuietly(name, null, context, attrs);
}
//2.系統視圖須要加上前綴
for (String prefix : sClassPrefix) {
View view = createViewOrFailQuietly(name, prefix, context, attrs);
if (view != null) {
return view;
}
}
return null;
}
private View createViewOrFailQuietly(String name, String prefix, Context context,
AttributeSet attrs) {
try {
//經過系統的inflater建立視圖,讀取系統的屬性
return inflater.createView(name, prefix, attrs);
} catch (Exception e) {
return null;
}
}複製代碼
讀取 attrs 裏面的屬性,給含有特色 attrs 屬性的 view設置 tag 並保存起來。
private void setViewTag(View view, Context context, AttributeSet attrs) {
//全部自定義的屬性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimationView);
if (a != null && a.length() > 0) {
//獲取自定義屬性的值
ParallaxViewTag tag = new ParallaxViewTag();
tag.xIn = a.getFloat(R.styleable.AnimationView_x_in, 0f);
tag.xOut = a.getFloat(R.styleable.AnimationView_x_out, 0f);
tag.yIn = a.getFloat(R.styleable.AnimationView_y_in, 0f);
tag.yOut = a.getFloat(R.styleable.AnimationView_y_in, 0f);
//index
view.setTag(view.getId(), tag);
a.recycle();
}
}複製代碼
好了,咱們自定義LayoutInflater.Factory已經結束了,so,咱們能夠直接調用 LayoutInflate.cloneInContext(context)獲取一個新的 LayoutInflate,而後再setFactor(customFactor)就能夠了。代碼以下:
@Override
public View onCreateView(LayoutInflater original, ViewGroup container,
Bundle savedInstanceState) {
Bundle args = getArguments();
int layoutId = args.getInt("layoutId");
LayoutInflater layoutInflater = original.cloneInContext(getActivity());
layoutInflater.setFactory(new ParallaxFactory(layoutInflater, this));
return layoutInflater.inflate(layoutId, null);
}複製代碼
接下來的代碼就不寫了吧,就是監聽 ViewPager 的滑動事件,獲取當前滑出滑進頁面的自定義了 attrs 屬性的 View 列表,而後再根據滑出屏幕的比例*屬性參數作 view 的 TranslationY/TranslationX 操做。
這裏我貼一下代碼倉庫地址吧,有興趣的小夥伴能夠把代碼跑起來看一下
看起來好像並無什麼卵用,就是秀了一波騷操做。寫一個自定義 view,繼承 ImageView,設置幾個自定義 attrs 屬性,再在構造方法裏面把屬性讀出來保存到類變量,對外提供讀取方法,而後一樣監聽 viewpager 的滑動就好了。
哈哈哈哈~~分享這篇文章的最終目的不是爲了實現這個動畫,就是想看一下 LayoutInflate 的源碼,瞭解一下 xml 文件是怎麼解析成 view的過程。。。。
##已知 bug:
版本升級引發的 bug,有時間我去找找這兩個 bug 的緣由,找到以後我會在這裏更新。
本次效果來源於動腦學院視頻課程,很不錯的一套課程,感興趣的小夥伴能夠去學學,適用於有必定基礎的 android 程序員,進階高級頗有效果哦。騰訊課堂有免費的公開課,或者去動腦學院學習,官網的課程好像是收費的,固然費用對程序員來講不算高,付不起課程費用的大學生能夠去騰訊課堂學習,或者某寶。。。。。。。。