Android 中LayoutInflater(佈局載入器)之實戰篇
前言
如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。
導航
效果
可以看出在滑動時,會出現視覺差效果。
可以看出在滑動時,物品會飄出去。
概述
(1)主要目的是通過這個Demo,理解自定義LayoutInflater.Factory的過程。
(2)理解小紅書的第一版引導頁是如何製作出來的。
分析
這個效果屬於視覺差的效果,原理是根據ViewPager的滑動方向,頁面內物理做同向偏移,只要偏移距離大於頁面的偏移,就會產生速度差,那麼就會實現該效果。
實現速度差,我們需要一個滑動的比例係數:
在頁面進入時:
頁面物體的移動距離 = (頁面長度 - 滑動距離) * 滑動係數
在頁面滑出時:
頁面物體的移動距離 = (0 - 滑動距離 ) * 滑動係數
同時考慮第二張Gif上,發現物體Y軸也存在移動,所以也得需要考慮Y軸方向的滑動,整理下:
//進入時:
view.setTranslateX((vpWidth - positionOffsetPixels) * xIn);
view.setTranslateY((vpWidth - positionOffsetPixels) * yIn);
//退出時
view.setTranslateX((0 - positionOffsetPixels) * xOut);
view.setTranslateY((0 - positionOffsetPixels) * yOut);
這樣就可以實現出:
(1)進入該介面時,介面上的物品快速飛進來。
(2)退出該介面時,介面上的物理快速飛出去。
實現思路
對於上述的分析,這裡的實現思路存在兩種:
自定義View,自定義xIn、yIn、xOut、yOut四個屬性的係數,所有介面上的物體繼承這個自定義View。
自定義LayoutInflater.Factory在解析時,將這些自定義屬性提取,以Tag方式儲存起來。
優缺點分析
自定義View:
優點:可以對物體做更多層面的擴充套件,這個自定義LayoutInflater.Factory是不具備的。
缺點:由於介面的物體數量過多,在findViewById時需要處理的View元素過多,極大的增加程式碼量。
自定義LayoutInflater.Factory :
優點:可以在解析過程中對View做統一操作,當出現大量的View時,能夠縮減大量程式碼。
缺點:在解析時預處理View,但是就不能動態的改變View的屬性,要對View進行擴充套件性操作,自定義LayoutInflater.Factory不具備這樣的功能。
自定義LayoutInflater.Factory
上述的兩種方案的優缺點已經分析完畢,但是本文作為實戰篇,所以只會介紹自定義LayoutInflater.Factory這種方式。
在實際場景中,需要結合自身情況,以及上述的優缺點,進行合理選擇。
在介紹之前,先看一段程式碼:
View view;
//如果Factory2存在,就會呼叫其onCreateView方法
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
//如果Factory存在,就會呼叫其onCreateView方法,和Factory2不同的時,這裡的引數沒有父View
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
//如果沒有Factory或者Factory2,就會尋找mPrivateFactory(本質上也是Factory2)
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
這段程式碼出自LayoutInflater中createViewFromTag()方法,作用是根據View的名稱(name引數)來建立View,這裡在原始碼篇已經詳細分析過,如果沒有看過,可以點選這裡。
在這裡就簡單描述下,這個方法的主要流程:
對一些特殊標籤,做分別處理,例如:view,TAG_1995(blink)
進行對Factory、Factory2的設定判斷,如果設定那麼就會通過設定Factory、Factory2進行生成View
如果沒有設定Factory或Factory2,那麼就會使用LayoutInflater預設的生成方式,進行View的生成
在實戰篇中,只有第二部分和我們今天的內容是相關的,我們在看一遍第二條。
進行對Factory、Factory2的設定判斷,如果設定那麼就會通過設定Factory、Factory2進行生成View
如果設定了Factory或者Factory2,那麼就不會使用LayoutInflater預設的生成方式,那麼生成View的過程,就由我們自主把控,這才是我們自定義LayoutInflater.Factory的主要原因。
自定義Factory還是Factory2 ?
View view;
//如果Factory2存在,就會呼叫其onCreateView方法
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
//如果Factory存在,就會呼叫其onCreateView方法,和Factory2不同的時,這裡的引數沒有父View
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
我們能夠從這段程式碼中得出,Factory2比Factory的優先順序要高,即Factory2存在Factory就不可能會被呼叫,同理可以得出結論:
優先順序順序:
mFactory2 > mFactory > mPrivateFactory > LayoutInflater預設處理方式
而且我們還能夠發現mFactory2的onCreateView()方法與mFactory是不相同的:
//mFactory2
mFactory2.onCreateView(parent, name, context, attrs);
//mFactory
view = mFactory.onCreateView(name, context, attrs);
根據上述的分析,我們可以得出結論:
(1)Factory2的呼叫優先順序比Factory要高
(2)Factory2的onCreateView()方法,會比Factory多返回一個父View的引數。
(3)Factory2和Factory是互斥的,(如果不通過反射的話)只能設定一個。
第三條在CreateViewFromTag的那篇文章已經分析過了,這裡不做過多的解釋了。
實際選擇的過程中,一般會選擇自定義Factory2,因為Factory2本身也繼承了Factory介面,而且Factory2的優先順序比較高。
注意事項
(1)設定Factory但是發現無響應,是因為本身LayoutInflater中存在Factory2
因為一般使用方式,是直接呼叫cloneInContext()方法,我們知道一般的預設解析器都是PhoneLayoutInflater,我們看下其實現方式:
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
本質就是呼叫LayoutInflater的兩參構造方法:
protected LayoutInflater(LayoutInflater original, Context newContext) {
mContext = newContext;
mFactory = original.mFactory;
mFactory2 = original.mFactory2;
mPrivateFactory = original.mPrivateFactory;
setFilter(original.mFilter);
}
在這裡可以看出,cloneInContext會把原LayoutInflater的Factory2和Factory一併複製。
因為Factory比Factory2的優先順序低,所以才會不出現效果。
解決方案 :
(1)自定義LayoutInflater,並且改寫cloneInContext,使其不復制原LayoutInflater的Factory2以及Factory。
public class CustomLayoutInflater extends LayoutInflater {
protected CustomLayoutInflater(Context context) {
super(context);
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new CustomLayoutInflater(newContext);
}
}
(2)使用時,直接通過new出例項,然後setFactory
CustomLayoutInflater newInflater = new CustomLayoutInflater(getActivity());
newInflater.setFactory2(new CustomAppFactory(newInflater, this));
return newInflater.inflate(layoutId, null);
(2)使用AppCompatActivity直接setFactory2或者setFactory為什麼報錯?
這是因為 AppCompatActivity 在初始化的時候,已經設定了 Factory,下面來看下這部分程式碼
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//注意這個方法
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
//.....省略多餘的程式碼..........
}
super.onCreate(savedInstanceState);
}
繼續檢視 installViewFactory()方法
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//這句話是設定 Factory 的方法
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
//省略部分程式碼。。。。。。
}
}
可以發現,在onCreate 時 LayoutInflater 已經設定過一次 Factory 了,然後我再來看下 setFactory() 的原始碼:
public void setFactory(Factory factory) {
if (mFactorySet) {
//原因就是這一句
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = factory;
} else {
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
根據上面程式碼,就可以發現報錯原因了。
解決方案 :
在使用前,先使用 cloneInContext()克隆出一個新的 LayoutInflater,然後在進行設定操作。
LayoutInflate newInflater = LayoutInflater.cloneInContext(inflater,context);
newInflater.setFactory(new CustomFactory());
這樣就避開在原 LayoutInflater 設定 Factory 報錯了。
自定義Factory2的實現 ——> CustomAppFactory
根據上面的展示效果,我們可以判斷出是ViewPager + Fragment的風格,所以我們自定義Factory應該在Fragment的onCreateView中,更改LayoutInflater。
而且根據注意事項,我們一般會自定義優先順序較高的Factory2,防止本身cloneInContext的LayoutInflater中已經存在Factory2,我們使用Factory會無效。
使用方式:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Bundle bundle = getArguments();
int layoutId = bundle.getInt(LAYOUT_ID);
//注意需要呼叫cloneInContext方法生成新的LayoutInflater
LayoutInflater newInflater = inflater.cloneInContext(getActivity());
//呼叫的是setFactory2而非setFactory
newInflater.setFactory2(new CustomAppFactory(newInflater, this));
return newInflater.inflate(layoutId, null);
}
自定義過程
那麼就建立一個類CustomAppFactory來實現Factory2的介面,複寫onCreateView的方法。
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
//<<<<<<<<<<<<<<<<<<<<<<<<<<<第一部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
try {
if (name.contains(".")) {
String checkName = name.substring(name.lastIndexOf("."));
String prefix = name.substring(0, name.lastIndexOf("."));
view = defaultInflater(checkName, prefix, attrs);
}
if (name.equals("View") || name.equals("ViewGroup")) {
view = defaultInflater(name, sClassPrefix[1], attrs);
} else {
view = defaultInflater(name, sClassPrefix[0], attrs);
}
//<<<<<<<<<<<<<<<<<<<<<<<<<<<第二部分>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
//例項化完成
if (view != null) {
//獲取自定義屬性,通過標籤關聯到檢視上
setViewTag(view, context, attrs);
mInflaterView.addView(view);
}
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = onCreateView(name, context, attrs);
return view;
}
其實如果我們採取自定義的方式,這裡只會呼叫onCreateView()四位引數的方法,因為在比較Factory2和Factory的程式碼也介紹過了。
我們實現的邏輯是在onCreateView()三位邏輯裡面,因為需要實現的效果不需要Parent(父View),所以這裡邏輯實現全在三位引數的onCreateView()中。
在這裡我們將onCreateView()中,分成2部分內容:
(1)根據名稱解析出View
(2)擴充套件操作,將額外的屬性,提取出來儲存在Tag中
onCreateView第一部分內容
if (name.contains(".")) {
String checkName = name.substring(name.lastIndexOf("."));
String prefix = name.substring(0, name.lastIndexOf("."));
view = defaultInflater(checkName, prefix, attrs);
}
if (name.equals("View") || name.equals("ViewGroup")) {
view = defaultInflater(name, sClassPrefix[1], attrs);
} else {
view = defaultInflater(name, sClassPrefix[0], attrs);
}
這裡判斷了name中是否包含“.”,是用來判斷生成的View是否是自定義View,下面來看下自定義View和Android自帶的元件的區別:
//原生的元件
RelativeLayout
//自定義View
com.demo.guidepagedemo.customview.CustomImageView
可以發現區別為原生的View不帶字首,而自定義View是包括字首的,所以會用name.contains(“.”)來區分。
而原生元件中View和ViewGroup是屬於android.view包下,其他的例如:RelativeLayout,LinearLayout是屬於android.widget包下。
private final String[] sClassPrefix = {
"android.widget.",
"android.view."
};
所以在之後會對View和ViewGroup作區分,上面把sClassPrefix貼出來了。
而這裡真正的解析過程最後還是交給LayoutInflater,呼叫LayoutInflater的onCreateView方法:
private View defaultInflater(String name, String prefix, AttributeSet attrs) {
View view = null;
try {
view = mInflater.createView(name, prefix, attrs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return view;
}
LayoutInflater的onCreateView方法這裡就不介紹了,在這裡已經分析過了
onCreateView第二部分內容
//例項化完成
if (view != null) {
//獲取自定義屬性,通過標籤關聯到檢視上
setViewTag(view, context, attrs);
mInflaterView.addView(view);
}
在這裡做拓展處理的,setViewTag方法是處理View的自定義屬性,然後將這些屬性包裝成類,給View設定Tag
setViewTag方法
/**
* 將View的屬性資訊儲存在Tag中
*/
private void setViewTag(View view, Context context, AttributeSet attrs) {
//解析自定義的屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomImageView);
if (attrs != null && array.length() > 0) {
AttrTagBean bean = new AttrTagBean();
bean.xIn = array.getFloat(R.styleable.CustomImageView_in_value_x, 0f);
bean.xOut = array.getFloat(R.styleable.CustomImageView_out_value_x, 0f);
bean.yIn = array.getFloat(R.styleable.CustomImageView_in_value_y, 0f);
bean.yOut = array.getFloat(R.styleable.CustomImageView_out_value_y, 0f);
//index
view.setTag(bean);
}
array.recycle();
}
上面對應的是本文我們開始設定的4個係數:
R.styleable.CustomImageView_in_value_x --> 進入時 x方向的係數
R.styleable.CustomImageView_out_value_x --> 退出時 x方向的係數
R.styleable.CustomImageView_in_value_y --> 進入時 y方向的係數
R.styleable.CustomImageView_out_value_y --> 退出時 y方向的係數
而這裡的mInflaterView是一個抽象介面,讓Fragment來實現的,通過在Fragment中內建一個List《View》,到時候可以遍歷統一操作這些View,下面是實現過程:
public interface InflaterViewImpl {
/**
* 獲取View集合
*
* @return
*/
List<View> getViews();
/**
* 新增元素
*/
void addView(View view);
}
Fragment中的實現過程:
public class PageFragment extends Fragment implements InflaterViewImpl {
private List<View> views = new ArrayList<>();
//**************篇幅原因省略了部分方法************************//
@Override
public List<View> getViews() {
return views;
}
@Override
public void addView(View view) {
if (views.contains(view)) {
return;
}
views.add(view);
}
}
處理ViewPager的滑動
這是實戰篇的最後一部分內容,主要介紹的是ViewPager的滑動監聽相關的處理,因為所有效果是基於ViewPager的滑動監聽來顯示的。
因為本文主要介紹內容是自定義LayoutInflater.Factory,所以這裡會簡單敘述下:
mInflaterVp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//獲取ViewPager的寬度
int vpWidth = mInflaterVp.getWidth();
//獲取正在進入的介面
PageFragment inFragment = getPosition(position - 1);
if (inFragment != null) {
List<View> views = inFragment.getViews();
if (views != null && views.size() > 0) {
for (View view : views) {
AttrTagBean tag = (AttrTagBean) view.getTag();
if (tag != null) {
view.setTranslationX((vpWidth - positionOffsetPixels) * tag.xIn);
view.setTranslationY((vpWidth - positionOffsetPixels) * tag.yIn);
}
}
}
}
//當前正在滑動的介面
PageFragment outFragment = getPosition(position);
if (outFragment != null) {
List<View> views = outFragment.getViews();
if (views != null && views.size() > 0) {
for (View view : views) {
AttrTagBean tag = (AttrTagBean) view.getTag();
if (tag != null) {
view.setTranslationX((0 - positionOffsetPixels) * tag.xOut);
view.setTranslationY((0 - positionOffsetPixels) * tag.yOut);
}
}
}
}
}
@Override
public void onPageSelected(int position) {
//當劃到最後一頁時,小人的圖示消失
if (position == fragments.size() - 1) {
mInflaterIv.setVisibility(View.GONE);
} else {
mInflaterIv.setVisibility(View.VISIBLE);
}
}
@Override
public void onPageScrollStateChanged(int state) {
//這裡是處理圖中的小人的幀動畫過程
Drawable anim = mInflaterIv.getBackground();
if (!(anim instanceof AnimationDrawable)) {
return;
}
AnimationDrawable animation = (AnimationDrawable) anim;
Log.d("滑動狀態", state + "");
switch (state) {
//空閒狀態
case ViewPager.SCROLL_STATE_IDLE:
animation.stop();
break;
//拖動狀態
case ViewPager.SCROLL_STATE_DRAGGING:
animation.start();
break;
//慣性滑動狀態
case ViewPager.SCROLL_STATE_SETTLING:
break;
}
}
});
Demo
本文的所有程式碼已上傳到CSDN的資源中心
Demo中包含兩種方式實現本文的效果:
(1)自定義View方式
(2)自定義LayoutInflater.Factory
拓展
其實當天下載的小紅書的App 後,發現引導頁,並不是實戰篇的樣子。
小紅書引導頁
不得不說,這種實現方式,博主感覺挺有靈性的,簡潔而不失觀賞性。
然後博主就高仿了一波,下面是 Demo 地址。
因為這個需求和最近寫的主題無關,就不打算寫部落格了,所以直接放出連結了。
先宣告,這個是有償的,也不多就2元,如果有實際需求的同學,可以下載下支援一下博主。
相關推薦
Android 中LayoutInflater(佈局載入器)之實戰篇
前言 如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。 導航 效果 可以看出在滑動時,會出現視覺差效果。 可以看出在滑動時,物品會飄出去。
Android 中LayoutInflater(佈局載入器)之原始碼篇
前言 如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。 導航 概述 (1)Activity 的 getSystemService的實現過程 (2
Android 中LayoutInflater(佈局載入器)之介紹篇
前言 如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。 本篇作為Android 中LayoutInflater(佈局載入器)系列的介紹篇,該篇內容知識內容比較基礎,建議先看一些概述,如果感覺
Android 中LayoutInflater(佈局載入器)原始碼篇之parseInclude方法
前言 如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。 導航 概述 本篇部落格,是作為Android中LayoutInflater(佈局載入器)原
Android 中LayoutInflater(佈局載入器)原始碼篇之rInflate方法
前言 如果讀者沒有閱讀過該系列部落格,建議先閱讀下博文說明,這樣會對後續的閱讀部落格思路上會有一個清晰的認識。 導航 概述 本篇部落格,是屬於Android 中LayoutInflater(佈局載入器)原始
PyQt5(2)——調整佈局(佈局管理器)第一個程式
我們拖拽一個UI檔案,轉為PY檔案後生成一個類Ui_MainWindow 此時,我們新建一個檔案,用來控制業務邏輯(繼承介面中的類),跟介面分開,這樣我們就完成了介面和邏輯相分離(這段程式碼使用率基本100%,牢牢記住)。 1 __author__ = "WSX" 2 import sys 3
Scrapy爬蟲入門教程七 Item Loaders(專案載入器)
目錄 專案載入器 巢狀裝載器 開發環境: Python 3.6.0 版本 (當前最新) Scrapy 1.3.2 版本 (當前最新) 專案載入器 專案載入器提
連結資料庫報錯(Communications link failure)之解決篇,
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet
Java中GUI簡介、AWT概述、以及佈局管理器(流式佈局管理器、邊界佈局管理器、網格佈局管理器、網格包佈局管理器、卡片佈局管理器)
1 GUI簡介 GUI的全稱是Graphical User Interface,即圖形使用者介面。顧名思義,就是應用程式提供給使用者操作的圖形介面,包括視窗、選單、按鈕、工具欄和其他各種使用者介面元素。Java中針對GUI設計提供了豐富的類庫,這些類分別位
Android中ListView上拉載入(分頁)功能
思路 1新增頁尾,並隱藏 2監聽滑動事件,判斷當滑到低部時,顯示頁尾,並載入資料(介面回撥到activity中載入) 3資料新增完成之後隱藏頁尾 效果圖: 專案結構: 自定義listView類LoadListView package com.zhh.android;
Android中ListView錯位佈局實現(無聊向)
由於某些原因,需要個錯位的頁面,在網上找不到好的例子,試著動手寫了寫。 不考慮配色的完成圖如下: 首先考慮的是,listview每一行左右都有可能縮排。 先假設一行的佈局就是ImageView,TextView,ImageView,程式碼如下: 1 <Line
WinForm中,每隔一段時間(參數)調用一次函數(使用定時器)
pre tick break switch 時間 器) chan pri args 1 System.Windows.Forms.Timer setTimer; //定義一個定時器 2 int flg = 0;
_030_Android_ Android開發之SmsManager(簡訊管理器)詳解
轉自https://blog.csdn.net/qq_37443229/article/details/80039836,感謝作者的無私分享。 Android開發之SmsManager(簡訊管理器)詳解 SmsManager是
反射(Constructor、Field、Method、類載入器)
一:什麼是反射 在認識反射之前,首先回顧下什麼是正向處理。如果我們需要例項化一個物件,首先需要匯入這個類的包,在這個包中找這個類: package CODE.反射; import java.util.Date; public class Fan { public static
1600802047 android 第三次作業(音樂播放器)
一、實現的功能 播放、暫停、上一首、下一首 顯示列表 二、UI介面截圖 第一首歌 第二首歌 第三首歌 第四首歌 list列表 點選播放音樂時圖片旋轉,點選上一首切換上一
一起Talk Android吧(第一百回:Android中使用自定義控制元件)
各位看官們,大家好,上一回中咱們說的是Android中使用自定義佈局的例子,這一回說的例子是Android中使用自定義控制元件。閒話休提,言歸正轉。讓我們一起Talk Android吧! 看官們,我們在上一回中通過自定義佈局巧妙地實現了分隔線,不過這個分隔線中看
Android開發筆記(一百六十)休眠模式下的定時器控制
定時器AlarmManager常常用於需要週期性處理的場合,比如鬧鐘提醒、任務輪詢等等。並且定時器來源於系統服務,即使App已經不在運行了,也能收到定時器發出的廣播而被喚醒。似此迴光返照的神技,便遭到開發者的濫用,造成使用者手機充斥著各種殺不光程序,就算通過手機安全工具一再地
Android自定義DataTimePicker(日期選擇器)
package com.wwj.datetimepicker; import java.text.SimpleDateFormat; import java.util.Calendar; import android.app.Activity; import android.app.AlertDialog
Android中Listview(七)--排序ListView
ListView的A-Z字母排序和過濾搜尋功能並且實現漢字轉成拼音的功能,我們知道一般我們對聯絡人,城市列表等實現A-Z的排序,因為聯絡人和城市列表我們可以直接從資料庫中獲取他的漢字拼音,而對於一般的資料,我們怎麼實現A-Z的排序,我們需要將漢字轉換成拼音就行
Android屬性動畫(三) TimeInterpolator(插值器)
OK,繼續學習屬性動畫,本篇文章是屬性動畫系列的第三篇文章了,今天來學習一下屬性動畫中的TimeInterpolator,如果你對屬性動畫還不太熟悉,可以點選下面的連結學習一下前兩篇文章的知識: 1.介紹 先說說Interpolator,在And