搞事情,自定義 LayoutInflater 實現酷炫引導頁
熱文導讀| 點選標題閱讀
來源:https://www.jianshu.com/p/b400b3547bee
今天,我們來搞點事情,自定義一個LayoutInflate,搞點有意思的東西,實現一個酷炫的動畫。首先,在自定義LayoutInflate之前,我們要先分析一下LayoutInflate的原始碼,瞭解了原始碼的實現方式,才能定製嘛~~~~
好了,怕你們無聊跑了,先放效果圖出來鎮貼
好了,效果看完了,那就先從LayoutInflate的原始碼開始吧。
LayoutInflate
先看看官方文件吧〜我英語不好,就不幫大家一句一句翻譯了,反正大家也都知道這個類是幹嘛的。
LayoutInflater
還是提取一下關鍵資訊吧.
1.LayoutInflate可以將xml檔案解析成檢視物件。獲取方式有兩種getLayoutInflater()和getSystemService(Class)。
2.如果要建立一個新的LayoutInflate去解析你自己的xml,可以使用cloneInContext,然後呼叫setFactor()。
好了,我們先來回顧一下平時我們是怎麼把xml轉換成檢視的吧。
的setContentView()
我們給活動設定佈局xml都是呼叫這個方法,現在我們就來看看這個方法到底幹了什麼事。
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);
}
View.inflate()
同樣是呼叫了LayoutInflate.inflate()方法
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
LayoutInflate.from(上下文).inflate()
同上
我們專案中所有的Xml轉檢視都離不開這三個方法吧,這三個方法最終呼叫的都還是LayoutInflate的膨脹方法。
我們再來看看怎麼獲取到LayoutInflate的例項。
上面三個xml解析成檢視的方法都是用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等等各種服務是不是都這樣獲取的。而我們都知道這些系統服務都是單例的,並且在應用啟動的時候系統為其初始化的。好了,撤遠了~~
回過頭來,我們繼續看LayoutInflate原始碼。
inflate(@LayoutRes int資源,@ Nullable ViewGroup root)
這個方法就是將xml檔案轉換成檢視的方法,我們專案中所有的xml解析呼叫的都是這個方法。第一個引數是xml資源id,第二個方法是解析後的檢視是否要新增到根視裡面去。
通過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,並且還返回檢視
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節點的檢視,並且處理了是否新增解析的佈局到rootView。呼叫rInflateChildren方法去解析子檢視並且新增到頂級節點臨裡面。最後返回解析結果。
我們先來看看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來建立檢視,如果mFactor為null或者mFactor建立是失敗,則最終呼叫LayoutInflate的createView方法來建立View的,它傳入了view的parent,name ,背景,attrs。
接下來繼續去看子檢視解析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的靜態方法形式獲取LayoutInflater實力
2.inflate解析xml資源
3.inflate呼叫createViewFromTag建立了頂級檢視
4.inflate呼叫rInflateChildren建立所有子檢視
5.rInflateChildren遞迴呼叫rInflate建立所有子觀
.6.rInflate通過呼叫createViewFromTag真正建立一個檢視
。7.createViewFromTag優先使用mFactory2,mFactory,mPrivateFactory來建立檢視,如果建立失敗,則最終呼叫createView方法來建立。建立的過程中用了parent,name ,context,attrs等引數,然後運用反射的方法,建立出View,
因此,我們所有的View的構造方法都是被LayoutInflate的工廠呼叫建立出來的。
如果要自定義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,設定不同的滑動速度滑動方向,可能你會瘋掉。
因此,我們需要想一個辦法,去解決這個問題。可能有的童鞋會說,我寫一個自定義檢視,設定滑動速度係數屬性就行了呀。這個方法可以實現,但是,你還是需要一個一個去findViewbyid 。
那麼,我們是不是可以給xml新增自定義解析。比如說,天上的雲,滑進來的阻尼係數是0.4,滑出去的阻尼係數是0.6,只需要在xml裡面設定好這兩個引數,然後我們再在合適的時使用這兩個引數即可啊。
自定義LayoutInflater.Factory
In,怎麼變成自定義LayoutInflater.Factory了,哈哈哈,還記得剛剛LayoutInflater的原始碼分析麼,檢視的建立全部在createViewFromTag裡面,而createViewFromTag優先使用Factory來建立。然後我們來看看Factory到底是幹嘛的。
您可以提供從LayoutInflater進行充氣時呼叫的掛鉤。
您可以使用它來自定義XML佈局檔案中可用的標記名稱。
當LayoutInflater在解析佈局的時候會被呼叫
可以用來讀取xml中的自定義標籤。
這個迷迷都解開了吧,啊哈哈哈哈~~
現在,我們就來定義這個工廠
思路很簡單
.1
。實現抽象方法onCreateView
3.在onCreateView裡面使用LayoutInflate的createView方法建立檢視
4.建立成功之後,讀取檢視的attrs屬性,作為標籤保持到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;
}
建立檢視的方法,這裡注意一下,xml標籤裡面系統的檢視只有類名,自定義檢視是全路徑。如:,<com.diamond。* .CustomView ... />而可以省略路徑的檢視又分為“android.widget。”和“android.view。”包下,所以對於只寫縮寫的檢視,需要遍歷這兩個路徑。</ com。diamond>
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設定標籤並儲存起來。
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已經結束了,所以,我們可以直接呼叫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的翻譯Y / TranslationX操作。
這裡我貼一下程式碼倉庫地址吧,有興趣的小夥伴可以把程式碼跑起來看一下
github傳送門:
https ://github.com/diamondlin2016/Parallaxlayoutinflater
看起來好像並沒有什麼卵用,就是秀了一波騷操作。寫一個自定義檢視,繼承ImageView,設定幾個自定義attrs屬性,再在構造方法裡面把屬性讀出來儲存到類變數,對外提供讀取方法,然後同樣監聽viewpager的滑動就行了。
哈哈哈哈~~分享這篇文章的最終目的不是為了實現這個動畫,就是想看一下LayoutInflate的原始碼,瞭解一下xml檔案是怎麼解析成觀的過程....
看完本文有收穫?請分享給更多人
喜歡就點「好看」唄~
相關文章
- 搞事情,自定義 LayoutInflate 實現酷炫引導頁
- 自定義View:實現炫酷的點贊特效(仿即刻)View特效
- 高仿京東到家APP引導頁炫酷動畫效果APP動畫
- LiulishuoPreview 手摸手帶你用 VideoView 實現英語流利說炫酷引導頁ViewIDE
- android 自定義酷炫進度條動畫Android動畫
- Android自定義View之實現簡單炫酷的球體進度球AndroidView
- 自定義一個酷炫的提交完成按鈕
- CSS3炫酷的發光文字 可自定義文字色彩CSSS3
- c++實現彩色炫酷(?)畫面C++
- CoordinatorLayout實現酷炫摺疊效果
- DataTables自定義分頁條數實現
- flutter Lottie 動畫引導頁的實現Flutter動畫
- Flutter 實現酷炫的3D效果Flutter3D
- 如何實現炫酷的數字大屏
- 使用CSS background實現炫酷懸停效果CSS
- Android——Activity切換炫酷動畫實現Android動畫
- 自定義 View 梳理:用貝塞爾曲線繪製酷炫輪廓背景View
- 使用three.js實現炫酷的酸性風格3D頁面JS3D
- 使用Webview實現app啟動引導頁WebViewAPP
- Android引導頁實現(帶動點)Android
- Flutter自定義控制元件第一式,炫酷“蛛網”控制元件Flutter控制元件
- InteractiveGraph 實現酷炫關係圖譜之前瞻
- C# Winform實現炫酷的透明動畫介面C#ORM動畫
- Spring Cloud自定義引導屬性源SpringCloud
- 自定義view 之多個引導層動畫效果View動畫
- 自定義View:自定義屬性(自定義按鈕實現)View
- [譯] CSS 變數實現炫酷滑鼠懸浮效果CSS變數
- React 實現炫酷的可拖拽網格佈局React
- 利用CSS變數實現炫酷的懸浮效果CSS變數
- jquery實現在滑鼠點選處的炫酷效果jQuery
- three.js實現炫酷的3d影院JS3D
- .net自定義錯誤頁面實現升級篇
- Android 自定義控制元件玩轉字型變色 打造炫酷ViewPager指示器Android控制元件Viewpager
- 神奇的Python,一行程式碼能做哪些炫酷的事情?Python行程
- 搞事情之 Vapor 初探Vapor
- canvas實現炫酷的黑客帝國數字雨特效Canvas黑客特效
- Qt實現炫酷啟動圖-動態進度條QT
- 【原始碼分析】Lottie 實現炫酷動畫背後的原理原始碼動畫