android插件化主题方案 (上--LayoutInflateFactory的使用)

青春壹個敷衍的年華 提交于 2019-12-03 02:52:35

android插件化主题方案 (上–LayoutInflateFactory的使用)

标签(空格分隔): android 主题 皮肤 插件化


“做成网易音乐那样的!”
这次产品经理提出的需求就是像网易音乐那样可以更换主题皮肤,当然皮肤切换很多app都有,产品经理也明确表示需要后台有皮肤主题管理能力,所以这次的功能免不了要做成外挂式,不能简单的在资源文件编写多套value的方式实现。
那我们就来一步步的实现吧。


首先,我们先不管插件化的方式,先来看看如何在不更改已编写xml的情况下,快速替换view的资源。

setContentView()做了些什么?
我们知道默认onCreate()执行时,马上会调用setContentView(int id)方法,将layout资源与activity进行绑定,我们看看setContentView做了些什么。

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这是Activity类中的setContentView()的方法,我们继续往下看。

@Override  
public void setContentView(int layoutResID) {  
   if (mContentParent == null) {  
       installDecor();  
   } else {  
       mContentParent.removeAllViews();  
   }  
   mLayoutInflater.inflate(layoutResID, mContentParent);  
   final Callback cb = getCallback();  
   if (cb != null && !isDestroyed()) {  
       cb.onContentChanged();  
   } 
}  

在这里我们看到了熟悉的充气类,我们View的创建离不开Inflate,那么它在创建View的时候做了什么呢?

   public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
   ....
       final View temp = createViewFromTag(root, name, inflaterContext, attrs);
       ...
}

我们找到这个创建view的方法

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
        ...
          View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        ...
        }

最终我们的view是通过factory进行创建的,
而这个factory就是我们这次需要重点介绍的工具。

   /**
 * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as
 * {@code LayoutInflater.Factory2}.
 */
public interface LayoutInflaterFactory {
    /**
     * 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.
     *
     * @param parent The parent that the created view will be placed
     * in; <em>note that this may be null</em>.
     * @param name Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs Inflation attributes as specified in XML file.
     *
     * @return View Newly created view. Return null for the default
     *         behavior.
     */
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}

LayoutInflateFactory是一个接口,其只有一个方法,我们重点关注注释,简单理解为

提供给你一个钩子(可以理解为监听),用以在inflate的时候用名字作为比对,生成对应的View对象进行返回。

so,整个inflate的过程大致就是,xmlParser对xml进行解析,然后传递至Factory根据名字生产相对应的View类,然后返回上层进行添加,好了,下面,我们就可以通过实现这个接口,在onCreate()中对生成的view设置各种属性,这样,我们就能在xml中书写固定的资源情况下,动态对view的属性进行更改。

自定义Factory

 @Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
    View view = delegate.createView(parent, name, context, attrs);
    if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
        TextViewRepository.add(mAppCompatActivity, (TextView) view);
    }

    if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
        if (view == null) {
            view = ViewProducer.createViewFromTag(context, name, attrs);
        }
        if (view == null) {
            return null;
        }
        parseSkinAttr(context, attrs, view);
    }
    return view;
}

这里的isSkinEnable我们用来在xml标记该view需要更换皮肤(自定义命名空间),下面判断TextView的部分用来处理更换字体,暂且不看,其余的创建View的方法于源码相同,创建View之后,我们用parseSkinAttr()方法进行view的处理,当然之前我们需要判断该view是否需要进行切换。
在这个方法中,我们就可以对这个view进行各种处理,更改背景,更改imageView的src等等。
waring
这个factory只用于在界面初始化(比如打开一个activity)的时候进行主题的更改,在界面被绘制完毕后,我们需要更改这个界面所有view的属性的话,得调用其他方法,所以为了标记那些view需要更改,我们在这个方法中可以定义一个集合,在初始化的时候进行保存,以方便后续处理。

  SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;
        mSkinItemMap.put(skinItem.view, skinItem);

skinItem就是需要更改的view及其属性的封装对象。
替换activity的factory为自定义的factory

  @Override
protected void onCreate(Bundle savedInstanceState) {
    mSkinInflaterFactory = new SkinInflaterFactory();
    mSkinInflaterFactory.setAppCompatActivity(this);
    LayoutInflaterCompat.setFactory(getLayoutInflater(), mSkinInflaterFactory);
    super.onCreate(savedInstanceState);
    changeStatusColor();
}

这里我建议书写一个baseActivity,在super.onCreate之前将activity与Factory进行绑定。

这样在每一个activity启动之后,创建view时,我们都可以控制到该view的属性,而我们将这些view保存之后,我们同样可以在activity已经创建完毕的条件下,准确找到需要更改的view并对其做相应的修改操作。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!