android网易云音乐动态换肤的实现

  • 本人所写博客都是一句话带过,只说重点,博客只针对自己所写,怕忘了,如果想要学习,直接看代码,尽量别看我的说明。会把你带沟里

  • 换肤分为两种:

  • 内置换肤:适用日间模式、夜间模式这种皮肤需求极少,直接把资源打包到APK中。

  • 动态换肤:适用于大量皮肤,用户选择下载、QQ、网易云音乐这种。它是将皮肤包下载到本地,皮肤包其实是个APK,网易云的皮肤包是skin结尾的,手动改成压缩包,即可看到项目资源。

(此处提一下 高德地图的换肤是动态换肤的,虽然看起来像是静态的,因为他的换肤是换路线颜色,高德地图的所有地图都是瓦片图片生成的,然后使用OpenGL画路线 点等)

-原理:

  • 采集需要换肤的所有控件
  • 加载皮肤包(resoure)
  • 应用皮肤包

第一步采集

image.png
  • 问: 首先明白setContView做了什么?
  • 答: 我们写的Activity都会对应一个XML,而setContentView的作用就是将XML和Activity绑定一起的,也是通过布局加载器LayoutInflater加载

阅读源码理解部分

  • 第一步 xml绑定activity先调用setContentView()方法,然后方法里执行了
    LayoutInflater.from(mContext).inflate(resId, contentParent);

  @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

  • inflate方法是获取Resoure, 看这句 final XmlResourceParser parser = res.getLayout(resource); 返回XML解析器解析布局文件。后来又调用一个inflate方法
  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
  • 第二个inflate方法,会将解析后的View再new出一个对象,此处调用了createViewFromTag(...)方法。
 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                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 (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException(" can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, 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);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(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) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }
  • createViewFromTag(View parent, String name, Context context, AttributeSet attrs) 此处的name是XML节点类型名称,比如是Imageview,或者Button,大家再看创建new对象的,如果工厂不为null,创建一个View对象,如果view为null,并且名字包含'.'(常用的imageView不包含. 自定义的才包含. 比如自定义View com.XXX.XXX.ImageView )。
 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

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

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }
  • 这里知道自定义和系统的View,接下来看创建View,上面的方法说,系统View调用oncreateView,其实oncreateView是createView拼接了系统的android.view的名字,拿到全类名,通过mContext.getClassLoader().loadClass(
    prefix != null ? (prefix + name) ,反射一下,获取View的Class对象,然后获得构造函数。 构造函数里有 static final Class[] mConstructorSignature = new Class[] {
    Context.class, AttributeSet.class};上下文,属性,调用构造函数, final View view = constructor.newInstance(args);创建一个View对象返回出去。
protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    } 

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;

        } catch (NoSuchMethodException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    attrs.getPositionDescription() + ": Error inflating class "
                            + (clazz == null ? "" : clazz.getName()), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

(看懂源码,咱就动手撸)
首先说一下用户调用的最终效果:

// 初始化:
SkinManager.init(this);
//换肤调用:
SkinManager.getInstance().loadSkin("/sdcard/app-skin-debug.skin");
  • 定义一个类,让用户初始化我们,拿到布局加载器。
  • 拿到布局加载器的前提,需要获取Activity,(就是LayoutInflater.from(Activity())),此处使用 application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());获取Activity的活动,实现SkinActivityLifecycle()类,
  • 那么拿到Activity,我们需要设置工厂让他帮我们生成View对象,并且加载到布局上面(就是源码Factory2的用法)。LayoutInflaterCompat.setFactory2(layoutInflater,new SkinLayoutFactory());
/**
 * Created by LiChangXing
 * on 2018/3/20.
 */

public class SkinManager {
    private static SkinManager instance;
    private Application application;

    public static SkinManager init(Application application) {
        synchronized (SkinManager.class) {
            if (instance != null) {
                instance = new SkinManager(application);
            }
            return instance;
        }
    }

    private SkinManager getInstance() {
        return instance;
    }

    public SkinManager(Application application) {
        //注册
        application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());
    }
}


public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

    }

    @Override
    public void onActivityStarted(Activity activity) {
        /**
         *  更新布局视图
         */
        //获得Activity的布局加载器
        LayoutInflater layoutInflater = LayoutInflater.from(activity);
        try {
            //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
            //如设置过抛出一次
            //设置 mFactorySet 标签为false
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
            LayoutInflaterCompat.setFactory2(layoutInflater,new SkinLayoutFactory());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onActivityResumed(Activity activity) {

    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {

    }

工厂实现类SkinLayoutFactory

  • 此处写的所有方法都是系统源码的逻辑
  • 根据传过来的name(com.xxxx或者Imageview) 先判断是自定义View还是系统View
  • 如果是系统的View(此处传过来的View就是XML解析后的数据)通过mClassPrefixList集合,拼接完整包名,再反射类,反射构造函数,最后实例化View对象,返回对象。自定义同样道理,只是不需要拼接系统包名了。
  • 因为反射影响性能,此处写了一个键值对,缓存了构造方法
public class SkinLayoutFactory implements LayoutInflater.Factory2 {

    private static final HashMap> sConstructorMap =
            new HashMap>();

    private static final String[] mClassPrefixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };
    static final Class[] mConstructorSignature = new Class[]{
            Context.class, AttributeSet.class};

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //每次渲染一个View 此方法都会执行
        //反射classLoader 和系统一样
        View view = createViewFromTag(name, context, attrs);
        //自定义view
        if (view == null) {
            createView(name, context, attrs);
        }
        return null;
    }

    private View createViewFromTag(String name, Context context, AttributeSet attrs) {
        //自定义组件
        if (-1 == name.indexOf('.')) {
            return null;
        }
        //拼接View的全包名 用于反射 获取View实例对象
        View view = null;
        for (int i = 0; i < mClassPrefixList.length; i++) {
            view = createView(mClassPrefixList[i] + name, context, attrs);
            if (null != view) {
                break;
            }
        }
        return view;
    }

    /**
     * 创建View对象
     *
     * @param name
     * @param context
     * @param attrs
     * @return
     */
    private View createView(String name, Context context, AttributeSet attrs) {
        Constructor constructor = sConstructorMap.get(name);
        if (null == constructor) {
            try {
                Class aClass = context.getClassLoader().loadClass(name).asSubclass
                        (View.class);
                constructor = aClass.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } catch (Exception e) {
            }

        }
        if (null != constructor) {
            try {
                return constructor.newInstance(context, attrs);
            } catch (Exception e) {
            }
        }
        return null;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }
}
  • 一个View中,不可能所有属性都需要更改,为了加载速度,我们需要筛选一下,只更改需要更改的属性。
public class SkinAttribute {
    //定义需筛选出的参数
    private static final List mAttributes = new ArrayList<>();

    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

    List mSkinViews = new ArrayList<>();

    public void load(View view, AttributeSet attributeSet) {
        List skinPairs = new ArrayList<>();
        for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
            //获取属性名
            String name = attributeSet.getAttributeName(i);
            //是否符合 需要筛选的属性名
            if (mAttributes.equals(name)) {
                //获取属性值
                String attributeValue = attributeSet.getAttributeValue(i);
                //写死了 不管了
                if (attributeValue.startsWith("#")) {
                    continue;
                }
                //资源ID
                int resId;
                if (attributeValue.startsWith("?")) {
                    //attr Id 这个是从Theme里拿 获取的Id 是主题的ItemID 所以需要再从itemId转换为colorId
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    //获得 主题 style 中的 对应 attr 的资源id值
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[attrId])[0];
                } else {
                    //attr Id 这个就是颜色ID
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    SkinPair skinPair = new SkinPair(name, resId);
                    skinPairs.add(skinPair);
                }
            }
        }
        if (!skinPairs.isEmpty()) {
            SkinView skinView = new SkinView(view, skinPairs);
            mSkinViews.add(skinView);
        }

    }

    //这就View的javaBean    存的属性集合  和对应的View
    static class SkinView {
        View view;
        List skinPairs;

        public SkinView(View view, List skinPairs) {
            this.view = view;
            this.skinPairs = skinPairs;
        }
    }

    //这是View的属性javaBean  比如说TextView的 width=100do;
    static class SkinPair {
        String attributeName;
        int resId;

        public SkinPair(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }

到第一部分,采集已经结束,接下来是加载皮肤包。
访问 外部资源,需要Assest,访问resId需要Resource,获取资源,缓存皮肤包地址,监听改变等

  /**
     * 加载皮肤包并更新
     *
     * @param path 皮肤包路径
     */
     public void loadSkin(String skinPath) {
        if (TextUtils.isEmpty(skinPath)) {
            //记录使用默认皮肤
            SkinPreference.getInstance().setSkin("");
            //清空资源管理器 皮肤资源属性
            SkinResources.getInstance().reset();
        } else {
            try {
                //反射创建AssetManager 与 Resource
                AssetManager assetManager = AssetManager.class.newInstance();
                //资源路径设置 目录或压缩包
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
                        String.class);
                addAssetPath.invoke(assetManager, skinPath);
                Resources appResource = mContext.getResources();
                //根据当前的显示与配置(横竖屏、语言等)创建Resources
                Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
                        (), appResource.getConfiguration());
                //记录
                SkinPreference.getInstance().setSkin(skinPath);
                //获取外部Apk(皮肤包) 包名
                PackageManager mPm = mContext.getPackageManager();
                PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
                        .GET_ACTIVITIES);
                String packageName = info.packageName;
                SkinResources.getInstance().applySkin(skinResource, packageName);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    //通知采集的View 更新皮肤
        //被观察者改变 通知所有观察者
        setChanged();
        notifyObservers(null);

加载皮肤包后,我们需要通知改变属性。注册监听。当用户调用则


        public void applySkin() {
            for (SkinPair skinPair : skinPairs) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        //Color
                        if (background instanceof Integer) {
                            view.setBackgroundColor((Integer) background);
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);
                        }
                        break;
                    case "src":
                        background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        if (background instanceof Integer) {
                            ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                                    background));
                        } else {
                            ((ImageView) view).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                                (skinPair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    default:
                        break;
                }
            }

        }
    }

切换主题颜色配置:

SkinThemeUtils:
  private static int[] APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS = {
            android.support.v7.appcompat.R.attr.colorPrimaryDark
    };
    private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
            .navigationBarColor};


 public static void updateStatusBarColor(Activity activity) {
        //5.0以上才能修改
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            return;
        }
        //获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
        //当与 colorPrimaryDark  不同时 以statusBarColor为准
        int[] statusBarColorResId = getResId(activity, STATUSBAR_COLOR_ATTRS);
        //如果直接在style中写入固定颜色值(而不是 @color/XXX ) 获得0
        if (statusBarColorResId[0] != 0) {
            activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor
                    (statusBarColorResId[0]));
        } else {
            //获得 colorPrimaryDark
            int colorPrimaryDarkResId = getResId(activity, APPCOMPAT_COLOR_PRIMARY_DARK_ATTRS)[0];
            if (colorPrimaryDarkResId != 0) {
                activity.getWindow().setStatusBarColor(SkinResources.getInstance().getColor
                        (colorPrimaryDarkResId));
            }
        }
        if (statusBarColorResId[1] != 0) {
            activity.getWindow().setNavigationBarColor(SkinResources.getInstance().getColor
                    (statusBarColorResId[1]));
        }
    }

配置字体

依赖库:
attrs.xml


    


主工程:
styles.xml
@string/typeface

strings.xml

    DNSkin
    


皮肤包:
assets/font/global.tff
Strings.xml:


    font/global.ttf

注:要改的View(不管Textview Button还是其他控件),都是继承View,换肤其实就是换属性,比如说View的TextColor,backGround,src(文字 图片 颜色)等。

你可能感兴趣的:(android网易云音乐动态换肤的实现)