├── .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 | 
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 | 
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 | 
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 extends View> 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 extends View> findConstructor(Context context, String name) {
123 | Constructor extends View> constructor = sConstructorMap.get(name);
124 | if (null == constructor) {
125 | try {
126 | Class extends View> 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 |
--------------------------------------------------------------------------------