1. 程式人生 > >Android對View進行全域性攔截處理

Android對View進行全域性攔截處理

前言

當我們繼承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函式,這個函式會依次呼叫FactoryFactory2進行建立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是一門非常強大的語言,尤其是其的多型性,可以讓你在處理問題的時候輕鬆的覆蓋原有邏輯。對於很多時候我們去攔截或處理一些操作的時候,都是尋找一個可以介入操作的點,然後利用偷天換日的手段來換成自己的邏輯。而尋找點的過程就是分析原始碼的過程。