一个例子,比如我们需要控制一下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 */
);
}
分析:
- 在installViewFactory中只能使用一个factory,如果在之前设定了factory,AppCompat的特性将不能加载。
- 在实现的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点:
- factory设置在super.onCreate(savedInstanceState)前执行,
因为factory只能设置一个,让super执行就会先设置AppCompatActivity的factory,而我们的自定义factory就不会生效。 - 我们设定了factory,AppCompatActivity的factory就没法设定,
所以我们必须调用AppCompatDelegate.createView来完成AppCompat特性的加载。
more
LayoutInflater setFactory 适合一些开发场景
- ui控件定制属性
- 换肤
- 加载外部资源