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布局:

<?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()是不是应该是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. 加载外部资源
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容