Android对View进行全局拦截处理

Posted Wastrel_xyz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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关键字等。


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是一门非常强大的语言,尤其是其的多态性,可以让你在处理问题的时候轻松的覆盖原有逻辑。对于很多时候我们去拦截或处理一些操作的时候,都是寻找一个可以介入操作的点,然后利用偷天换日的手段来换成自己的逻辑。而寻找点的过程就是分析源码的过程。

以上是关于Android对View进行全局拦截处理的主要内容,如果未能解决你的问题,请参考以下文章

android事件分发,拦截,处理

Android-View的事件分发及拦截-父控件和子控件都处理触摸事件的方式

Android 逆向函数拦截原理 ( 通过修改 GOT 全局偏移表拦截函数 | 通过在实际被调用的函数中添加跳转代码实现函数拦截 )

Android的事件分发(dispatchTouchEvent),拦截(onInterceptTouchEvent)与处理(onTouchEvent)

qt 事件到达最顶层怎么处理

android单例中的监听如何回调