author:andy
- 日常定义Button最常用的就是使用 xml中定义好,然后加上backgroud属性,然后部分特殊效果,单独加上xml文件的背景效果,比如:
问题1
当今天设计给个5dp,明天设计给个8dp,后天上面需要2个角为5dp,我们的xml定义的drawable,将会无限膨胀,越来越多。。。。
问题2
市面上换肤框架的基础原理,是啥?
为了解决这个问题1,我们就需要分2步:
1.从 xml加载成Button,
2.xml加载成drawable的图片背景,然后再用Button设置图片背景 的流程
步骤1:xml控件加载流程图
根据xml加载控件流程图,factory2加载xml优先,如果我们自己定义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 = [email protected](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");
}
}
}
根据源码,AppCompatActivity--oncreate()已经会自己创建factory2,所以只需要设置我们的factory2,在super.oncreate(),之前即可
然而AppCompatActiivty设置factory2就是为了兼容新定义的AppCompatTextView,AppCompatButton等等,所以我们为了兼容只能这么处理
val view = [email protected](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;
}
}
根据上面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" 属性避免了
在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就使用系统的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;
}
如果已经设置过factory2,那么反射修改factory2为自己的 BackgroundFactory
private static void forceSetFactory2(LayoutInflater inflater) {
Class compatClass = LayoutInflaterCompat.class;
Class 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继承 系统的控件,利用自带的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中的调用
-: 最新的androidstudio,已经支持自定义的属性直接显示了,简直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);
->如果已经缓存就 ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
->ResourcesImpl resourcesImpl = createResourcesImpl(key);
-> final AssetManager assets = createAssetManager(key);
->assets.addAssetPath(key.mResDir)
该方法内调用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();
//
//反射创建AssetManager 与 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置 目录或压缩包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);
//根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建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);
}