├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hikvision │ │ └── skinpeeler │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── skin.apk │ ├── java │ │ └── com │ │ │ └── hikvision │ │ │ └── skinpeeler │ │ │ ├── MainActivity.kt │ │ │ ├── app │ │ │ └── MyApplication.kt │ │ │ └── utils │ │ │ ├── FileUtil.java │ │ │ ├── SharedPreferencesUtil.java │ │ │ └── threadpool │ │ │ ├── AppExecutors.java │ │ │ └── MyThreadFactory.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_bg.jpg │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── pic_test.jpg │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── custom_attrs.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hikvision │ └── skinpeeler │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── skin.apk └── skinlibrary ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── hikvision │ └── skinlibrary │ ├── SkinAttribute.java │ ├── SkinChangeListener.java │ ├── SkinFactory.java │ ├── SkinManager.java │ ├── app │ └── SkinActivityLifecycleCallbacks.java │ ├── data │ └── SkinPathDataSource.java │ ├── util │ ├── SkinResourcess.java │ └── SkinThemeUitls.java │ └── view │ ├── SkinAttrParms.java │ └── SkinView.java └── res └── values ├── colors.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Android template 30 | # Built application files 31 | #*.apk 32 | *.ap_ 33 | 34 | # Files for the ART/Dalvik VM 35 | *.dex 36 | 37 | # Java class files 38 | *.class 39 | 40 | # Generated files 41 | bin/ 42 | gen/ 43 | out/ 44 | 45 | # Gradle files 46 | .gradle/ 47 | build/ 48 | 49 | # Local configuration file (sdk path, etc) 50 | local.properties 51 | 52 | # Proguard folder generated by Eclipse 53 | proguard/ 54 | 55 | # Log Files 56 | *.log 57 | 58 | # Android Studio Navigation editor temp files 59 | .navigation/ 60 | 61 | # Android Studio captures folder 62 | captures/ 63 | 64 | # IntelliJ 65 | *.iml 66 | .idea/workspace.xml 67 | .idea/tasks.xml 68 | .idea/gradle.xml 69 | .idea/assetWizardSettings.xml 70 | .idea/dictionaries 71 | .idea/libraries 72 | .idea/caches 73 | 74 | .idea/libraries/ 75 | .idea/.name 76 | .idea/compiler.xml 77 | .idea/copyright/profiles_settings.xml 78 | .idea/encodings.xml 79 | .idea/misc.xml 80 | .idea/modules.xml 81 | .idea/scopes/scope_settings.xml 82 | .idea/vcs.xml 83 | .classpath 84 | .project 85 | 86 | .idea 87 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 88 | .gradle 89 | 90 | # Keystore files 91 | # Uncomment the following line if you do not want to check your keystore files in. 92 | #*.jks 93 | 94 | # External native build folder generated in Android Studio 2.2 and later 95 | .externalNativeBuild 96 | 97 | # Google Services (e.g. APIs or Firebase) 98 | google-services.json 99 | 100 | # Freeline 101 | freeline.py 102 | freeline/ 103 | freeline_project_description.json 104 | 105 | # fastlane 106 | fastlane/report.xml 107 | fastlane/Preview.html 108 | fastlane/screenshots 109 | fastlane/test_output 110 | fastlane/readme.md 111 | 112 | .idea/fileTemplates/ 113 | fingersm2blibrary/src/androidTest/java/ 114 | fingersm2blibrary/src/main/java/com/example/fingersm2blibrary/ 115 | fingersm2blibrary/src/main/res/ 116 | fingersm2blibrary/src/test/ 117 | app/libs/ 118 | app/src/main/assets/ 119 | skinlibrary/libs/ 120 | skinlibrary/src/androidTest/ 121 | skinlibrary/src/main/res/drawable/ 122 | skinlibrary/src/test/ 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skinpeeler 2 | Android插件化换肤 3 | 4 | 之前看过一点关于Android换肤的文章,插件化、Hook、无缝换肤什么的,听起来好像很难的样子,也没有好好看;现在关于换肤的开源项目现在也比较多,但其实原理都差不多;最近看了一下,自己实现了一波,感觉还是很简单的样子;这里也只是讲讲换肤的原理,知道了原理每个点就可以快速学习,然后完成整个流程;具体实现可以看代码; 5 | 6 | 7 | ![未标题-1.jpg-126.8kB](http://static.zybuluo.com/Tyhj/gy1f1pr1ahuzuutb2k4gpjof/%E6%9C%AA%E6%A0%87%E9%A2%98-1.jpg) 8 | 9 | 10 | ### 换肤原理 11 | 换肤其实很简单,说白了就是修改View的属性,一般就是修改字体颜色、背景、图片等;如果是一个超级简单的界面,最简单的实现方式就是点击换肤的时候把每一个View都重新设置一下属性就完事了; 12 | 13 | View设置属性简单吧,问题就在于在实际项目中不可能手动去获取到每一个控件进行换肤,因为控件太多了;那么问题就变为**如何获取到所有的控件进行属性设置**;然后换肤,其实就是换一套皮肤,换一套资源文件对吧,如何去更换资源文件也是一个问题 14 | 15 | 16 | ### 使用theme实现 17 | Activity的**theme**属性肯定都有用过,theme里面可以设置各种属性,更改了theme里面的属性比如颜色,我们的导航栏什么的使用了theme里面的颜色属性的控件颜色都会改变;可以从这个点入手,设置不同的theme,然后更换theme就可以实现;但是有一个问题,设置theme只有在activity的**setContentView**之前才有效,所以要实现换肤必须得重启Activity才能实现,而且每次新增皮肤必须重新修改源码,重新打包,这种方法感觉不太行; 18 | 19 | ### 获取到所有View 20 | 所以还是那个问题,如何获取到所有的View进行换肤处理;有一个点就是每个Activity都有`setContentView`方法,其实猜也能猜到,就是把xml布局解析成一个View对象;有点像**AOP(面向切面编程)**的思想,如果我们能从这个点切入,拿到每一个生成的View对象,我们就可以统一处理了; 21 | 22 | 那就是去看源码了,其实很简单,我的MainActivity继承至**AppCompatActivity**,跟着方法深入下去 23 | ```java 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | ... 29 | ``` 30 | 31 | AppCompatActivity里面的方法,我们跟着`layoutResID`走,直到layoutResID变为View 32 | ```java 33 | @Override 34 | public void setContentView(@LayoutRes int layoutResID) { 35 | getDelegate().setContentView(layoutResID); 36 | } 37 | ``` 38 | 39 | AppCompatDelegate里面的抽象方法 40 | ```java 41 | public abstract void setContentView(@LayoutRes int resId); 42 | ``` 43 | 44 | AppCompatDelegateImpl里面的实现,其实看到`LayoutInflater.from(mContext).inflate(resId, contentParent);`这句代码就很熟悉了,我们也会经常使用它去加载布局; 45 | ```java 46 | 47 | @Override 48 | public void setContentView(int resId) { 49 | ensureSubDecor(); 50 | ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); 51 | contentParent.removeAllViews(); 52 | LayoutInflater.from(mContext).inflate(resId, contentParent); 53 | mOriginalWindowCallback.onContentChanged(); 54 | } 55 | ``` 56 | 57 | 还是一样跟着`resId`走到**LayoutInflater**里面 58 | ```java 59 | public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { 60 | final Resources res = getContext().getResources(); 61 | if (DEBUG) { 62 | Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" 63 | + Integer.toHexString(resource) + ")"); 64 | } 65 | 66 | final XmlResourceParser parser = res.getLayout(resource); 67 | try { 68 | return inflate(parser, root, attachToRoot); 69 | } finally { 70 | parser.close(); 71 | } 72 | } 73 | ``` 74 | 75 | 走到这个方法是返回生成的View,那生成View肯定是在`inflate(parser, root, attachToRoot);`方法里面 76 | ```java 77 | public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { 78 | final Resources res = getContext().getResources(); 79 | if (DEBUG) { 80 | Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" 81 | + Integer.toHexString(resource) + ")"); 82 | } 83 | 84 | final XmlResourceParser parser = res.getLayout(resource); 85 | try { 86 | return inflate(parser, root, attachToRoot); 87 | } finally { 88 | parser.close(); 89 | } 90 | } 91 | ``` 92 | 找到了生成View的地方 93 | ```java 94 | // Temp is the root view that was found in the xml 95 | final View temp = createViewFromTag(root, name, inflaterContext, attrs); 96 | ``` 97 | 继续看`createViewFromTag`方法,里面使用各种`Factory`去创建View 98 | ```java 99 | try { 100 | View view; 101 | if (mFactory2 != null) { 102 | view = mFactory2.onCreateView(parent, name, context, attrs); 103 | } else if (mFactory != null) { 104 | view = mFactory.onCreateView(name, context, attrs); 105 | } else { 106 | view = null; 107 | } 108 | 109 | if (view == null && mPrivateFactory != null) { 110 | view = mPrivateFactory.onCreateView(parent, name, context, attrs); 111 | } 112 | 113 | if (view == null) { 114 | final Object lastContext = mConstructorArgs[0]; 115 | mConstructorArgs[0] = context; 116 | try { 117 | if (-1 == name.indexOf('.')) { 118 | view = onCreateView(parent, name, attrs); 119 | } else { 120 | view = createView(name, null, attrs); 121 | } 122 | } finally { 123 | mConstructorArgs[0] = lastContext; 124 | } 125 | } 126 | 127 | return view; 128 | ``` 129 | 130 | 131 | 好的,就是这里了,因为所有加载xml布局创建View的流程都会走到这里来,然后Factory只是一个接口,到这里后从逻辑也可以看出来可能会有不同的`Factory`去创建View,也就是说不能再深入下去了;我们只需要实现我们的Factory然后设置给`mFactory2`就可以获取到所有的View了,这里是一个Hook点; 132 | 133 | 134 | 那么问题来了,我们怎么去实现用`Factory`创建View,这里xml里面的东西已经解析完了,看这个方法的参数,有了`attrs`和控件类名`name`,我们自己用反射不就轻松的可以生成View吗; 135 | 136 | ```java 137 | View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 138 | boolean ignoreThemeAttr) { 139 | ... 140 | ``` 141 | 142 | 143 | 还有有最简单的方法,其实系统原来已经实现了对吧,我们照着他写不就完事儿了吗;我们在这里打个断点,进入这个方法,他怎么实现我们就跟着写就完事儿了; 144 | 145 | 146 | ![屏幕快照 2019-09-07 下午9.19.14.png-73.4kB](http://static.zybuluo.com/Tyhj/7vx237rozp8wlmu3ep2xa2ex/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-09-07%20%E4%B8%8B%E5%8D%889.19.14.png) 147 | 148 | 149 | 150 | 发现是在`AppCompatDelegateImpl`这个类实现的方法,好的直接看retur的地方,进去进入方法 151 | ```java 152 | @Override 153 | public View createView(View parent, final String name, @NonNull Context context, 154 | @NonNull AttributeSet attrs) { 155 | if (mAppCompatViewInflater == null) { 156 | TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); 157 | String viewInflaterClassName = 158 | a.getString(R.styleable.AppCompatTheme_viewInflaterClass); 159 | if ((viewInflaterClassName == null) 160 | || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) { 161 | // Either default class name or set explicitly to null. In both cases 162 | // create the base inflater (no reflection) 163 | mAppCompatViewInflater = new AppCompatViewInflater(); 164 | } else { 165 | try { 166 | Class viewInflaterClass = Class.forName(viewInflaterClassName); 167 | mAppCompatViewInflater = 168 | (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() 169 | .newInstance(); 170 | } catch (Throwable t) { 171 | Log.i(TAG, "Failed to instantiate custom view inflater " 172 | + viewInflaterClassName + ". Falling back to default.", t); 173 | mAppCompatViewInflater = new AppCompatViewInflater(); 174 | } 175 | } 176 | } 177 | 178 | boolean inheritContext = false; 179 | if (IS_PRE_LOLLIPOP) { 180 | inheritContext = (attrs instanceof XmlPullParser) 181 | // If we have a XmlPullParser, we can detect where we are in the layout 182 | ? ((XmlPullParser) attrs).getDepth() > 1 183 | // Otherwise we have to use the old heuristic 184 | : shouldInheritContext((ViewParent) parent); 185 | } 186 | 187 | return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, 188 | IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */ 189 | true, /* Read read app:theme as a fallback at all times for legacy reasons */ 190 | VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */ 191 | ); 192 | } 193 | ``` 194 | 195 | 196 | 好的,终于看见最终的方法了 197 | ```java 198 | final View createView(View parent, final String name, @NonNull Context context, 199 | @NonNull AttributeSet attrs, boolean inheritContext, 200 | boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { 201 | final Context originalContext = context; 202 | 203 | // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy 204 | // by using the parent's context 205 | if (inheritContext && parent != null) { 206 | context = parent.getContext(); 207 | } 208 | if (readAndroidTheme || readAppTheme) { 209 | // We then apply the theme on the context, if specified 210 | context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); 211 | } 212 | if (wrapContext) { 213 | context = TintContextWrapper.wrap(context); 214 | } 215 | 216 | View view = null; 217 | 218 | // We need to 'inject' our tint aware Views in place of the standard framework versions 219 | switch (name) { 220 | case "TextView": 221 | view = createTextView(context, attrs); 222 | verifyNotNull(view, name); 223 | break; 224 | case "ImageView": 225 | view = createImageView(context, attrs); 226 | verifyNotNull(view, name); 227 | break; 228 | case "Button": 229 | view = createButton(context, attrs); 230 | verifyNotNull(view, name); 231 | break; 232 | case "EditText": 233 | view = createEditText(context, attrs); 234 | verifyNotNull(view, name); 235 | break; 236 | case "Spinner": 237 | view = createSpinner(context, attrs); 238 | verifyNotNull(view, name); 239 | break; 240 | case "ImageButton": 241 | view = createImageButton(context, attrs); 242 | verifyNotNull(view, name); 243 | break; 244 | case "CheckBox": 245 | view = createCheckBox(context, attrs); 246 | verifyNotNull(view, name); 247 | break; 248 | case "RadioButton": 249 | view = createRadioButton(context, attrs); 250 | verifyNotNull(view, name); 251 | break; 252 | case "CheckedTextView": 253 | view = createCheckedTextView(context, attrs); 254 | verifyNotNull(view, name); 255 | break; 256 | case "AutoCompleteTextView": 257 | view = createAutoCompleteTextView(context, attrs); 258 | verifyNotNull(view, name); 259 | break; 260 | case "MultiAutoCompleteTextView": 261 | view = createMultiAutoCompleteTextView(context, attrs); 262 | verifyNotNull(view, name); 263 | break; 264 | case "RatingBar": 265 | view = createRatingBar(context, attrs); 266 | verifyNotNull(view, name); 267 | break; 268 | case "SeekBar": 269 | view = createSeekBar(context, attrs); 270 | verifyNotNull(view, name); 271 | break; 272 | default: 273 | // The fallback that allows extending class to take over view inflation 274 | // for other tags. Note that we don't check that the result is not-null. 275 | // That allows the custom inflater path to fall back on the default one 276 | // later in this method. 277 | view = createView(context, name, attrs); 278 | } 279 | 280 | if (view == null && originalContext != context) { 281 | // If the original context does not equal our themed context, then we need to manually 282 | // inflate it using the name so that android:theme takes effect. 283 | view = createViewFromTag(context, name, attrs); 284 | } 285 | 286 | if (view != null) { 287 | // If we have created a view, check its android:onClick 288 | checkOnClickListener(view, attrs); 289 | } 290 | 291 | return view; 292 | } 293 | ``` 294 | 295 | 仔细看的话,它创建出来的控件都是`androidx.appcompat.widget`里面的一些比较新的控件,就是升了一下级;其实感觉`mFactory2`就是Google自己修改皮肤用的; 296 | ![屏幕快照 2019-09-07 下午9.36.22.png-152kB](http://static.zybuluo.com/Tyhj/95s1yo2c69xtigj0w3fp0evc/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-09-07%20%E4%B8%8B%E5%8D%889.36.22.png) 297 | 298 | 299 | 如果我们的MainActivity继承至Activity的话,同样打断点会进入到另一个创建View的方法;虽然看起来代码很复杂,我们只要记住我们只是来创建View的,其他我们不管,我们自己实现的时候也是这个道理,我们就是实现创建View的方法;所以直接看创建View很简单了,就是直接用反射,传入View的参数AttributeSet,new一个View出来 300 | ```java 301 | ... 302 | Object[] args = mConstructorArgs; 303 | args[1] = attrs; 304 | final View view = constructor.newInstance(args); 305 | ... 306 | ``` 307 | 308 | 这里还有个问题,既然这里可能有不同的`Factory`来创建View,我们随便实现一个,去设置给`mFactory2`,那肯定只会用我们的`mFactory2`来创建了;那是不是有问题,那我们的MainActivity其实继承**Activity**还是**AppCompatActivity**都会走我们自己的方法了;那我们的这个Factory到底是应该照着AppCompatActivity走的方法来写还是Activity走的这个方法来写,或者还有其他的方法来写 309 | 310 | 其实问题不大,正常开发中我们一般只会选一个Activity来做我们的BaseActivity是吧,我们就按照BaseActivity继承的这种类型来写;而且不同的Activity也可以,因为每个Activity的LayoutInflater是不一样的,我们可以实现不同的Factory分别设置给不同的Activity的LayoutInflater就行了; 311 | 312 | 313 | 好的在这里我们实现自己的Factory去创建View对象,就可以趁机保存所有的对象,然后当我们想换肤的时候就可以把每一个对象的属性修改就可以了;至于这里View怎么保存,怎么销毁,怎么防止内存泄漏这些小问题简单提一下,全局监听一下Activity的生命周期就完事了 314 | ```java 315 | application.registerActivityLifecycleCallbacks(new SkinActivityLifecycleCallbacks()); 316 | ``` 317 | 318 | ### 更换资源文件 319 | 如何更换资源文件?插件化换肤感觉是最好的方法,通过一个皮肤包,可以理解为我们更换了一套皮肤后重新打的一个apk包;这样点击换肤的时候,我们拿到每一个View控件,获取到当前View对应属性的资源的ID,然后通过这个ID去皮肤包里面获取出对应的资源对象,然后设置给当前View就完成了换肤; 320 | 321 | 这里面有一个点,就是我们没法更换我们运行的APP里面的资源文件,我们只是从皮肤包里面读取出相应的资源,比如图片,就是读取出`Drawable`对象,通过`setImageDrawable`设置给当前的View; 322 | 323 | 具体如何去读取其实很简单,就是AssetManager通过反射设置apk文件的路径,就可以拿到Resources对象,Resources就可以通过resId拿到各种资源对象; 324 | ```java 325 | AssetManager assetManager = AssetManager.class.newInstance(); 326 | Method method = assetManager.getClass().getMethod("addAssetPath", String.class); 327 | method.setAccessible(true); 328 | method.invoke(assetManager, path); 329 | Resources resources = mApplication.getResources(); 330 | Resources skinRes = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration()); 331 | 332 | //根据ID获取到资源文件 333 | Drawable drawable = skinRes.getDrawable(resId); 334 | ``` 335 | 336 | 337 | 其实通过皮肤包来实现非常方便,不管是想内置几种皮肤还是上线后更新皮肤包都可以实现,而且不需要改动之前的代码; 338 | 339 | 340 | ### 整体流程 341 | 总结一下,其实就是APP启动的时候,通过`application.registerActivityLifecycleCallbacks();`,监听Activity的生命周期;每个Activity启动的时候,获取Activity的布局加载器**LayoutInflater**,给它设置一个Factory,首先会用它去创建View,在创建的时候就会给View设置指定皮肤包里面的资源了;然后保存这个Activity里面的每一个View,当再次换肤的时候获取到每一个View,重新设置指定皮肤的资源;当然Activity销毁的时候肯定是要释放掉View的;大致的流程就是这样 342 | 343 | 344 | ### 缺点 345 | 这个东西肯定是有缺点的,我们只是针对布局加载器**LayoutInflater**进行换肤,也就是说,只要是通过**LayoutInflater**创建的View我们都可以进行换肤;但是如果有些View是我们new出来的,是换不了的,解决方法也很简单,就是手动添加到换肤的View集合里面去; 346 | 347 | 第二是只换资源文件里面的属性,这没什么好说的,本来就是根据资源文件换肤; 348 | 349 | 第三就是和theme相关的控件颜色没法换,这个很简单,因为我们从皮肤包里面是获取不到theme对象的;其实获取到也没有办法,因为重新给Activity设置theme是必须重启Activity的;我自己各种看源码,各种反射搞了半天,发现这个东西的确是搞不定的,这个东西比较复杂,因为它不是一个具体的资源文件; 350 | 351 | 解决方法是在加载View的时候判断一下View,比如`RadioButton`或者`TabLayout`这种可以设置属性进去的就单独改改很简单,但是你要是涉及到那些只能跟随theme属性的控件比如`Switch`这种,那的确是换不了的,theme换不掉,没办法修改颜色; 352 | 353 | ```java 354 | if (view instanceof RadioButton) { 355 | if (isDrawable()) { 356 | RadioButton radioButton = (RadioButton) view; 357 | Drawable drawable = SkinResourcesUtils.getDrawable(attrValueRefId); 358 | radioButton.setButtonDrawable(drawable); 359 | } 360 | } 361 | 362 | 363 | 364 | if (view instanceof TabLayout) { 365 | TabLayout tl = (TabLayout) view; 366 | if (isColor()) { 367 | int color = SkinResourcesUtils.getColor(attrValueRefId); 368 | tl.setSelectedTabIndicatorColor(color); 369 | } 370 | } 371 | ``` 372 | 373 | 其实还是不错了,有些问题虽然存在,但是实际项目中换肤应该都比较简单,随便写写,适配一下肯定没问题的; 374 | 375 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Android template 30 | # Built application files 31 | *.apk 32 | *.ap_ 33 | 34 | # Files for the ART/Dalvik VM 35 | *.dex 36 | 37 | # Java class files 38 | *.class 39 | 40 | # Generated files 41 | bin/ 42 | gen/ 43 | out/ 44 | 45 | # Gradle files 46 | .gradle/ 47 | build/ 48 | 49 | # Local configuration file (sdk path, etc) 50 | local.properties 51 | 52 | # Proguard folder generated by Eclipse 53 | proguard/ 54 | 55 | # Log Files 56 | *.log 57 | 58 | # Android Studio Navigation editor temp files 59 | .navigation/ 60 | 61 | # Android Studio captures folder 62 | captures/ 63 | 64 | # IntelliJ 65 | *.iml 66 | .idea/workspace.xml 67 | .idea/tasks.xml 68 | .idea/gradle.xml 69 | .idea/assetWizardSettings.xml 70 | .idea/dictionaries 71 | .idea/libraries 72 | .idea/caches 73 | 74 | .idea/libraries/ 75 | .idea/.name 76 | .idea/compiler.xml 77 | .idea/copyright/profiles_settings.xml 78 | .idea/encodings.xml 79 | .idea/misc.xml 80 | .idea/modules.xml 81 | .idea/scopes/scope_settings.xml 82 | .idea/vcs.xml 83 | .classpath 84 | .project 85 | 86 | .idea 87 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 88 | .gradle 89 | 90 | # Keystore files 91 | # Uncomment the following line if you do not want to check your keystore files in. 92 | #*.jks 93 | 94 | # External native build folder generated in Android Studio 2.2 and later 95 | .externalNativeBuild 96 | 97 | # Google Services (e.g. APIs or Firebase) 98 | google-services.json 99 | 100 | # Freeline 101 | freeline.py 102 | freeline/ 103 | freeline_project_description.json 104 | 105 | # fastlane 106 | fastlane/report.xml 107 | fastlane/Preview.html 108 | fastlane/screenshots 109 | fastlane/test_output 110 | fastlane/readme.md 111 | 112 | .idea/fileTemplates/ 113 | fingersm2blibrary/src/androidTest/java/ 114 | fingersm2blibrary/src/main/java/com/example/fingersm2blibrary/ 115 | fingersm2blibrary/src/main/res/ 116 | fingersm2blibrary/src/test/ 117 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | 5 | android { 6 | compileSdkVersion 28 7 | buildToolsVersion '28.0.3' 8 | defaultConfig { 9 | applicationId "com.hikvision.skinpeeler" 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | compileOptions { 23 | sourceCompatibility = '1.8' 24 | targetCompatibility = '1.8' 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test:runner:1.2.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 35 | implementation project(path: ':skinlibrary') 36 | 37 | implementation 'com.android.support:appcompat-v7:28.0.0' 38 | implementation 'com.android.support:design:28.0.0' 39 | implementation 'com.android.support:support-v4:28.0.0' 40 | compile "androidx.core:core-ktx:+" 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 42 | } 43 | repositories { 44 | mavenCentral() 45 | } 46 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hikvision/skinpeeler/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.hikvision.skinpeeler", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/assets/skin.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/assets/skin.apk -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.widget.Switch 8 | import android.widget.Toast 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.core.app.ActivityCompat 11 | import androidx.core.content.ContextCompat 12 | import com.hikvision.skinlibrary.SkinManager 13 | import com.hikvision.skinpeeler.utils.FileUtil 14 | import com.hikvision.skinpeeler.utils.SharedPreferencesUtil 15 | import com.hikvision.skinpeeler.utils.threadpool.AppExecutors 16 | import java.io.File 17 | import java.util.concurrent.TimeUnit 18 | 19 | /** 20 | * 21 | * @author Tyhj 22 | * @date 2019-12-12 23 | * 24 | */ 25 | 26 | class MainActivity : AppCompatActivity() { 27 | 28 | /** 29 | * 换肤包保存位置 30 | */ 31 | private val skinPath = "/sdcard/skin.apk" 32 | 33 | /** 34 | * 是否进行了换肤key 35 | */ 36 | private val skinChangedKey = "skin_changed" 37 | 38 | /** 39 | * theme key 40 | */ 41 | private val androidThemeKey = "android_theme" 42 | 43 | /** 44 | * 插件化换肤 45 | */ 46 | private lateinit var switchJump: Switch 47 | /** 48 | * 切换Theme 49 | */ 50 | private lateinit var swTheme: Switch 51 | 52 | 53 | 54 | 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | val themeStatus = SharedPreferencesUtil.getBoolean(androidThemeKey, false) 58 | //设置主题 59 | if (themeStatus) { 60 | setTheme(R.style.AppTheme) 61 | } else { 62 | setTheme(R.style.AppTheme2) 63 | } 64 | setContentView(R.layout.activity_main) 65 | 66 | 67 | //复制文件 68 | val file = File(skinPath) 69 | if (!file.exists()) { 70 | FileUtil.copyFileFromAssets(this@MainActivity, "skin.apk", skinPath) 71 | } 72 | 73 | switchJump = findViewById(R.id.switchSkin); 74 | swTheme = findViewById(R.id.swTheme) 75 | 76 | swTheme.isChecked = themeStatus 77 | swTheme.setOnClickListener { v -> 78 | SharedPreferencesUtil.save(androidThemeKey, swTheme.isChecked) 79 | startActivity(Intent(this, MainActivity::class.java)) 80 | finish() 81 | } 82 | 83 | switchJump.isChecked = SharedPreferencesUtil.getBoolean(skinChangedKey, false) 84 | //获取存储权限 85 | if (lacksPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 86 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0) 87 | } 88 | 89 | switchJump.setOnClickListener { 90 | var isChecked = switchJump.isChecked 91 | if (isChecked) { 92 | if (SkinManager.getInstance().loadSkin(skinPath)) { 93 | Toast.makeText(this@MainActivity, "换肤成功", Toast.LENGTH_SHORT).show() 94 | } else { 95 | Toast.makeText(this@MainActivity, "换肤失败", Toast.LENGTH_SHORT).show() 96 | AppExecutors.getInstance().scheduledExecutorService().schedule({ AppExecutors.getInstance().mainThread().execute { switchJump.isChecked = false } }, 1, TimeUnit.SECONDS) 97 | isChecked = false 98 | } 99 | } else { 100 | SkinManager.getInstance().clearSkin() 101 | Toast.makeText(this@MainActivity, "恢复默认", Toast.LENGTH_SHORT).show() 102 | } 103 | SharedPreferencesUtil.save(skinChangedKey, isChecked) 104 | } 105 | } 106 | 107 | 108 | /** 109 | * 判断是否缺少权限 110 | * 111 | * @param permission 112 | * @return 113 | */ 114 | private fun lacksPermission(permission: String): Boolean { 115 | return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_DENIED 116 | } 117 | 118 | 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/app/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler.app 2 | 3 | import android.app.Application 4 | import com.hikvision.skinlibrary.SkinManager 5 | import com.hikvision.skinpeeler.utils.SharedPreferencesUtil 6 | import com.hikvision.skinpeeler.utils.threadpool.AppExecutors 7 | 8 | /** 9 | * 10 | * @author Tyhj 11 | * @date 2019-12-12 12 | * 13 | */ 14 | class MyApplication : Application() { 15 | override fun onCreate() { 16 | super.onCreate() 17 | SkinManager.init(this) 18 | SharedPreferencesUtil.init(this) 19 | AppExecutors.getInstance() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/utils/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler.utils; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import java.io.File; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | /** 12 | * 〈一句话功能简述〉 13 | * 〈功能详细描述〉 14 | * 15 | * @author hanpei 16 | * @version 1.0, 2019/9/7 17 | * @since 产品模块版本 18 | */ 19 | public class FileUtil { 20 | 21 | /** 22 | * 从assets目录下拷贝文件 23 | * 24 | * @param context 上下文 25 | * @param assetsFilePath 文件的路径名如:SBClock/0001cuteowl/cuteowl_dot.png 26 | * @param targetFileFullPath 目标文件路径如:/sdcard/SBClock/0001cuteowl/cuteowl_dot.png 27 | */ 28 | public static void copyFileFromAssets(Context context, String assetsFilePath, String targetFileFullPath) { 29 | Log.d("Tag", "copyFileFromAssets "); 30 | InputStream assestsFileImputStream; 31 | try { 32 | assestsFileImputStream = context.getAssets().open(assetsFilePath); 33 | copyFile(assestsFileImputStream, targetFileFullPath); 34 | } catch (IOException e) { 35 | Log.d("Tag", "copyFileFromAssets " + "IOException-" + e.getMessage()); 36 | e.printStackTrace(); 37 | } 38 | } 39 | 40 | private static void copyFile(InputStream in, String targetPath) { 41 | try { 42 | FileOutputStream fos = new FileOutputStream(new File(targetPath)); 43 | byte[] buffer = new byte[1024]; 44 | int byteCount = 0; 45 | while ((byteCount = in.read(buffer)) != -1) {// 循环从输入流读取 46 | // buffer字节 47 | fos.write(buffer, 0, byteCount);// 将读取的输入流写入到输出流 48 | } 49 | fos.flush();// 刷新缓冲区 50 | in.close(); 51 | fos.close(); 52 | } catch (Exception e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/utils/SharedPreferencesUtil.java: -------------------------------------------------------------------------------- 1 | // 2 | // Source code recreated from a .class file by IntelliJ IDEA 3 | // (powered by Fernflower decompiler) 4 | // 5 | 6 | package com.hikvision.skinpeeler.utils; 7 | 8 | import android.app.Application; 9 | import android.content.SharedPreferences; 10 | import android.content.SharedPreferences.Editor; 11 | 12 | public class SharedPreferencesUtil { 13 | private static String name = "skinpeeler_config"; 14 | private static Application sApplication; 15 | 16 | public SharedPreferencesUtil() { 17 | } 18 | 19 | public static void init(Application application) { 20 | sApplication = application; 21 | } 22 | 23 | public static SharedPreferences getSharedPreference() { 24 | return sApplication.getSharedPreferences(name, 0); 25 | } 26 | 27 | public static void save(String key, String value) { 28 | Editor sharedData = getSharedPreference().edit(); 29 | sharedData.putString(key, value); 30 | sharedData.commit(); 31 | } 32 | 33 | public static void save(String key, boolean value) { 34 | Editor sharedData = getSharedPreference().edit(); 35 | sharedData.putBoolean(key, value); 36 | sharedData.commit(); 37 | } 38 | 39 | public static void save(String key, float value) { 40 | Editor sharedData = getSharedPreference().edit(); 41 | sharedData.putFloat(key, value); 42 | sharedData.commit(); 43 | } 44 | 45 | public static void save(String key, int value) { 46 | Editor sharedData = getSharedPreference().edit(); 47 | sharedData.putInt(key, value); 48 | sharedData.commit(); 49 | } 50 | 51 | public static void save(String key, long value) { 52 | Editor sharedData = getSharedPreference().edit(); 53 | sharedData.putLong(key, value); 54 | sharedData.commit(); 55 | } 56 | 57 | public static boolean getBoolean(String key, Boolean defValue) { 58 | SharedPreferences sharedPreference = getSharedPreference(); 59 | return sharedPreference.getBoolean(key, defValue); 60 | } 61 | 62 | public static float getFloat(String key, Float defValue) { 63 | SharedPreferences sharedPreference = getSharedPreference(); 64 | return sharedPreference.getFloat(key, defValue); 65 | } 66 | 67 | public static int getInt(String key, Integer defValue) { 68 | SharedPreferences sharedPreference = getSharedPreference(); 69 | return sharedPreference.getInt(key, defValue); 70 | } 71 | 72 | public static long getLong(String key, Long defValue) { 73 | SharedPreferences sharedPreference = getSharedPreference(); 74 | return sharedPreference.getLong(key, defValue); 75 | } 76 | 77 | public static String getString(String key, String defValue) { 78 | SharedPreferences sharedPreference = getSharedPreference(); 79 | return sharedPreference.getString(key, defValue); 80 | } 81 | 82 | public static void removeKey(String key) { 83 | Editor sharedData = getSharedPreference().edit(); 84 | sharedData.remove(key); 85 | sharedData.commit(); 86 | } 87 | 88 | public static void clearData() { 89 | Editor sharedData = getSharedPreference().edit(); 90 | sharedData.clear().commit(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/utils/threadpool/AppExecutors.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler.utils.threadpool; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import java.util.concurrent.Executor; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.ScheduledThreadPoolExecutor; 11 | import java.util.concurrent.ThreadPoolExecutor; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import androidx.annotation.NonNull; 15 | 16 | /** 17 | * Global executor pools for the whole application. 18 | *

19 | * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind 20 | * webservice requests). 21 | * 22 | * @author dhht 23 | */ 24 | public class AppExecutors { 25 | 26 | private final ExecutorService diskIO; 27 | private final ExecutorService networkIO; 28 | private final Executor mainThread; 29 | private final ScheduledExecutorService mExecutorService; 30 | 31 | private AppExecutors() { 32 | diskIO = new ThreadPoolExecutor(1, 33 | 1, 34 | 0L, 35 | TimeUnit.MILLISECONDS, 36 | new LinkedBlockingQueue(), 37 | new MyThreadFactory("DiskIoThreadExecutor")); 38 | 39 | networkIO = new ThreadPoolExecutor( 40 | 3, 41 | 3, 42 | 0L, 43 | TimeUnit.MILLISECONDS, 44 | new LinkedBlockingQueue(), 45 | new MyThreadFactory("NetworkIOExecutor")); 46 | mainThread = new MainThreadExecutor(); 47 | mExecutorService = new ScheduledThreadPoolExecutor(1, new MyThreadFactory("ScheduledExecutorService")); 48 | } 49 | 50 | public ExecutorService diskIO() { 51 | return diskIO; 52 | } 53 | 54 | public ExecutorService networkIO() { 55 | return networkIO; 56 | } 57 | 58 | public Executor mainThread() { 59 | return mainThread; 60 | } 61 | 62 | public ScheduledExecutorService scheduledExecutorService() { 63 | return mExecutorService; 64 | } 65 | 66 | private static class MainThreadExecutor implements Executor { 67 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 68 | 69 | @Override 70 | public void execute(@NonNull Runnable command) { 71 | mainThreadHandler.post(command); 72 | } 73 | } 74 | 75 | 76 | public static AppExecutors getInstance() { 77 | return Holder.sAppExecutors; 78 | } 79 | 80 | static class Holder { 81 | static AppExecutors sAppExecutors = new AppExecutors(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/hikvision/skinpeeler/utils/threadpool/MyThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler.utils.threadpool; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.concurrent.ThreadFactory; 8 | 9 | /** 10 | * @author HanPei 11 | * @date 2019/3/19 上午10:50 12 | */ 13 | public class MyThreadFactory implements ThreadFactory { 14 | 15 | private int counter; 16 | private String name; 17 | private List stats; 18 | 19 | public MyThreadFactory(String name) { 20 | counter = 0; 21 | this.name = name; 22 | stats = new ArrayList(); 23 | } 24 | 25 | @Override 26 | public Thread newThread(Runnable runnable) { 27 | Thread t = new Thread(runnable, name + "-Thread-" + counter); 28 | stats.add(String.format("Created thread %d with name %s on%s\n", t.getId(), t.getName(), new Date())); 29 | counter++; 30 | return t; 31 | } 32 | 33 | 34 | public String getStas() { 35 | StringBuffer buffer = new StringBuffer(); 36 | Iterator it = stats.iterator(); 37 | while (it.hasNext()) { 38 | buffer.append(it.next()); 39 | } 40 | return buffer.toString(); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/drawable/ic_bg.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | 13 | 17 | 18 | 25 | 26 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 50 | 51 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 75 | 76 | 82 | 83 | 88 | 89 | 90 | 91 | 92 | 99 | 100 | 105 | 106 | 110 | 111 | 120 | 121 | 122 | 123 | 124 | 132 | 133 | 138 | 139 | 144 | 145 | 146 | 147 | 148 | 157 | 158 | 163 | 164 | 169 | 170 | 171 | 172 | 173 | 181 | 182 | 188 | 189 | 196 | 197 | 198 | 199 | 200 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/pic_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xhdpi/pic_test.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #3F51B5 5 | #A292C7 6 | 7 | 8 | #f6f6f6 9 | #757575 10 | #ffffff 11 | 12 | #f4f4f4 13 | #2ca6cb 14 | #A292C7 15 | 16 | 17 | #1ba2ba 18 | #2ca6cb 19 | #d4237a 20 | 21 | #f2f5f6 22 | #ededed 23 | #f8f8f8 24 | #f6f6f6 25 | 26 | #ffffff 27 | #fffff0 28 | #ffffe0 29 | #ffff00 30 | #fffafa 31 | #fffaf0 32 | #fffacd 33 | #fff8dc 34 | #fff5ee 35 | #fff0f5 36 | #ffefd5 37 | #ffebcd 38 | #ffe4e1 39 | #ffe4c4 40 | #ffe4b5 41 | #ffdead 42 | #ffdab9 43 | #ffd700 44 | #ffc0cb 45 | #ffb6c1 46 | #ffa500 47 | #ffa07a 48 | #ff8c00 49 | #ff7f50 50 | #ff69b4 51 | #ff6347 52 | #ff4500 53 | #ff1493 54 | #ff00ff 55 | #ff00ff 56 | #ff0000 57 | #fdf5e6 58 | #fafad2 59 | #faf0e6 60 | #faebd7 61 | #fa8072 62 | #f8f8ff 63 | #f5fffa 64 | #f5f5f5 65 | #f5f5dc 66 | #f5deb3 67 | #f4a460 68 | #f0ffff 69 | #f0fff0 70 | #f0f8ff 71 | #f0e68c 72 | #f08080 73 | #eee8aa 74 | #ee82ee 75 | #e9967a 76 | #e6e6fa 77 | #e0ffff 78 | #deb887 79 | #dda0dd 80 | #dcdcdc 81 | #dc143c 82 | #db7093 83 | #daa520 84 | #da70d6 85 | #d8bfd8 86 | #d3d3d3 87 | #d3d3d3 88 | #d2b48c 89 | #d2691e 90 | #cd853f 91 | #cd5c5c 92 | #c71585 93 | #c0c0c0 94 | #bdb76b 95 | #bc8f8f 96 | #ba55d3 97 | #b8860b 98 | #b22222 99 | #b0e0e6 100 | #b0c4de 101 | #afeeee 102 | #adff2f 103 | #add8e6 104 | #a9a9a9 105 | #a9a9a9 106 | #a52a2a 107 | #a0522d 108 | #9932cc 109 | #98fb98 110 | #9400d3 111 | #9370db 112 | #90ee90 113 | #8fbc8f 114 | #8b4513 115 | #8b008b 116 | #8b0000 117 | #8a2be2 118 | #87cefa 119 | #87ceeb 120 | #808080 121 | #808080 122 | #808000 123 | #800080 124 | #800000 125 | #7fffd4 126 | #7fff00 127 | #7cfc00 128 | #7b68ee 129 | #778899 130 | #778899 131 | #708090 132 | #708090 133 | #6b8e23 134 | #6a5acd 135 | #696969 136 | #696969 137 | #66cdaa 138 | #6495ed 139 | #5f9ea0 140 | #556b2f 141 | #4b0082 142 | #48d1cc 143 | #483d8b 144 | #4682b4 145 | #4169e1 146 | #40e0d0 147 | #3cb371 148 | #32cd32 149 | #2f4f4f 150 | #2f4f4f 151 | #2e8b57 152 | #228b22 153 | #20b2aa 154 | #1e90ff 155 | #191970 156 | #00ffff 157 | #00ffff 158 | #00ff7f 159 | #00ff00 160 | #00fa9a 161 | #00ced1 162 | #00bfff 163 | #008b8b 164 | #008080 165 | #008000 166 | #006400 167 | #0000ff 168 | #0000cd 169 | #00008b 170 | #000080 171 | #000000 172 | #757575 173 | 174 | #555555 175 | 176 | 177 | 178 | #aa72d572 179 | #aa738ffe 180 | #aae84e40 181 | #00000000 182 | #25808080 183 | #f5f5f5 184 | 185 | 186 | -------------------------------------------------------------------------------- /app/src/main/res/values/custom_attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | skinpeeler 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/test/java/com/hikvision/skinpeeler/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinpeeler; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.61' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.4.2' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 06 09:34:35 GMT+08:00 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':skinlibrary' 2 | -------------------------------------------------------------------------------- /skin.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyhjh/Skinpeeler/af40458afd0767ab1d357eef2e0a921e8dab3e45/skin.apk -------------------------------------------------------------------------------- /skinlibrary/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Android template 30 | # Built application files 31 | *.apk 32 | *.ap_ 33 | 34 | # Files for the ART/Dalvik VM 35 | *.dex 36 | 37 | # Java class files 38 | *.class 39 | 40 | # Generated files 41 | bin/ 42 | gen/ 43 | out/ 44 | 45 | # Gradle files 46 | .gradle/ 47 | build/ 48 | 49 | # Local configuration file (sdk path, etc) 50 | local.properties 51 | 52 | # Proguard folder generated by Eclipse 53 | proguard/ 54 | 55 | # Log Files 56 | *.log 57 | 58 | # Android Studio Navigation editor temp files 59 | .navigation/ 60 | 61 | # Android Studio captures folder 62 | captures/ 63 | 64 | # IntelliJ 65 | *.iml 66 | .idea/workspace.xml 67 | .idea/tasks.xml 68 | .idea/gradle.xml 69 | .idea/assetWizardSettings.xml 70 | .idea/dictionaries 71 | .idea/libraries 72 | .idea/caches 73 | 74 | .idea/libraries/ 75 | .idea/.name 76 | .idea/compiler.xml 77 | .idea/copyright/profiles_settings.xml 78 | .idea/encodings.xml 79 | .idea/misc.xml 80 | .idea/modules.xml 81 | .idea/scopes/scope_settings.xml 82 | .idea/vcs.xml 83 | .classpath 84 | .project 85 | 86 | .idea 87 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 88 | .gradle 89 | 90 | # Keystore files 91 | # Uncomment the following line if you do not want to check your keystore files in. 92 | #*.jks 93 | 94 | # External native build folder generated in Android Studio 2.2 and later 95 | .externalNativeBuild 96 | 97 | # Google Services (e.g. APIs or Firebase) 98 | google-services.json 99 | 100 | # Freeline 101 | freeline.py 102 | freeline/ 103 | freeline_project_description.json 104 | 105 | # fastlane 106 | fastlane/report.xml 107 | fastlane/Preview.html 108 | fastlane/screenshots 109 | fastlane/test_output 110 | fastlane/readme.md 111 | 112 | .idea/fileTemplates/ 113 | fingersm2blibrary/src/androidTest/java/ 114 | fingersm2blibrary/src/main/java/com/example/fingersm2blibrary/ 115 | fingersm2blibrary/src/main/res/ 116 | fingersm2blibrary/src/test/ 117 | -------------------------------------------------------------------------------- /skinlibrary/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '28.0.3' 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | 31 | implementation 'androidx.appcompat:appcompat:1.0.2' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test:runner:1.2.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 35 | } 36 | -------------------------------------------------------------------------------- /skinlibrary/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /skinlibrary/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/SkinAttribute.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary; 2 | 3 | import android.util.AttributeSet; 4 | import android.view.View; 5 | 6 | import com.hikvision.skinlibrary.util.SkinThemeUitls; 7 | import com.hikvision.skinlibrary.view.SkinAttrParms; 8 | import com.hikvision.skinlibrary.view.SkinView; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * view属性处理 15 | * 16 | * @author hanpei 17 | * @version 1.0, 2019/9/6 18 | * @since 产品模块版本 19 | */ 20 | public class SkinAttribute { 21 | 22 | public static final List list = new ArrayList<>(); 23 | 24 | private ArrayList skinViews = new ArrayList(); 25 | 26 | static { 27 | list.add("background"); 28 | list.add("src"); 29 | list.add("textColor"); 30 | list.add("drawableLeft"); 31 | list.add("drawableTop"); 32 | list.add("drawableRight"); 33 | list.add("drawableBottom"); 34 | } 35 | 36 | /** 37 | * 保存view,分解属性,并对view进行换肤处理(当前皮肤可能不是默认时需要更换) 38 | * 39 | * @param view 40 | * @param attrs 41 | */ 42 | public void loadView(View view, AttributeSet attrs) { 43 | 44 | 45 | 46 | ArrayList skinAttrParms = new ArrayList<>(); 47 | for (int i = 0; i < attrs.getAttributeCount(); i++) { 48 | String attributeName = attrs.getAttributeName(i); 49 | if (list.contains(attributeName)) { 50 | String attributeValue = attrs.getAttributeValue(i); 51 | if (attributeValue.startsWith("#")) { 52 | continue; 53 | } 54 | int id; 55 | if (attributeValue.startsWith("?")) { 56 | int attrid = Integer.parseInt(attributeValue.substring(1)); 57 | id = SkinThemeUitls.getThemeResid(view.getContext(), new int[]{attrid})[0]; 58 | } else { 59 | id = Integer.parseInt(attributeValue.substring(1)); 60 | } 61 | if (id != 0) { 62 | SkinAttrParms attrParms = new SkinAttrParms(attributeName, id); 63 | skinAttrParms.add(attrParms); 64 | } 65 | } 66 | } 67 | //将View与之对应的可以动态替换的属性集合 放入 集合中 68 | if (!skinAttrParms.isEmpty()) { 69 | SkinView skinView = new SkinView(view, skinAttrParms); 70 | skinView.applySkin(); 71 | skinViews.add(skinView); 72 | } 73 | } 74 | 75 | /** 76 | * 进行换肤 77 | */ 78 | public void applySkin() { 79 | for (SkinView skinView : skinViews) { 80 | skinView.applySkin(); 81 | } 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/SkinChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary; 2 | 3 | /** 4 | * @author Tyhj 5 | * @date 2019-12-11 6 | * 换肤监听 7 | */ 8 | public interface SkinChangeListener { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/SkinFactory.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | import java.lang.reflect.Constructor; 12 | import java.util.HashMap; 13 | import java.util.Observable; 14 | import java.util.Observer; 15 | 16 | /** 17 | * Factory类,初始化view,并记录所有的view 18 | * 19 | * @author hanpei 20 | * @version 1.0, 2019/9/6 21 | * @since 产品模块版本 22 | */ 23 | public class SkinFactory implements LayoutInflater.Factory2, Observer { 24 | 25 | 26 | /** 27 | * 属性处理类 28 | */ 29 | SkinAttribute mSkinAttribute; 30 | 31 | /** 32 | * 保存view的构造方法 33 | */ 34 | private static final HashMap> sConstructorMap = 35 | new HashMap>(); 36 | 37 | static final Class[] mConstructorSignature = new Class[]{ 38 | Context.class, AttributeSet.class}; 39 | 40 | 41 | public final String[] a = new String[]{ 42 | "android.widget.", 43 | "android.view.", 44 | "android.webkit." 45 | }; 46 | 47 | public SkinFactory() { 48 | mSkinAttribute = new SkinAttribute(); 49 | } 50 | 51 | @Nullable 52 | @Override 53 | public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attributeSet) { 54 | View view = createViewFormTag(name, context, attributeSet); 55 | if (view == null) { 56 | view = createView(name, context, attributeSet); 57 | } 58 | if (view != null) { 59 | mSkinAttribute.loadView(view, attributeSet); 60 | } 61 | return view; 62 | } 63 | 64 | @Override 65 | public void update(Observable observable, Object o) { 66 | //接受到换肤请求 67 | mSkinAttribute.applySkin(); 68 | } 69 | 70 | 71 | /** 72 | * 参考LayoutInflater源码 73 | * 74 | * @param name 75 | * @param context 76 | * @param attrs 77 | * @return 78 | */ 79 | private View createViewFormTag(String name, Context context, AttributeSet attrs) { 80 | //包含自定义控件 81 | if (-1 != name.indexOf('.')) { 82 | return null; 83 | } 84 | View view = null; 85 | for (int i = 0; i < a.length; i++) { 86 | view = createView(a[i] + name, context, attrs); 87 | if (view != null) { 88 | break; 89 | } 90 | } 91 | return view; 92 | } 93 | 94 | 95 | /** 96 | * 参考LayoutInflater源码 97 | * 获取构造函数,创建view 98 | * 99 | * @param name 100 | * @param context 101 | * @param attrs 102 | * @return 103 | */ 104 | private View createView(String name, Context context, AttributeSet attrs) { 105 | Constructor constructor = findConstructor(context, name); 106 | try { 107 | return constructor.newInstance(context, attrs); 108 | } catch (Exception e) { 109 | } 110 | return null; 111 | } 112 | 113 | 114 | /** 115 | * 参考LayoutInflater源码 116 | * 通过反射获取View构造函数 117 | * 118 | * @param context 119 | * @param name 120 | * @return 121 | */ 122 | private Constructor findConstructor(Context context, String name) { 123 | Constructor constructor = sConstructorMap.get(name); 124 | if (null == constructor) { 125 | try { 126 | Class clazz = context.getClassLoader().loadClass 127 | (name).asSubclass(View.class); 128 | constructor = clazz.getConstructor(mConstructorSignature); 129 | sConstructorMap.put(name, constructor); 130 | } catch (Exception e) { 131 | e.printStackTrace(); 132 | } 133 | } 134 | return constructor; 135 | } 136 | 137 | 138 | @Nullable 139 | @Override 140 | public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) { 141 | return null; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/SkinManager.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary; 2 | 3 | import android.app.Application; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.content.res.AssetManager; 7 | import android.content.res.Resources; 8 | import android.text.TextUtils; 9 | 10 | import com.hikvision.skinlibrary.app.SkinActivityLifecycleCallbacks; 11 | import com.hikvision.skinlibrary.data.SkinPathDataSource; 12 | import com.hikvision.skinlibrary.util.SkinResourcess; 13 | 14 | import java.io.File; 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.lang.reflect.Method; 17 | import java.util.Observable; 18 | 19 | /** 20 | * @author hanpei 21 | * @version 1.0, 2019/9/6 22 | * @since 产品模块版本 23 | */ 24 | public class SkinManager extends Observable { 25 | 26 | private static Application mApplication; 27 | 28 | /** 29 | * 初始化 30 | * 31 | * @param application 32 | */ 33 | public static void init(Application application) { 34 | mApplication = application; 35 | application.registerActivityLifecycleCallbacks(new SkinActivityLifecycleCallbacks()); 36 | SkinPathDataSource.init(application); 37 | SkinResourcess.init(application); 38 | getInstance().loadSkin(SkinPathDataSource.getInstance().getSkinPath()); 39 | } 40 | 41 | 42 | /** 43 | * 进行换肤 44 | * 45 | * @param path 路径为插件包地址,为空则恢复默认 46 | */ 47 | public boolean loadSkin(String path) { 48 | if (TextUtils.isEmpty(path)) { 49 | return false; 50 | } 51 | 52 | File file = new File(path); 53 | if (!file.exists()) { 54 | return false; 55 | } 56 | try { 57 | AssetManager assetManager = AssetManager.class.newInstance(); 58 | Method method = assetManager.getClass().getMethod("addAssetPath", String.class); 59 | method.setAccessible(true); 60 | method.invoke(assetManager, path); 61 | Resources resources = mApplication.getResources(); 62 | Resources skinRes = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration()); 63 | 64 | //获取外部Apk(皮肤包) 包名 65 | PackageManager mPm = mApplication.getPackageManager(); 66 | PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager 67 | .GET_ACTIVITIES); 68 | String packageName = info.packageName; 69 | SkinResourcess.getInstance().applySkin(skinRes, packageName); 70 | //记录 71 | SkinPathDataSource.getInstance().saveSkinPath(path); 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | return false; 75 | } 76 | setChanged(); 77 | notifyObservers(); 78 | return true; 79 | } 80 | 81 | /** 82 | * 清除换肤,恢复默认 83 | */ 84 | public void clearSkin() { 85 | SkinPathDataSource.getInstance().saveSkinPath(null); 86 | SkinResourcess.getInstance().reset(); 87 | setChanged(); 88 | notifyObservers(); 89 | } 90 | 91 | 92 | /** 93 | * 获取资源文件的Resources 94 | * 95 | * @param path 96 | * @return 97 | */ 98 | public Resources getSkinRes(String path) { 99 | Resources resources = mApplication.getResources(); 100 | if (TextUtils.isEmpty(path)) { 101 | return resources; 102 | } else { 103 | try { 104 | AssetManager assetManager = null; 105 | assetManager = AssetManager.class.newInstance(); 106 | Method method = assetManager.getClass().getMethod("addAssetPath", String.class); 107 | method.setAccessible(true); 108 | method.invoke(assetManager, path); 109 | Resources skinRes = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration()); 110 | return skinRes; 111 | } catch (IllegalAccessException e) { 112 | e.printStackTrace(); 113 | } catch (InstantiationException e) { 114 | e.printStackTrace(); 115 | } catch (NoSuchMethodException e) { 116 | e.printStackTrace(); 117 | } catch (InvocationTargetException e) { 118 | e.printStackTrace(); 119 | } 120 | return resources; 121 | } 122 | 123 | 124 | } 125 | 126 | 127 | public static SkinManager getInstance() { 128 | return Holder.instance; 129 | } 130 | 131 | private SkinManager() { 132 | 133 | } 134 | 135 | private static class Holder { 136 | private static final SkinManager instance = new SkinManager(); 137 | } 138 | 139 | 140 | } 141 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/app/SkinActivityLifecycleCallbacks.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.app; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | 8 | import com.hikvision.skinlibrary.SkinFactory; 9 | import com.hikvision.skinlibrary.SkinManager; 10 | 11 | import java.lang.reflect.Field; 12 | import java.util.HashMap; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | import androidx.core.view.LayoutInflaterCompat; 17 | 18 | /** 19 | * 监听activity生命周期变化 20 | * 21 | * @author hanpei 22 | * @version 1.0, 2019/9/6 23 | * @since 产品模块版本 24 | */ 25 | public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { 26 | 27 | private HashMap mLayoutFactoryMap = new HashMap<>(); 28 | 29 | @Override 30 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { 31 | LayoutInflater layoutInflater = LayoutInflater.from(activity); 32 | //获得Activity的布局加载器 33 | try { 34 | //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory 35 | //如设置过源码会抛出异常 36 | //设置 mFactorySet 标签为false 37 | Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); 38 | field.setAccessible(true); 39 | field.setBoolean(layoutInflater, false); 40 | } catch (Exception e) { 41 | e.printStackTrace(); 42 | } 43 | 44 | SkinFactory skinLayoutFactory = new SkinFactory(); 45 | LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory); 46 | //注册观察者 47 | SkinManager.getInstance().addObserver(skinLayoutFactory); 48 | mLayoutFactoryMap.put(activity, skinLayoutFactory); 49 | 50 | } 51 | 52 | @Override 53 | public void onActivityStarted(@NonNull Activity activity) { 54 | 55 | } 56 | 57 | @Override 58 | public void onActivityResumed(@NonNull Activity activity) { 59 | 60 | } 61 | 62 | @Override 63 | public void onActivityPaused(@NonNull Activity activity) { 64 | 65 | } 66 | 67 | @Override 68 | public void onActivityStopped(@NonNull Activity activity) { 69 | 70 | } 71 | 72 | @Override 73 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) { 74 | 75 | } 76 | 77 | @Override 78 | public void onActivityDestroyed(@NonNull Activity activity) { 79 | //删除观察者 80 | SkinFactory skinLayoutFactory = mLayoutFactoryMap.remove(activity); 81 | SkinManager.getInstance().deleteObserver(skinLayoutFactory); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/data/SkinPathDataSource.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.data; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | 7 | /** 8 | * 皮肤管理 9 | * 10 | * @author hanpei 11 | * @version 1.0, 2019/9/6 12 | * @since 产品模块版本 13 | */ 14 | public class SkinPathDataSource { 15 | 16 | private static final String SKIN_SHARED = "skin-peeler-lib"; 17 | private static final String KEY_SKIN_PATH = "skin-path"; 18 | private final SharedPreferences mPref; 19 | 20 | private static Application mApplication; 21 | 22 | public static void init(Application application) { 23 | mApplication = application; 24 | } 25 | 26 | 27 | public void saveSkinPath(String path) { 28 | mPref.edit().putString(KEY_SKIN_PATH, path).apply(); 29 | } 30 | 31 | public String getSkinPath() { 32 | return mPref.getString(KEY_SKIN_PATH, null); 33 | } 34 | 35 | 36 | public static SkinPathDataSource getInstance() { 37 | return Holder.instance; 38 | } 39 | 40 | private SkinPathDataSource() { 41 | mPref = mApplication.getSharedPreferences(SKIN_SHARED, Context.MODE_PRIVATE); 42 | } 43 | 44 | private static class Holder { 45 | private static SkinPathDataSource instance = new SkinPathDataSource(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/util/SkinResourcess.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.util; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.content.res.ColorStateList; 6 | import android.content.res.Resources; 7 | import android.graphics.Typeface; 8 | import android.graphics.drawable.Drawable; 9 | import android.text.TextUtils; 10 | 11 | import com.hikvision.skinlibrary.SkinManager; 12 | 13 | /** 14 | * 资源获取类,从皮肤包里面获取资源 15 | * 16 | * @author hanpei 17 | * @version 1.0, 2019/9/6 18 | * @since 产品模块版本 19 | */ 20 | public class SkinResourcess { 21 | 22 | private static SkinResourcess skinManager; 23 | /** 24 | * 皮肤包的Resources 25 | */ 26 | private Resources skinResources; 27 | /** 28 | * APP的Resources 29 | */ 30 | private Resources appResources; 31 | /** 32 | * 皮肤包名 33 | */ 34 | private String mSkinPkgName; 35 | /** 36 | * 是否使用默认皮肤 37 | */ 38 | private boolean isDefaultSkin = true; 39 | 40 | private Context mContext; 41 | 42 | private SkinResourcess(Context context) { 43 | mContext = context; 44 | this.appResources = context.getResources(); 45 | } 46 | 47 | public static void init(Context context) { 48 | synchronized (SkinManager.class) { 49 | if (skinManager == null) { 50 | skinManager = new SkinResourcess(context); 51 | } 52 | } 53 | } 54 | 55 | public static SkinResourcess getInstance() { 56 | return skinManager; 57 | } 58 | 59 | /** 60 | * 设置皮肤 61 | * 62 | * @param resources 63 | * @param pkgName 64 | */ 65 | public void applySkin(Resources resources, String pkgName) { 66 | skinResources = resources; 67 | mSkinPkgName = pkgName; 68 | //是否使用默认皮肤 69 | isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null; 70 | } 71 | 72 | 73 | /** 74 | * 获取资源ID 75 | * 76 | * @param resId 77 | * @return 78 | */ 79 | public int getIdentifier(int resId) { 80 | if (isDefaultSkin) { 81 | return resId; 82 | } 83 | //在皮肤包中不一定就是 当前程序的 id 84 | //获取对应id 在当前的名称 colorPrimary 85 | //R.drawable.ic_launcher 86 | String resName = appResources.getResourceEntryName(resId); 87 | String resType = appResources.getResourceTypeName(resId); 88 | int skinId = skinResources.getIdentifier(resName, resType, mSkinPkgName); 89 | return skinId; 90 | } 91 | 92 | 93 | /** 94 | * 恢复默认皮肤 95 | */ 96 | public void reset() { 97 | skinResources = null; 98 | mSkinPkgName = ""; 99 | isDefaultSkin = true; 100 | } 101 | 102 | 103 | /** 104 | * 可惜获取到的还是本身的theme 105 | * 106 | * @param resId 107 | * @return 108 | */ 109 | public Resources.Theme getTheme(int resId) { 110 | if (isDefaultSkin) { 111 | return null; 112 | } 113 | int skinId = getIdentifier(resId); 114 | if (skinId == 0) { 115 | return null; 116 | } 117 | Resources.Theme newTheme = skinResources.newTheme(); 118 | Context context = null; 119 | try { 120 | context = mContext.createPackageContext(mSkinPkgName, 121 | Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); 122 | final Resources.Theme theme = context.getTheme(); 123 | if (theme != null) { 124 | newTheme.setTo(theme); 125 | } 126 | theme.applyStyle(resId, true); 127 | } catch (PackageManager.NameNotFoundException e) { 128 | e.printStackTrace(); 129 | } 130 | return null; 131 | } 132 | 133 | 134 | public int getColor(int resId) { 135 | if (isDefaultSkin) { 136 | return appResources.getColor(resId); 137 | } 138 | int skinId = getIdentifier(resId); 139 | if (skinId == 0) { 140 | return appResources.getColor(resId); 141 | } 142 | return skinResources.getColor(skinId); 143 | } 144 | 145 | 146 | public ColorStateList getColorStateList(int resId) { 147 | if (isDefaultSkin) { 148 | return appResources.getColorStateList(resId); 149 | } 150 | int skinId = getIdentifier(resId); 151 | if (skinId == 0) { 152 | return appResources.getColorStateList(resId); 153 | } 154 | return skinResources.getColorStateList(skinId); 155 | } 156 | 157 | public Drawable getDrawable(int resId) { 158 | //如果有皮肤 isDefaultSkin false 没有就是true 159 | if (isDefaultSkin) { 160 | return appResources.getDrawable(resId); 161 | } 162 | int skinId = getIdentifier(resId); 163 | if (skinId == 0) { 164 | return appResources.getDrawable(resId); 165 | } 166 | return skinResources.getDrawable(skinId); 167 | } 168 | 169 | 170 | /** 171 | * 可能是Color 也可能是drawable 172 | * 173 | * @return 174 | */ 175 | public Object getBackground(int resId) { 176 | String resourceTypeName = appResources.getResourceTypeName(resId); 177 | 178 | if (resourceTypeName.equals("color")) { 179 | return getColor(resId); 180 | } else { 181 | // drawable 182 | return getDrawable(resId); 183 | } 184 | } 185 | 186 | public String getString(int resId) { 187 | try { 188 | if (isDefaultSkin) { 189 | return appResources.getString(resId); 190 | } 191 | int skinId = getIdentifier(resId); 192 | if (skinId == 0) { 193 | return appResources.getString(skinId); 194 | } 195 | return skinResources.getString(skinId); 196 | } catch (Resources.NotFoundException e) { 197 | 198 | } 199 | return null; 200 | } 201 | 202 | public Typeface getTypeface(int resId) { 203 | String skinTypefacePath = getString(resId); 204 | if (TextUtils.isEmpty(skinTypefacePath)) { 205 | return Typeface.DEFAULT; 206 | } 207 | try { 208 | Typeface typeface; 209 | if (isDefaultSkin) { 210 | typeface = Typeface.createFromAsset(appResources.getAssets(), skinTypefacePath); 211 | return typeface; 212 | 213 | } 214 | typeface = Typeface.createFromAsset(skinResources.getAssets(), skinTypefacePath); 215 | return typeface; 216 | } catch (RuntimeException e) { 217 | } 218 | return Typeface.DEFAULT; 219 | } 220 | 221 | private static class Holder { 222 | 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/util/SkinThemeUitls.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | 6 | 7 | /** 8 | * @author hanpei 9 | */ 10 | public class SkinThemeUitls { 11 | public static int[] getThemeResid(Context context, int[] attrs) { 12 | int[] resIds = new int[]{attrs.length}; 13 | TypedArray typedArray = context.obtainStyledAttributes(attrs); 14 | for (int i = 0; i < typedArray.length(); i++) { 15 | int resourceId = typedArray.getResourceId(i, 0); 16 | resIds[i] = resourceId; 17 | } 18 | typedArray.recycle(); 19 | return resIds; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/view/SkinAttrParms.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.view; 2 | 3 | /** 4 | * View属性类 5 | * 6 | * @author hanpei 7 | */ 8 | public class SkinAttrParms { 9 | private String attrName; 10 | private int id; 11 | 12 | public SkinAttrParms(String attrName, int id) { 13 | this.attrName = attrName; 14 | this.id = id; 15 | } 16 | 17 | public String getAttrName() { 18 | return attrName; 19 | } 20 | 21 | public void setAttrName(String attrName) { 22 | this.attrName = attrName; 23 | } 24 | 25 | public int getId() { 26 | return id; 27 | } 28 | 29 | public void setId(int id) { 30 | this.id = id; 31 | } 32 | } -------------------------------------------------------------------------------- /skinlibrary/src/main/java/com/hikvision/skinlibrary/view/SkinView.java: -------------------------------------------------------------------------------- 1 | package com.hikvision.skinlibrary.view; 2 | 3 | import android.graphics.drawable.ColorDrawable; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.View; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import androidx.core.view.ViewCompat; 10 | 11 | import com.hikvision.skinlibrary.util.SkinResourcess; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author hanpei 17 | */ 18 | public class SkinView { 19 | View view; 20 | List parms; 21 | 22 | public SkinView(View view, List parms) { 23 | this.view = view; 24 | this.parms = parms; 25 | } 26 | 27 | public View getView() { 28 | return view; 29 | } 30 | 31 | public void setView(View view) { 32 | this.view = view; 33 | } 34 | 35 | /** 36 | * 加载属性 37 | */ 38 | public void applySkin() { 39 | for (SkinAttrParms parms : parms) { 40 | Drawable left = null, top = null, right = null, bottom = null; 41 | switch (parms.getAttrName()) { 42 | case "background": 43 | Object background = SkinResourcess.getInstance().getBackground(parms 44 | .getId()); 45 | //Color 46 | if (background instanceof Integer) { 47 | view.setBackgroundColor((Integer) background); 48 | } else { 49 | ViewCompat.setBackground(view, (Drawable) background); 50 | } 51 | break; 52 | case "src": 53 | background = SkinResourcess.getInstance().getBackground(parms 54 | .getId()); 55 | if (background instanceof Integer) { 56 | ((ImageView) view).setImageDrawable(new ColorDrawable((Integer) 57 | background)); 58 | } else { 59 | ((ImageView) view).setImageDrawable((Drawable) background); 60 | } 61 | break; 62 | case "textColor": 63 | ((TextView) view).setTextColor(SkinResourcess.getInstance().getColorStateList 64 | (parms.getId())); 65 | break; 66 | case "drawableLeft": 67 | left = SkinResourcess.getInstance().getDrawable(parms.getId()); 68 | break; 69 | case "drawableTop": 70 | top = SkinResourcess.getInstance().getDrawable(parms.getId()); 71 | break; 72 | case "drawableRight": 73 | right = SkinResourcess.getInstance().getDrawable(parms.getId()); 74 | break; 75 | case "drawableBottom": 76 | bottom = SkinResourcess.getInstance().getDrawable(parms.getId()); 77 | break; 78 | default: 79 | break; 80 | } 81 | if (null != left || null != right || null != top || null != bottom) { 82 | ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, 83 | bottom); 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /skinlibrary/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #3F51B5 5 | #aae84e40 6 | 7 | 8 | #f6f6f6 9 | #757575 10 | #ffffff 11 | 12 | #f4f4f4 13 | #2ca6cb 14 | #A292C7 15 | 16 | 17 | 18 | 19 | 20 | 21 | #1ba2ba 22 | #2ca6cb 23 | #d4237a 24 | 25 | #f2f5f6 26 | #ededed 27 | #f8f8f8 28 | #f6f6f6 29 | 30 | #ffffff 31 | #fffff0 32 | #ffffe0 33 | #ffff00 34 | #fffafa 35 | #fffaf0 36 | #fffacd 37 | #fff8dc 38 | #fff5ee 39 | #fff0f5 40 | #ffefd5 41 | #ffebcd 42 | #ffe4e1 43 | #ffe4c4 44 | #ffe4b5 45 | #ffdead 46 | #ffdab9 47 | #ffd700 48 | #ffc0cb 49 | #ffb6c1 50 | #ffa500 51 | #ffa07a 52 | #ff8c00 53 | #ff7f50 54 | #ff69b4 55 | #ff6347 56 | #ff4500 57 | #ff1493 58 | #ff00ff 59 | #ff00ff 60 | #ff0000 61 | #fdf5e6 62 | #fafad2 63 | #faf0e6 64 | #faebd7 65 | #fa8072 66 | #f8f8ff 67 | #f5fffa 68 | #f5f5f5 69 | #f5f5dc 70 | #f5deb3 71 | #f4a460 72 | #f0ffff 73 | #f0fff0 74 | #f0f8ff 75 | #f0e68c 76 | #f08080 77 | #eee8aa 78 | #ee82ee 79 | #e9967a 80 | #e6e6fa 81 | #e0ffff 82 | #deb887 83 | #dda0dd 84 | #dcdcdc 85 | #dc143c 86 | #db7093 87 | #daa520 88 | #da70d6 89 | #d8bfd8 90 | #d3d3d3 91 | #d3d3d3 92 | #d2b48c 93 | #d2691e 94 | #cd853f 95 | #cd5c5c 96 | #c71585 97 | #c0c0c0 98 | #bdb76b 99 | #bc8f8f 100 | #ba55d3 101 | #b8860b 102 | #b22222 103 | #b0e0e6 104 | #b0c4de 105 | #afeeee 106 | #adff2f 107 | #add8e6 108 | #a9a9a9 109 | #a9a9a9 110 | #a52a2a 111 | #a0522d 112 | #9932cc 113 | #98fb98 114 | #9400d3 115 | #9370db 116 | #90ee90 117 | #8fbc8f 118 | #8b4513 119 | #8b008b 120 | #8b0000 121 | #8a2be2 122 | #87cefa 123 | #87ceeb 124 | #808080 125 | #808080 126 | #808000 127 | #800080 128 | #800000 129 | #7fffd4 130 | #7fff00 131 | #7cfc00 132 | #7b68ee 133 | #778899 134 | #778899 135 | #708090 136 | #708090 137 | #6b8e23 138 | #6a5acd 139 | #696969 140 | #696969 141 | #66cdaa 142 | #6495ed 143 | #5f9ea0 144 | #556b2f 145 | #4b0082 146 | #48d1cc 147 | #483d8b 148 | #4682b4 149 | #4169e1 150 | #40e0d0 151 | #3cb371 152 | #32cd32 153 | #2f4f4f 154 | #2f4f4f 155 | #2e8b57 156 | #228b22 157 | #20b2aa 158 | #1e90ff 159 | #191970 160 | #00ffff 161 | #00ffff 162 | #00ff7f 163 | #00ff00 164 | #00fa9a 165 | #00ced1 166 | #00bfff 167 | #008b8b 168 | #008080 169 | #008000 170 | #006400 171 | #0000ff 172 | #0000cd 173 | #00008b 174 | #000080 175 | #000000 176 | #757575 177 | 178 | 179 | #aa72d572 180 | #aa738ffe 181 | #aae84e40 182 | #00000000 183 | #25808080 184 | #f5f5f5 185 | 186 | 187 | -------------------------------------------------------------------------------- /skinlibrary/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | skinLibrary 3 | 4 | -------------------------------------------------------------------------------- /skinlibrary/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 18 | 19 | 20 | --------------------------------------------------------------------------------