换肤框架

45 篇文章 0 订阅

在进行换肤框架讲解之前,我先把View创建过程说一下:
调用Context.getSystemService()方法

LayoutInflater inflater = LayoutInflater.from(context);  
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
View rootView = inflater.inflate(R.layout.view_layout, null);

直接使用LayoutInflater.from()方法
 
View rootView = inflater.inflate(R.layout.view_layout, null);  

在Activity下直接调用getLayoutInflater()方法
 
LayoutInflater inflater = getLayoutInflater();  
View rootView = inflater.inflate(R.layout.view_layout, null);

使用View的静态方法View.inflate()
 
rootView = View.inflate(context, R.layout.view_layout, null);
  
这四种方式他们的源代码实质都是一样的,过程都是先获取LayoutInflater 对象context.getSystemService(Context.LAYOUT_INFLATER_SERVICE),然后调用LayoutInflater 对象的函数inflate(int resource, ViewGroup root, boolean attachToRoot),生成对应的View,我们来看看inflate函数:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {  
    if (DEBUG) System.out.println("INFLATING from resource: " + resource);  
    XmlResourceParser parser = getContext().getResources().getLayout(resource);  
    try {  
        return inflate(parser, root, attachToRoot);  
    } finally {  
        parser.close();  
    }  
}  


可以看到上面的函数是先通过resourceId获取对应的XmlResourceParser对象,这个对象用于解析XML文件的。
然后在调用inflate()函数,接着往下看
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
    synchronized (mConstructorArgs) {  
        final AttributeSet attrs = Xml.asAttributeSet(parser);  
        Context lastContext = (Context)mConstructorArgs[0];  
        mConstructorArgs[0] = mContext;  
        View result = root;  
  
        try {  
            // 寻找root节点
            int type;  
            while ((type = parser.next()) != XmlPullParser.START_TAG &&  
                    type != XmlPullParser.END_DOCUMENT) {  
                // Empty  
            }  
  
            if (type != XmlPullParser.START_TAG) {  
                throw new InflateException(parser.getPositionDescription()  
                        + ": No start tag found!");  
            }  
  
            final String name = parser.getName();  
              
          ....
....

            if (TAG_MERGE.equals(name)) {  

  ....
....

            } else {  
                // Temp is the root view that was found in the xml  
                View temp;  
                if (TAG_1995.equals(name)) {  
                    temp = new BlinkLayout(mContext, attrs);  
                } else {  
                    temp = createViewFromTag(root, name, attrs);  
                }  
  
                ViewGroup.LayoutParams params = null;  
  
                if (root != null) {  
                    if (DEBUG) {  
                        System.out.println("Creating params from root: " +  
                                root);  
                    }  
                    // Create layout params that match root, if supplied  
                    params = root.generateLayoutParams(attrs);  
                    if (!attachToRoot) {  
                        // Set the layout params for temp if we are not  
                        // attaching. (If we are, we use addView, below)  
                        temp.setLayoutParams(params);  
                    }  
                }  
  
              ....
....

                // Inflate all children under temp  
                rInflate(parser, temp, attrs, true);  
                if (DEBUG) {  
                    System.out.println("-----> done inflating children");  
                }  
  
                // We are supposed to attach all the views we found (int temp)  
                // to root. Do that now.  
                if (root != null && attachToRoot) {  
                    root.addView(temp, params);  
                }  
  
                // Decide whether to return the root that was passed in or the  
                // top view found in xml.  
                if (root == null || !attachToRoot) {  
                    result = temp;  
                }  
            }  
  
        } catch (XmlPullParserException e) {  
           ....
   ....
}
  
        return result;  
    }  
} 



关键代码就是那两句红色代码,首先找到开始节点,然后调用CreateViewFromTag创建root布局,然后调用rinflate函数创建root布局的所有子View。
上面两个函数,我们先看CreateViewFromTag函数

View createViewFromTag(View parent, String name, AttributeSet attrs) {  
    if (name.equals("view")) {  
        name = attrs.getAttributeValue(null, "class");  
    }  
  
    .....
  
    try {  
        View view;  
        if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);  
        else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);  
        else view = null;  
  
        if (view == null && mPrivateFactory != null) {  
            view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);  
        }  
          
        if (view == null) {  
            if (-1 == name.indexOf('.')) {  
                view = onCreateView(parent, name, attrs);  
            } else {  
                view = createView(name, null, attrs);  
            }  
        }  
  
        if (DEBUG) System.out.println("Created view is: " + view);  
        return view;  
  
    } catch (InflateException e) {  
       ....
       ....
    } 
}  



可以看到mFactory,Factory两个对象用户创建view视图,只有当两个对象都没有创建成功时才会依次调用mPrivateFactory.onCreateView,onCreateView,createView方法,直达view创建成功为止。


其中mFactory,Factory是LayoutInflater的两个可实现的接口,他们的作用是用于创建View,我们在进行皮肤替换的时候,就是对Factory接口进行实现然后替换系统自带的Factory接口,LayoutInflater的boolean类型的标志位mFactorySet代表着是否设置过factory接口。其中AppCompatActivity,在setContentView之前factory接口就设置过,mFactorySet就被设置成true,因此用户自己设置factory就会报错,提示已经被设置过的错误,因此在AppCompatActivity中,就需要通过反射将mFactorySet设置成false,然后把自己实现的factory设置给LayoutInflater,这些操作都必须在setContentView之前完成。


在生成view视图的时候,如果我们已经设置了factory,那么我们的view就一定是根据factory来生成的,我们可以在实现了的factory中来收集需要皮肤更新的view。
换肤框架的思路就是如下:




这里面最主要的两个部分就是,factory的实现类,异步加载其他apk资源的类,其他的类就是容器,接口,回调函数之类的。容器的关系是,一个view容器内部对应一个属性容器表,因为每一个view有多个属性,而属性也需要一个容器类来保存,因为不同的属性设置函数不一样,所以需要多个容器来保存不同的属性。下面来看看factory的onCreate函数(都有注释)

public class SkinFactory implements Factory {  
  
    private static final String DEFAULT_SCHEMA_NAME = "http://schemas.android.com/apk/res-auto";  
    private static final String DEFAULT_ATTR_NAME = "enable";  
      
    private List<SkinView> mSkinViews = new ArrayList<SkinView>();  
      
    @Override  
    public View onCreateView(String name, Context context, AttributeSet attrs) {  
        View view = null;  
//判断view有没有skin:enable属性,自定义属性,设置了就表示该view需要换肤
        final boolean skinEnable = attrs.getAttributeBooleanValue(DEFAULT_SCHEMA_NAME, DEFAULT_ATTR_NAME, false);  
        if(skinEnable) {  //有这个属性,那么就使用自定义函数来创建view
            view = createView(name, context, attrs);  
            if(null != view) {
//收集所有需要换肤的view加入到list中
                parseAttrs(name, context, attrs, view);  
            }  
        }  
//如果返回的是null,那么就会调用系统的创建方法,前面已经说过了
        return view;  
    }  
      
    public final View createView(String name, Context context, AttributeSet attrs) {  
        View view = null;  
//给view设置前缀,这样系统才能识别是什么控件
        if(-1 == name.indexOf('.')) {  
            if("View".equalsIgnoreCase(name)) {  
                view = createView(name, context, attrs, "android.view.");  
            }  
            if(null == view) {  
                view = createView(name, context, attrs, "android.widget.");  
            }  
            if(null == view) {  
                view = createView(name, context, attrs, "android.webkit.");  
            }  
        } else {  
            view = createView(name, context, attrs, null);  
        }  
        return view;  
    }  
      
    View createView(String name, Context context, AttributeSet attrs, String prefix) {  
        View view = null;  
        try {  
            view = LayoutInflater.from(context).createView(name, prefix, attrs);  
        } catch (Exception e) {  
        }  
        return view;  
    }  
      
    private void parseAttrs(String name, Context context, AttributeSet attrs, View view) {  
        int attrCount = attrs.getAttributeCount();  
        final Resources temp = context.getResources();  
        List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>();  
        for(int i = 0; i < attrCount; i++) {  
            String attrName = attrs.getAttributeName(i);  
            String attrValue = attrs.getAttributeValue(i);  
            if(isSupportedAttr(attrName)) {  
//由此可见,属性只有设置了@引用资源的才能换肤
                if(attrValue.startsWith("@")) {  
                    int id = Integer.parseInt(attrValue.substring(1));  
                    String entryName = temp.getResourceEntryName(id);  
                    String entryType = temp.getResourceTypeName(id);  
                      
                    BaseAttr viewAttr = createAttr(attrName, attrValue, id, entryName, entryType);  
                    if(null != viewAttr) {  
                        viewAttrs.add(viewAttr);  
                    }  
                }  
            }  
        }  
          
        if(viewAttrs.size() > 0) {  
            SkinView skinView = new SkinView();  
            skinView.view = view;  
            skinView.viewAttrs = viewAttrs;  
            mSkinViews.add(skinView);  
        }  
    }  
  
    // attrName:textColor   attrValue:2130968576   entryName:common_bg_color   entryType:color  
    private BaseAttr createAttr(String attrName, String attrValue, int id, String entryName, String entryType) {  
        BaseAttr viewAttr = null;  
//目前只实现了background和textcolor属性设置
        if("background".equalsIgnoreCase(attrName)) {  
            viewAttr = new BackgroundAttr();   //背景属性容器
        } else if("textColor".equalsIgnoreCase(attrName)) {  
            viewAttr = new TextColorAttr();  //文字颜色属性容器
        }  
        if(null != viewAttr) {  
            viewAttr.attrName = attrName;  
            viewAttr.attrValue = id;  
            viewAttr.entryName = entryName;  
            viewAttr.entryType = entryType;  
        }  
        return viewAttr;  
    }  
  
    //是否支持该属性
    private boolean isSupportedAttr(String attrName) {  
        if("background".equalsIgnoreCase(attrName)) {  
            return true;  
        } else if("textColor".equalsIgnoreCase(attrName)) {  
            return true;  
        }  
        return false;  
    }  
  
   //应用皮肤
    public void applaySkin() {  
        if(null != mSkinViews) {  
            for(SkinView skinView : mSkinViews) {  
                if(null != skinView.view) {  
                    skinView.apply();  
                }  
            }  
        }  
    }  
}  


再来看看SkinManager

public final class SkinManager {  
  
    private static final Object mClock = new Object();  
    private static SkinManager mInstance;  
      
    private Context mContext;  //当前上下文
    private Resources mResources;   //该变量是保存对应皮肤资源的Resources 对象,不同的Resources 代表着不同的皮肤
    private String mSkinPkgName;    //Resources 对应的包名
      
    private SkinManager() {  
    }  
      
   //懒汉加载单例 
    public static SkinManager getInstance() {  
        if(null == mInstance) {  
            synchronized (mClock) {  
                if(null == mInstance) {  
                    mInstance = new SkinManager();  
                }  
            }  
        }  
        return mInstance;  
    }  
      
    public void init(Context context) {  
        enableContext(context);  
        mContext = context.getApplicationContext();  
    }  
      
    public void loadSkin(String skinPath) {  
        loadSkin(skinPath, null);  
    }  
      
    public void loadSkin(final String skinPath, final ILoadListener listener) {  
        enableContext(mContext);  
        if(TextUtils.isEmpty(skinPath)) {  
            return;  
        }  
         //异步加载其他apk的资源 ,给定一个apk包路径,返回一个资源Resources
        new AsyncTask<String, Void, Resources>() {  
            @Override  
            protected void onPreExecute() {  
                if(null != listener) {  
                    listener.onStart();  
                }  
            }  
              
            @Override  
            protected Resources doInBackground(String... params) {  
                if(null != params && params.length == 1) {  
                    String skinPath = params[0];  
                    File file = new File(skinPath);  
                    if(null != file && file.exists()) {  
                        PackageManager packageManager = mContext.getPackageManager();  
                        PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, 1);  
                        if(null != packageInfo) {  
                            mSkinPkgName = packageInfo.packageName;  
                        }  
                        return getResources(mContext, skinPath);  
                    }  
                }  
                return null;  
            }  
            @Override  
            protected void onPostExecute(Resources result) {  
                if(null != result) {  
                    mResources = result;  
                    if(null != listener) {  
   // 如果返回的Resources 不是空的话,那么就回调listener,这里就回到了activity中
 //  根据之前说factory是在setContentView()之前设置的,所以Activity中就可以使用factory
// 然后这个回调就调用   factory.applyskin()函数,这样收集的view就会设置Resources 对应的属性
// 下面的getColor(),getDrawer(),函数就是根据ID返回Resources中的属性值, 那么就可以达到换肤的效果了。
                        listener.onSuccess();  
                    }  
                } else {  
                    if(null != listener) {  
                        listener.onFailure();  
                    }  
                }  
            }  
        }.execute(skinPath);  
    }  
      
    public Resources getResources(Context context, String apkPath) {  
        try {   
            //下面四行是固定写法,反射加载静态资源。
            AssetManager assetManager = AssetManager.class.newInstance();  
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);  
            addAssetPath.setAccessible(true);  
            addAssetPath.invoke(assetManager, apkPath);  
              
    //生成Resources 
            Resources r = context.getResources();  
            Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());  
            return skinResources;  
        } catch (Exception e) {  
        }  
        return null;  
    }  
      
    public void restoreDefaultSkin() {  
        if(null != mResources) {  
            mResources = null;  
            mSkinPkgName = null;  
        }  
    }  
      
    public int getColor(int id) {  
        enableContext(mContext);  
        Resources originResources = mContext.getResources();  
        int originColor = originResources.getColor(id);  
        if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {  
            return originColor;  
        }  
        String entryName = mResources.getResourceEntryName(id);  
        int resourceId = mResources.getIdentifier(entryName, "color", mSkinPkgName);  
        try {  
            return mResources.getColor(resourceId);  
        } catch (Exception e) {  
        }  
        return originColor;  
    }  
      
    public Drawable getDrawable(int id) {  
        enableContext(mContext);  
        Resources originResources = mContext.getResources();  
        Drawable originDrawable = originResources.getDrawable(id);  
        if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {  
            return originDrawable;  
        }  
        String entryName = mResources.getResourceEntryName(id);  
        int resourceId = mResources.getIdentifier(entryName, "drawable", mSkinPkgName);  
        try {  
            return mResources.getDrawable(resourceId);  
        } catch (Exception e) {  
        }  
        return originDrawable;  
    }  
      
    private void enableContext(Context context) {  
        if(null == context) {  
            throw new NullPointerException();  
        }  
    }  
}  



最后  附上源码地址:http://download.csdn.net/detail/llew2011/9518833
    参考的博客地址:http://blog.csdn.net/llew2011/article/details/51252401


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值