LayoutInflater factory的使用,從修改RecyclerView的fling速率說起

一個例子,比如我們需要控制一下RecyclerView的滑動速率。于是就產(chǎn)生了一個新類,如下:

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 {

    //設(shè)置recycle橫向飛滑之后的速率 默認(rèn)為1.0
    private double flingSpeedX = 1.0f;
    //設(shè)置recycle豎向飛滑之后的速率 默認(rèn)為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,你可以參考下:鏈接

當(dāng)你寫完這個類,應(yīng)用到你的項目中,你就需要把項目中java代碼中的RecycleView換成FlingSpeedRecycleView,把xml處RecycleView的定義改成com.rduwan.ui.FlingSpeedRecycleView

思考1個問題:項目中各種ui都需要一定的自定義,能不能有一種更透明的方式,讓我們不用手動去替換原來的java文件和xml定義???

我們來觀察一個現(xiàn)象,構(gòu)建一個最基本的Android工程。

xml布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.rduwan.basictestapp.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/tv_hello"/>
</RelativeLayout>

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()是不是應(yīng)該是android.widget.TextView?
logcat的實際輸出:

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

***思考:TextView在java類和xml布局中沒有去手動替換為AppCompatTextView,卻被無聲無息的透明化替換掉了。

源碼中找答案,從AppCompatActivity跟進(jìn)去

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

delegate.installViewFactory(),其中delegate為抽象類AppCompatDelegate,實現(xiàn)類AppCompatDelegateImplV9,V11,V14,V23等。這些實現(xiàn)類又繼承了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);
        //注意點:這里的判斷表明只能設(shè)定一個factory,如果在之前設(shè)定了factory,
        //執(zhí)行else,因為activity已經(jīng)有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,如果在之前設(shè)定了factory,AppCompat的特性將不能加載。
  2. 在實現(xiàn)的onCreateView方法中,首先讓activity的factory加載view,在執(zhí)行createView方法,該方法最后進(jìn)入到
    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;
    }

看到這里大家明白了,系統(tǒng)通過AppCompatActivity通過設(shè)置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設(shè)置在super.onCreate(savedInstanceState)前執(zhí)行,
    因為factory只能設(shè)置一個,讓super執(zhí)行就會先設(shè)置AppCompatActivity的factory,而我們的自定義factory就不會生效。
  2. 我們設(shè)定了factory,AppCompatActivity的factory就沒法設(shè)定,
    所以我們必須調(diào)用AppCompatDelegate.createView來完成AppCompat特性的加載。

more

LayoutInflater setFactory 適合一些開發(fā)場景

  1. ui控件定制屬性
  2. 換膚
  3. 加載外部資源
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容