android自定义属性,解决xml泛滥问题

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加载图.png

根据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流程)


xml加载背景流程图.png

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);
    }

你可能感兴趣的:(android自定义属性,解决xml泛滥问题)