一個例子,比如我們需要控制一下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 */
);
}
分析:
- 在installViewFactory中只能使用一個factory,如果在之前設(shè)定了factory,AppCompat的特性將不能加載。
- 在實現(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點:
- factory設(shè)置在super.onCreate(savedInstanceState)前執(zhí)行,
因為factory只能設(shè)置一個,讓super執(zhí)行就會先設(shè)置AppCompatActivity的factory,而我們的自定義factory就不會生效。 - 我們設(shè)定了factory,AppCompatActivity的factory就沒法設(shè)定,
所以我們必須調(diào)用AppCompatDelegate.createView來完成AppCompat特性的加載。
more
LayoutInflater setFactory 適合一些開發(fā)場景
- ui控件定制屬性
- 換膚
- 加載外部資源