author:andy
- 日常定義Button最常用的就是使用 xml中定義好,然后加上backgroud屬性,然后部分特殊效果,單獨加上xml文件的背景效果,比如:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_white" />
<corners android:radius="12dp" />
</shape>
問題1
當今天設計給個5dp,明天設計給個8dp,后天上面需要2個角為5dp,我們的xml定義的drawable,將會無限膨脹,越來越多。。。。
問題2
市面上換膚框架的基礎原理,是啥?
為了解決這個問題1,我們就需要分2步:
1.從 xml加載成Button,
2.xml加載成drawable的圖片背景,然后再用Button設置圖片背景 的流程
步驟1:xml控件加載流程圖

根據(jù)xml加載控件流程圖,factory2加載xml優(yōu)先,如果我們自己定義factory2,就可以攔截整個View生成的流程:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LayoutInflater.from(this).factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
Log.i(TAG, "name=$name ")
val view = this@MainActivity.delegate.createView(parent, name, context, attrs)
val typeArray =
context.obtainStyledAttributes(attrs, R.styleable.CustomeDrawable)
for (i in 0 until attrs.attributeCount) {
Log.i(
TAG,
" name ${attrs.getAttributeName(i)} value=${attrs.getAttributeValue(i)}"
)
}
val radius =
typeArray.getDimension(R.styleable.CustomeDrawable_corner_radius, 0f)
if (radius > 0) {
val drawable = GradientDrawable()
drawable.cornerRadius = radius
drawable.setColor((Color.parseColor("#ff0000")))
view?.background = drawable
}
typeArray.recycle()
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null;
}
}
setContentView(R.layout.activity_main)
btn_join?.setOnClickListener {
startActivity(Intent(baseContext, TwoActivity::class.java))
}
}
}
然而問題是會報
Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.view.LayoutInflater.setFactory2(LayoutInflater.java:369)
at com.yy.customedrawable.MainActivity.onCreate(MainActivity.kt:22)
at android.app.Activity.performCreate(Activity.java:7966)
at android.app.Activity.performCreate(Activity.java:7955)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1306)
再次看源碼AppCompatActivity的oncreate()方法
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
AppCompatDelegateImpl中的 installViewFactory
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
根據(jù)源碼,AppCompatActivity--oncreate()已經(jīng)會自己創(chuàng)建factory2,所以只需要設置我們的factory2,在super.oncreate(),之前即可
然而AppCompatActiivty設置factory2就是為了兼容新定義的AppCompatTextView,AppCompatButton等等,所以我們?yōu)榱思嫒葜荒苓@么處理
val view = this@MainActivity.delegate.createView(parent, name, context, attrs)
步驟2:
xml加載為圖片背景 (xml->drawable流程)

DrawableInflater.inflaterFromTag()
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "animated-selector":
return new AnimatedStateListDrawable();
case "level-list":
return new LevelListDrawable();
case "layer-list":
return new LayerDrawable();
case "transition":
return new TransitionDrawable();
case "ripple":
return new RippleDrawable();
case "adaptive-icon":
return new AdaptiveIconDrawable();
case "color":
return new ColorDrawable();
case "shape":
return new GradientDrawable();
case "vector":
return new VectorDrawable();
case "animated-vector":
return new AnimatedVectorDrawable();
case "scale":
return new ScaleDrawable();
case "clip":
return new ClipDrawable();
case "rotate":
return new RotateDrawable();
case "animated-rotate":
return new AnimatedRotateDrawable();
case "animation-list":
return new AnimationDrawable();
case "inset":
return new InsetDrawable();
case "bitmap":
return new BitmapDrawable();
case "nine-patch":
return new NinePatchDrawable();
case "animated-image":
return new AnimatedImageDrawable();
default:
return null;
}
}
根據(jù)上面2個圖,可以看出最終我們的xml最終轉換成了一個個對象(StateListDrawable,ColorDrawable等),也就是說我們只需要將xml定義的屬性轉化成 Drawable 的子類就可以。
如何自定義相關屬性,減少xml的定義
- 方法1 既然xml圖片背景最終生成drawable,完全可以使用drawable的子類,然后自己設置
上代碼
//方式1
TextView tv = findViewById(R.id.test_view1);
tv.setClickable(true);
ColorStateList colors = new DrawableCreator.Builder().setPressedTextColor(Color.RED
).setUnPressedTextColor(Color.BLUE).buildTextColor();
tv.setTextColor(colors);
- 方法2 注冊factory2,xml直接使用自定義屬性
注意:由于自定義的 xml屬性,androidstudio 不支持,所以會報紅,只能加上
tools:ignore="MissingPrefix" 屬性避免了
<Button
android:id="@+id/btn_feedback"
app:layout_constraintStart_toStartOf="@id/top_view"
app:layout_constraintEnd_toEndOf="@id/top_view"
app:layout_constraintTop_toBottomOf="@id/test_view1"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_marginTop="15dp"
android:gravity="center"
android:padding="0dp"
android:text="方式2-factory"
android:textColor="@android:color/white"
android:textSize="20sp"
app:bl_corners_radius="20dp"
app:bl_pressed_color="#7CCD7C"
app:bl_shape="rectangle"
app:bl_unpressed_color="#7CFC00" />
在activity中注冊factory2
public static LayoutInflater inject(Context context) {
LayoutInflater inflater;
if (context instanceof Activity) {
inflater = ((Activity) context).getLayoutInflater();
} else {
inflater = LayoutInflater.from(context);
}
if (inflater == null) {
return null;
}
if (inflater.getFactory2() == null) {
BackgroundFactory factory = setDelegateFactory(context);
inflater.setFactory2(factory);
} else if (!(inflater.getFactory2() instanceof BackgroundFactory)) {
forceSetFactory2(inflater);
}
return inflater;
}
如果activity繼承AppCompatActivity就使用系統(tǒng)的factory2
@NonNull
private static BackgroundFactory setDelegateFactory(Context context) {
BackgroundFactory factory = new BackgroundFactory();
if (context instanceof AppCompatActivity) {
final AppCompatDelegate delegate = ((AppCompatActivity) context).getDelegate();
factory.setInterceptFactory(new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return delegate.createView(null, name, context, attrs);
}
});
}
return factory;
}
如果已經(jīng)設置過factory2,那么反射修改factory2為自己的 BackgroundFactory
private static void forceSetFactory2(LayoutInflater inflater) {
Class<LayoutInflaterCompat> compatClass = LayoutInflaterCompat.class;
Class<LayoutInflater> inflaterClass = LayoutInflater.class;
try {
Field sCheckedField = compatClass.getDeclaredField("sCheckedField");
sCheckedField.setAccessible(true);
sCheckedField.setBoolean(inflater, false);
Field mFactory = inflaterClass.getDeclaredField("mFactory");
mFactory.setAccessible(true);
Field mFactory2 = inflaterClass.getDeclaredField("mFactory2");
mFactory2.setAccessible(true);
BackgroundFactory factory = new BackgroundFactory();
if (inflater.getFactory2() != null) {
factory.setInterceptFactory2(inflater.getFactory2());
} else if (inflater.getFactory() != null) {
factory.setInterceptFactory(inflater.getFactory());
}
mFactory2.set(inflater, factory);
mFactory.set(inflater, factory);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
- 方法3定義view繼承 系統(tǒng)的控件,利用自帶的factory2,生成view,再解析自定義的atrribute,解析,然后設置相關屬性背景等。。。
public class BLTextView extends AppCompatTextView {
public BLTextView(Context context) {
super(context);
}
public BLTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public BLTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
BackgroundFactory.setViewBackground(context, attrs, this);
}
}
xml中的調用
<com.noober.background.view.BLTextView
android:id="@+id/bl_text_view"
android:layout_width="120dp"
android:layout_height="48dp"
android:text="方式3-textview"
android:gravity="center"
app:layout_constraintStart_toStartOf="@id/top_view"
app:layout_constraintEnd_toEndOf="@id/top_view"
app:layout_constraintTop_toBottomOf="@id/btn_feedback"
android:layout_marginTop="20dp"
app:bl_pressed_drawable="#9DD1F6"
app:bl_unPressed_drawable="#1B82D1"
/>
-: 最新的androidstudio,已經(jīng)支持自定義的屬性直接顯示了,簡直666
上一個demo[]
資源加載應用----換膚原理-流程(1.獲取attributeSet屬性 2.加載資源和替換)
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
->ContextImpl context = new ContextImpl(
-> context.setResources(packageInfo.getResources());
-> mResources = ResourcesManager.getInstance().getResources(。。
-> return getOrCreateResources(activityToken, key, classLoader);
->如果已經(jīng)緩存就 ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
->ResourcesImpl resourcesImpl = createResourcesImpl(key);
-> final AssetManager assets = createAssetManager(key);
->assets.addAssetPath(key.mResDir)
該方法內(nèi)調用native方法完成資源加載
---換膚的核心的資源的替換
--AssetManager.addAssetPath() -> addAssetPathInternal(String path, boolean appAsLib)
將資源庫apk加載到assetManager
/**
* 記載皮膚并應用
*
* @param skinPath 皮膚路徑 如果為空則使用默認皮膚
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//還原默認皮膚
SkinPreference.getInstance().reset();
SkinResources.getInstance().reset();
} else {
try {
//宿主app的 resources;
Resources appResource = mContext.getResources();
//
//反射創(chuàng)建AssetManager 與 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//資源路徑設置 目錄或壓縮包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);
//根據(jù)當前的設備顯示器信息 與 配置(橫豎屏、語言等) 創(chuàng)建Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
(), appResource.getConfiguration());
//獲取外部Apk(皮膚包) 包名
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);
//記錄
SkinPreference.getInstance().setSkin(skinPath);
} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮膚
//被觀察者改變 通知所有觀察者
setChanged();
notifyObservers(null);
}