LayoutInflater factory的使用,从修改RecyclerView的fling速率说起

一个例子,比如我们需要控制一下RecyclerView的滑动速率。于是就产生了一个新类,如下:

package com.rduwan.ui.recycleview;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;


public class FlingSpeedRecycleView extends RecyclerView {

    //设置recycle横向飞滑之后的速率 默认为1.0
    private double flingSpeedX = 1.0f;
    //设置recycle竖向飞滑之后的速率 默认为1.0
    private double flingSpeedY = 1.0f;


    public FlingSpeedRecycleView(Context context) {
        super(context);
    }

    public FlingSpeedRecycleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }


    public FlingSpeedRecycleView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public void setFlingSpeedX(double speedX) {
        flingSpeedX = speedX;
    }

    public void setFlingSpeedY(double speedY) {
        flingSpeedY = speedY;
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {
        velocityX = (int)(velocityX * flingSpeedX);
        velocityY = (int)(velocityY * flingSpeedY);
        return super.fling(velocityX, velocityY);
    }
}

ps:如果你需要smooth scrolling,你可以参考下:链接

当你写完这个类,应用到你的项目中,你就需要把项目中java代码中的RecycleView换成FlingSpeedRecycleView,把xml处RecycleView的定义改成com.rduwan.ui.FlingSpeedRecycleView

思考1个问题:项目中各种ui都需要一定的自定义,能不能有一种更透明的方式,让我们不用手动去替换原来的java文件和xml定义???

我们来观察一个现象,构建一个最基本的Android工程。

xml布局:




    

MainAcitivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = (TextView)findViewById(R.id.tv_hello);
        Log.d("MainActivity","textView:"+textView.getClass());
    }

}

textview.getClass()是不是应该是android.widget.TextView?
logcat的实际输出:

1930-1930/? D/MainActivity: 
textView:class android.support.v7.widget.AppCompatTextView

***思考:TextView在java类和xml布局中没有去手动替换为AppCompatTextView,却被无声无息的透明化替换掉了。

源码中找答案,从AppCompatActivity跟进去

  • 1 AppCompatActivity.onCreate()
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        ............
    }

delegate.installViewFactory(),其中delegate为抽象类AppCompatDelegate,实现类AppCompatDelegateImplV9,V11,V14,V23等。这些实现类又继承了LayoutInflaterFactory接口。

  • 2 查看LayoutInflaterFactory源码
    public interface LayoutInflaterFactory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }
    其中对这个接口android说明如下:Hook you can supply that is called when inflating from a LayoutInflater. You can use this to customize the tag names available in your XML layout files.
  • 3 查看AppCompatDelegateImplV9源码
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflaterFactory    
    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        //注意点:这里的判断表明只能设定一个factory,如果在之前设定了factory,
        //执行else,因为activity已经有LayoutInflater,所以AppCompat不能加载
        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");
            }
        }
    }

     /**
     * From {@link android.support.v4.view.LayoutInflaterFactory}
     */
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }


    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = (attrs instanceof XmlPullParser)
                    // If we have a XmlPullParser, we can detect where we are in the layout
                    ? ((XmlPullParser) attrs).getDepth() > 1
                    // Otherwise we have to use the old heuristic
                    : shouldInheritContext((ViewParent) parent);
        }

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

分析:

  1. 在installViewFactory中只能使用一个factory,如果在之前设定了factory,AppCompat的特性将不能加载。
  2. 在实现的onCreateView方法中,首先让activity的factory加载view,在执行createView方法,该方法最后进入到
    AppCompatViewInflater的createView方法。
  • 4 查看AppCompatViewInflater源码
public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        .........

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

看到这里大家明白了,系统通过AppCompatActivity通过设置factory,透明化的把TextView替换成了AppCompatTextView等类了。

解决方案

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new DWInflaterFactory(this.getDelegate()));
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = (TextView) findViewById(R.id.tv_hello);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview_hello);
        Log.d("MainActivity", "textView:" + textView.getClass());
        Log.d("MainActivity", "recyclerView:" + recyclerView.getClass());
    }


    public class DWInflaterFactory implements LayoutInflaterFactory {
        private AppCompatDelegate appCompatDelegate;

        public DWInflaterFactory(AppCompatDelegate appCompatDelegate) {
            this.appCompatDelegate = appCompatDelegate;
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            View view = null;
            if (name.equals("android.support.v7.widget.RecyclerView")) {
                view = new FlingSpeedRecycleView(context, attrs);
            }

            if (view == null) {
                view = appCompatDelegate.createView(parent, name, context, attrs);
            }
            return view;
        }
    }
}

验证logcat输出:

4635-4635/com.rduwan.basictestapp 
D/MainActivity: textView:class android.support.v7.widget.AppCompatTextView
4635-4635/com.rduwan.basictestapp 
D/MainActivity: recyclerView:class com.rduwan.ui.FlingSpeedRecycleView

recyclerView顺利被我们透明替换了

注意2点:

  1. factory设置在super.onCreate(savedInstanceState)前执行,
    因为factory只能设置一个,让super执行就会先设置AppCompatActivity的factory,而我们的自定义factory就不会生效。
  2. 我们设定了factory,AppCompatActivity的factory就没法设定,
    所以我们必须调用AppCompatDelegate.createView来完成AppCompat特性的加载。

more

LayoutInflater setFactory 适合一些开发场景

  1. ui控件定制属性
  2. 换肤
  3. 加载外部资源

你可能感兴趣的:(LayoutInflater factory的使用,从修改RecyclerView的fling速率说起)