美文网首页别人的经验
android网易云音乐动态换肤的实现

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

作者: 代码界的泥石流 | 来源:发表于2018-03-20 14:46 被阅读208次
    • 本人所写博客都是一句话带过,只说重点,博客只针对自己所写,怕忘了,如果想要学习,直接看代码,尽量别看我的说明。会把你带沟里

    • 换肤分为两种:

    • 内置换肤:适用日间模式、夜间模式这种皮肤需求极少,直接把资源打包到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("<merge /> 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<? extends View> constructor = sConstructorMap.get(name);
            if (constructor != null && !verifyClassLoader(constructor)) {
                constructor = null;
                sConstructorMap.remove(name);
            }
            Class<? extends View> 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 ? "<unknown>" : 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<String, Constructor<? extends View>> sConstructorMap =
                new HashMap<String, Constructor<? extends View>>();
    
        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<? extends View> constructor = sConstructorMap.get(name);
            if (null == constructor) {
                try {
                    Class<? extends View> 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<String> 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<SkinView> mSkinViews = new ArrayList<>();
    
        public void load(View view, AttributeSet attributeSet) {
            List<SkinPair> 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<SkinPair> skinPairs;
    
            public SkinView(View view, List<SkinPair> 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
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <attr name="skinTypeface" format="string"/>
    </resources>
    
    主工程:
    styles.xml
    <item name="skinTypeface">@string/typeface</item>
    
    strings.xml
    <resources>
        <string name="app_name">DNSkin</string>
        <string name="typeface"/>
    </resources>
    
    皮肤包:
    assets/font/global.tff
    Strings.xml:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="typeface">font/global.ttf</string>
    </resources>
    

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

    相关文章

      网友评论

      本文标题:android网易云音乐动态换肤的实现

      本文链接:https://www.haomeiwen.com/subject/jtguqftx.html