Android對View進行全域性攔截處理
阿新 • • 發佈:2019-02-17
前言
當我們繼承AppCompatActivity時,會發現一些系統控制元件會被替換成v4包擴充套件過後的View,它是如何做到全域性攔截替換的呢,有時候我們也有一些需求,需要對某一型別的View進行統一操作。
LayoutInflater 原始碼分析
先來看看inflate
函式:
//Layoutinflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// 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);
}
}
...
}
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
...
View 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;
}
可以看到最終建立View
呼叫的是createViewFromTag
函式,這個函式會依次呼叫Factory
、Factory2
進行建立View
,因此我們就找到了介入View
載入的點,也就是換掉factory
成我們自己的,在LayoutInflater
中找到了兩個Factory
,原始碼如下。
//Layoutinflater.java
public interface Factory {
/**
* 你可以在LayoutInflater呼叫inflating之前處理它。
* 並且可以在Layout檔案中使用自定義Tag並且在這裡處理它。
*
* <p>
* 提示:在這些自定義標籤前面加包名的字首是一個不錯的選擇(例如: com.coolcompany.apps) 來避免與系統的標籤產生衝突。
*
* @param 要解析的標籤名
* @param 建立View的Context
* @param XML解析出來的屬性
*
* @return 返回建立的View,如果返回空則使用預設建立行為
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
/**
* 跟Factory功能差不多,這個只是多了一個parent屬性。
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
從原始碼中可以看到Factory
的作用是在系統解析View之前呼叫者可以先接管解析邏輯,如果不解析才使用預設的行為去解析,也就是我們只需要給LayoutInflater
設定自己的處理邏輯即可。接下來我們看看V4
包是怎麼處理的。
//AppCompatActiviyt.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
...
}
接下來我們又看看installViewFactory
函式幹了什麼?
//AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
顯然就是給LayoutInflater
設定Factory
的,然後再建立View的時候把一些控制元件替換成了AppCompat
打頭的控制元件。有了上面的思路我們就可以依葫蘆畫瓢來攔截處理。
攔截處理View
我這裡的需求是對所有輸入框進行特殊字元處理,比如英文分號,SQL關鍵字等。
package com.cnksi.ndgk.utils;
import android.content.Context;
import android.support.v7.app.AppCompatDelegate;
import android.util.AttributeSet;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
/**
* @version 1.0
* @auth venusic
* @date 2018/2/5 14:44
* @copyRight 四川金信石資訊科技有限公司
* @since 1.0
*/
public class XSSLayoutFactory implements LayoutInflater.Factory2 {
private static final String TAG = "LayoutFactory";
private AppCompatDelegate appCompatDelegate;
private LayoutInflater inflater;
XSSLayoutFactory(AppCompatDelegate appCompatDelegate, LayoutInflater inflater) {
this.appCompatDelegate = appCompatDelegate;
this.inflater = inflater;
if (inflater.getFactory2() == null) {
inflater.setFactory2(this);
} else {
throw new InflateException("inflater has a LayoutFactory!!!");
}
}
public static XSSLayoutFactory installViewFactory(AppCompatDelegate appCompatDelegate, LayoutInflater inflater) {
return new XSSLayoutFactory(appCompatDelegate, inflater);
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//先呼叫AppCompat的處理邏輯
View result = appCompatDelegate.createView(parent, name, context, attrs);
//如果是系統控制元件則直接New一個出來
if (result == null) {
if ("EditText".equals(name)) {
result = new EditText(context, attrs);
}
}
//如果是自定義控制元件 則先檢查是否是我們要處理的子類,如果是子類則呼叫inflate加載出來。
if (result == null && name.indexOf(".") != -1) {
try {
Class clz = Class.forName(name);
if (EditText.class.isAssignableFrom(clz)) {
result = inflater.createView(name, null, attrs);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//判斷屬於我們要處理的型別 然後處理
if (EditText.class.isInstance(result)) {
//doSomeThing
}
//如果上面都沒有處理到 則返回null 這樣就表明是交給系統載入。
return result;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
}
然後在BaseActivity的onCreate
裡呼叫一下就可以看到效果了。
@Override
protected void onCreate(Bundle savedInstanceState) {
//必須要在super.onCreate()之前呼叫,因為LayoutInflater.setFactory()只能呼叫一次。
//因此要趕在Appcompat呼叫之前設定。我們的Factory代理了AppCompatDelegate,因此不會影響到相容性問題。
XSSLayoutFactory.installViewFactory(getDelegate(), getLayoutInflater());
super.onCreate(savedInstanceState);
}
結語
Java是一門非常強大的語言,尤其是其的多型性,可以讓你在處理問題的時候輕鬆的覆蓋原有邏輯。對於很多時候我們去攔截或處理一些操作的時候,都是尋找一個可以介入操作的點,然後利用偷天換日的手段來換成自己的邏輯。而尋找點的過程就是分析原始碼的過程。