PaintedSkin

A solution to Android App skin framework, very low invasion and learning costs.Copy the code

Frame effect video

The latest version

The module instructions version
PaintedSkin Skin core pack 3.1.6
StandardPlugin Plug-in package to reduce code intrusion 3.1.6
AutoPlugin Fully automatic plug-in package 3.1.6
ConstraintLayoutCompat ConstraintLayout Skin compatible package 3.1.6
TypefacePlugin Font replacement plugin 3.1.6

Give the project address to Star if you like!


Framework Implementation Principle

TODO

Function is introduced

  1. Supports XML full View peels
  2. Support for XML specified View peels
  3. Support code to create View peels
  4. Support custom View, View provided by the tripartite library, custom attribute skin
  5. Supports most basic View peels
  6. Support differentiated skin change (suitable for partial View festival skin change)
  7. [support global dynamic font replacement](#TypefacePlugin used)
  8. Support for intercepting View creation through interceptors
  9. Support Androidx and support
  10. Support custom extensions
  11. Does not conflict with other libraries that rely on LayoutInflater.Factory

use

Add the dependent

  1. In the engineering ofbuild.gradleAdd:

buildscript {
    repositories {
	maven { url "https://jitpack.io" } // Must be added
    }
    dependencies{...classpath 'com. Hujiang. Aspectjx: gradle - android plugin - aspectjx: 2.0.10' // If you do not use AutoPlugin, do not add it
    }
    allprojects {
	maven { url "https://jitpack.io" } // Must be added}}Copy the code
  1. If you want to useAutoPluginIn the project,appthebuild.gradleAdd:

apply plugin: 'android-aspectjx' 
android {
	...
}

Copy the code
  1. In the projectappthebuild.gradleAdd :: to file

dependencies {
	// Dependent reflection library
 	implementation 'com. Making. CoderAlee: Reflex: 1.2.0'
 	/ / core library
    implementation 'com.github.CoderAlee.PaintedSkin:PaintedSkin:TAG'
	implementation 'com.github.CoderAlee.PaintedSkin:StandardPlugin:TAG'
	// StandardPlugin and AutoPlugin only need to be added
	annotationProcessor 'com.github.CoderAlee.PaintedSkin:AopPlugin:TAG'
	implementation 'com.github.CoderAlee.PaintedSkin:AopPlugin:TAG'
	// Import if the project ConstraintLayout needs to be skinned
	implementation 'com.github.CoderAlee.PaintedSkin:ConstraintLayoutCompat:TAG'
	// when the font library needs to be replaced
	implementation 'com.github.CoderAlee.PaintedSkin:TypefacePlugin:TAG'. }Copy the code

Run the configure

PaintedSkin supports three peels:

Skinmode. REPLACE_ALL All views participate in skinmode. View with skin:enable=”false” tag will not participate in skinmode. skinmode. REPLACE_ALL

Skinmode. REPLACE_MARKED Only views with **skin:enable=”true”** tagged participated in skinmode. REPLACE_MARKED

Skinmode. DO_NOT_REPLACE None of the views are involved in peels

API:

public final class App extends Application {
    static{ Config.getInstance().setSkinMode(Config.SkinMode.REPLACE_ALL); }}Copy the code

PaintedSkin supports debug mode and strict mode:

In debugging mode, logs of some key nodes within the framework and the execution time of skin changing task will be output;

In strict mode, an exception is thrown if an error occurs within the framework.

API:

public final class App extends Application {
    static {
         Config.getInstance().setEnableDebugMode(false);
         Config.getInstance().setEnableStrictMode(false); }}Copy the code

The plug-in USES

StandardPlugin use:

public final class App extends Application {
    
    @Override
    public void onCreate(a) {
        super.onCreate();
        WindowManager.getInstance().init(this.newOptionFactory()); }}Copy the code
final class OptionFactory implements IOptionFactory {
    @Override
    public int defaultTheme(a) {
        return 0;
    }

    @Override
    public IThemeSkinOption requireOption(int theme) {
        switch (theme) {
            case 1:
                return new NightOption();
            default:
                return null; }}}Copy the code

The AutoPlugin no longer requires the developer to invoke the initialization code and simply adds the annotation ** @skin ** to the implementation class that implements the IOptionFactory interface:

@Skin
public final class OptionFactory implements IOptionFactory {
    @Override
    public int defaultTheme(a) {
        return 0;
    }

    @Override
    public IThemeSkinOption requireOption(int theme) {
        switch (theme) {
            case 1:
                return new NightOption();
            default:
                return null; }}}Copy the code

The topic configuration

class NightOption implements IThemeSkinOption {

    @Override
    public LinkedHashSet<String> getStandardSkinPackPath(a) {
        LinkedHashSet<String> pathSet = new LinkedHashSet<>();
        pathSet.add("/sdcard/night.skin");
        returnpathSet; }}Copy the code

In the skin

 ThemeSkinService.getInstance().switchThemeSkin(int theme);
Copy the code

Skin pack construction

  1. Create an Android Application project

  2. The skin project package name cannot be the same as the host application package name

  3. Place the resources that need to be skinned in the corresponding directory of RES

    For example, Button text color

    APK in the res/values/colors. XML

    <color name="textColor">#FFFFFFFF</color>
    Copy the code

    Res /values/colors.xml in the skin package

    <color name="textColor">#FF000000</color>
    Copy the code

    For example, the Button background image

    APK in the res/mipmap bg_button. PNG

    Res /mipmap/bg_button.png in skin pack

  4. Add to the build.gradle file of the skin pack project:

      applicationVariants.all { variant ->
            variant.outputs.all { output ->
                outputFileName = "xxx.skin"}}Copy the code

Dynamically create View peels

The core interface WindowManager. GetInstance (). GetWindowProxy (getContext ()). AddEnabledThemeSkinView (View, SkinElement);

  TextView textView = new TextView(getContext());
        textView.setTextColor(getResources().getColor(R.color.textColor));
        textView.setText("Dynamically create View to participate in skin change");
        WindowManager.getInstance().getWindowProxy(getContext()).addEnabledThemeSkinView(textView, new SkinElement("textColor", R.color.textColor));
        layout.addView(textView);
Copy the code

Advanced usage

Intercept the View creation process

        ThemeSkinService.getInstance().getCreateViewInterceptor().add(new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                return onCreateView(name, context, attrs);
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                if (TextUtils.equals(name,"TextView")) {return new Button(context, attrs);
                }
                return null; }});Copy the code

You can do a lot of things by intercepting the View creation process. For example, this code can replace a global TextView with a Button. This is much faster and more convenient than making changes one by one in XML. This is how Google can replace a Button with an AppCompatButton. AppCompatDelegate is the same technical solution.

Custom View, tripartite library View skin

When a custom View or a tripartite library View has a custom property that needs to be skinned:

  1. Implement IThemeSkinExecutorBuilder interfaces for parsing support for a skin and create the corresponding attribute for skin actuators. See the DefaultExecutorBuilder that comes with the framework:
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class DefaultExecutorBuilder implements IThemeSkinExecutorBuilder {
    /** ** Attribute background */ supported by peels
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_BACKGROUND = "background";
    /** ** Peels the supported properties of the foreground color */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_FOREGROUND = "foreground";
    /** ** The supported properties of the skin font color */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_TEXT_COLOR = "textColor";
    /** ** Attributes supported by peels indicate font colors */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_TEXT_COLOR_HINT = "textColorHint";
    /** * The attribute supported by peels highlights the background color */ when selected
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_TEXT_COLOR_HIGH_LIGHT = "textColorHighlight";
    /** ** The color of the attribute link supported by peels */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_TEXT_COLOR_LINK = "textColorLink";
    /** ** Properties progress bar background */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_PROGRESS_DRAWABLE = "progressDrawable";
    /** ** The property supported by peels is ListView split line */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_LIST_VIEW_DIVIDER = "divider";
    /** * The properties supported by peels are padded */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_SRC = "src";
    /** ** Properties button background */ supported by peels
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final String ATTRIBUTE_BUTTON = "button";
    private static final Map<Integer, String> SUPPORT_ATTR = new HashMap<>();

    static {
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_background, ATTRIBUTE_BACKGROUND);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_foreground, ATTRIBUTE_FOREGROUND);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColor, ATTRIBUTE_TEXT_COLOR);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorHint, ATTRIBUTE_TEXT_COLOR_HINT);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorHighlight, ATTRIBUTE_TEXT_COLOR_HIGH_LIGHT);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_textColorLink, ATTRIBUTE_TEXT_COLOR_LINK);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_progressDrawable, ATTRIBUTE_PROGRESS_DRAWABLE);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_divider, ATTRIBUTE_LIST_VIEW_DIVIDER);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_src, ATTRIBUTE_SRC);
        SUPPORT_ATTR.put(R.styleable.BasicSupportAttr_android_button, ATTRIBUTE_BUTTON);
    }

    /** * Parse the attribute ** that supports peels@param context      {@link Context}
     * @param attributeSet {@link AttributeSet}
     * @return {@link SkinElement}
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @Override
    public Set<SkinElement> parse(@NonNull Context context, @NonNull AttributeSet attributeSet) {
        TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.BasicSupportAttr);
        if (null == typedArray) {
            return null;
        }
        Set<SkinElement> elementSet = new HashSet<>();
        try {
            for (Integer key : SUPPORT_ATTR.keySet()) {
                try {
                    if (typedArray.hasValue(key)) {
                        elementSet.add(new SkinElement(SUPPORT_ATTR.get(key), typedArray.getResourceId(key, -1))); }}catch (Throwable ignored) {
                }
            }
        } catch (Throwable ignored) {
        } finally {
            typedArray.recycle();
        }
        return elementSet;
    }

    /** ** Requires a skin changer **@paramView needs to be skinned view *@paramElement Specifies the element to execute@return {@link ISkinExecutor}
     */
    @Override
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public ISkinExecutor requireSkinExecutor(@NonNull View view, @NonNull SkinElement element) {
        return BasicViewSkinExecutorFactory.requireSkinExecutor(view, element);
    }

    /** * Whether the attribute ** is supported@param view     View
     * @paramAttrName Attribute name *@returnTrue: * / support
    @Override
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean isSupportAttr(@NonNull View view, @NonNull String attrName) {
        returnSUPPORT_ATTR.containsValue(attrName); }}Copy the code
  1. Inherit from BaseSkinExecutor to provide the corresponding peel executor:
 public class ViewSkinExecutor<T extends View> extends BaseSkinExecutor<T> {
      
          public ViewSkinExecutor(@NonNull SkinElement fullElement) {
              super(fullElement);
          }
      
          @Override
          protected void applyColor(@NonNull T view, @NonNull ColorStateList colorStateList, @NonNull String attrName) {
              switch (attrName) {
                  case ATTRIBUTE_BACKGROUND:
                  case ATTRIBUTE_FOREGROUND:
                      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                          applyDrawable(view, new ColorStateListDrawable(colorStateList), attrName);
                      } else {
                          applyColor(view, colorStateList.getDefaultColor(), attrName);
                      }
                      break;
                  default:
                      break; }}@Override
          protected void applyColor(@NonNull T view, int color, @NonNull String attrName) {
              switch (attrName) {
                  case ATTRIBUTE_BACKGROUND:
                      view.setBackgroundColor(color);
                      break;
                  case ATTRIBUTE_FOREGROUND:
                      applyDrawable(view, new ColorDrawable(color), attrName);
                      break;
                  default:
                      break; }}@Override
          protected void applyDrawable(@NonNull T view, @NonNull Drawable drawable, @NonNull String attrName) {
              switch (attrName) {
                  case ATTRIBUTE_BACKGROUND:
                      view.setBackground(drawable);
                      break;
                  case ATTRIBUTE_FOREGROUND:
                      view.setForeground(drawable);
                      break;
                  default:
                      break; }}}Copy the code
  1. Add the custom ThemeSkinExecutorBuilder to the framework:
ThemeSkinService.getInstance().addThemeSkinExecutorBuilder(xxx);
Copy the code

ConstraintLayout Skin compatible package to use

public final class App extends Application {
    static{ ConstraintLayoutCompat.init(); }}Copy the code

TypefacePlugin use

public final class App extends Application {
    static {
        TypefacePlugin.init();
    }
    
    @Override
    public void onCreate(a) {
        super.onCreate();
       TypefacePlugin.getInstance().setEnable(true).switchTypeface(Typeface); }}Copy the code

License Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License"); Copyright [2018] [mingyu. Liu] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.Copy the code