Android插件化开发之动态加载本地皮肤包进行换肤
前言: 本文主要讲解如何用开源换肤框架 android-skin-loader-lib来实现加载本地皮肤包文件进行换肤,具体可自行参考框架原理进行更改!
实现:
1. https://github.com/fengjundev/Android-Skin-Loader 框架地址,下载文件,根据自己需要进行删减得到自己的文件. 我的文件主要有android-skin-loader-lib依赖包文件,android-skin-loader-skin自己的皮肤包文件,其它则可以不要
2.创建项目,导入依赖包和皮肤包文件
注意点 :
a. 依赖包,皮肤包的build.gradle里的版本相关需要和自己app的build.gradle里的一致!
b.依赖包,皮肤包的app name要和自己app的app name一直!
**c.**app里要替换的颜色,图片,在皮肤包目录也要有,且id要一直,才可以找到,并更换!
d.要换肤的界面需继承换肤依赖包里的base里的相关界面元素,当前有activity,fragment,fragmentActivity,在布局文件需添加相关标识,如下:
...
xmlns:skin="http://schemas.android.com/android/skin"
...
<TextView
...
skin:enable="true"
... />
e. 需要在自己App的Application文件中设置初始化:
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
f.自定义view换肤需要实现IDynamicNewView 接口:
public interface IDynamicNewView {
void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
}
3.编译皮肤包文件
因为我们的皮肤包文件是android-skin-loader-skin文件,它就是一个没有Java文件,只有资源文件的项目,所以我们要生成皮肤包文件也是由它生成的,就是一个apk啦,但是我们需要对它修改后缀名,防止用户点击.
当我们制定了皮肤包的相关文件时,修改皮肤包文件里的build.gradle,把编译生成的Apk文件进行重命名,并放到主项目的assets目录下.build.gradle目录如下:
apply plugin: 'com.android.application'
def skinName = "BlackFantacy.skin"
android {
signingConfigs {
config {
keyAlias 'fengjun'
keyPassword 'fengjun'
storeFile file('keystore.key')
storePassword 'fengjun'
}
}
compileSdkVersion 23
buildToolsVersion '25.0.0'
defaultConfig {
applicationId "com.example.android_skin_laoder_skin"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
}
final def TARGET_SKIN_DIR = '../app/src/main/assets/'
gradle.projectsEvaluated {
assembleRelease.doLast {
println("=====================assembleRelease.doLast.begin.=========================")
def dir = new File(TARGET_SKIN_DIR)
if (!dir.exists()) {
dir.mkdirs()
}
def f = new File(TARGET_SKIN_DIR + skinName)
if (f.exists()) {
f.delete()
}
copy {
from('build/outputs/apk')
into(TARGET_SKIN_DIR)
include '*.apk'
exclude '**/*-unaligned.apk'
rename ('android-skin-loader-skin-release.apk', skinName)
}
println("=====================assembleRelease.doLast success.=========================")
}
assembleDebug.doLast {
println("=====================assembleDebug.doLast.begin.=========================")
def dir = new File(TARGET_SKIN_DIR)
if (!dir.exists()) {
dir.mkdirs()
}
def f = new File(TARGET_SKIN_DIR + skinName)
if (f.exists()) {
f.delete()
}
copy {
from('build/outputs/apk')
into(TARGET_SKIN_DIR)
include '*.apk'
exclude '**/*-unaligned.apk'
rename ('android-skin-loader-skin-debug.apk', skinName)
}
println("=====================assembleDebug.doLast success.=========================")
}
}
然后进行编译:
当编译完成时这时我们的主项目assets目录下会出现一个BlackFantacy.skin的文件,这就是我们要换肤的文件:
4.实现换肤
创建Application文件:
package com.example.administrator.replaceappskin;
import android.app.Application;
import cn.feng.skin.manager.loader.SkinManager;
/**
* Created by Administrator on 2017/5/19.
*/
public class ReplaceAppSkinApplication extends Application {
public void onCreate() {
super.onCreate();
initSkinLoader();
}
/**
* Must call init first
*/
private void initSkinLoader() {
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}
在注册文件添加指定的application文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.administrator.replaceappskin">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<application
android:name=".ReplaceAppSkinApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
在要实现的activity或者fragment,fragmentActivity继承换肤的基类,我这里是把assets里的换肤文件写入sd卡中,生成自己的目录,方便以后置换,activity代码如下:
package com.example.administrator.replaceappskin;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import cn.feng.skin.manager.base.BaseActivity;
import cn.feng.skin.manager.listener.ILoaderListener;
import cn.feng.skin.manager.loader.SkinManager;
import cn.feng.skin.manager.util.L;
public class MainActivity extends BaseActivity {
private static final String DATAPATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "我的主题包";
/**
* 在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录。
*/
private static final String tessdata = DATAPATH + File.separator + "主题";
/**
* TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
*/
private static final String DEFAULT_LANGUAGE = "BlackFantacy";
/**
* assets中的文件名
*/
private static final String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".skin";
/**
* 保存到SD卡中的完整文件名
*/
private static final String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;
private TextView titleText;
private Button setOfficalSkinBtn;
private Button setNightSkinBtn;
private boolean isOfficalSelected = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initSkinData();
initView();
}
private void initSkinData() {
//如果存在就删掉
File f = new File(LANGUAGE_PATH);
if (f.exists()) {
f.delete();
}
if (!f.exists()) {
File p = new File(f.getParent());
if (!p.exists()) {
p.mkdirs();
}
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
InputStream is = null;
OutputStream os = null;
try {
is = this.getAssets().open(DEFAULT_LANGUAGE_NAME);
File file = new File(LANGUAGE_PATH);
os = new FileOutputStream(file);
byte[] bytes = new byte[2048];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void initView() {
titleText = (TextView) findViewById(R.id.title_text);
titleText.setText("设置皮肤");
setOfficalSkinBtn = (Button) findViewById(R.id.set_default_skin);
setNightSkinBtn = (Button) findViewById(R.id.set_night_skin);
isOfficalSelected = !SkinManager.getInstance().isExternalSkin();
if (isOfficalSelected) {
setOfficalSkinBtn.setText("官方默认(当前)");
setNightSkinBtn.setText("黑色幻想");
} else {
setNightSkinBtn.setText("黑色幻想(当前)");
setOfficalSkinBtn.setText("官方默认");
}
setNightSkinBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onSkinSetClick();
}
});
setOfficalSkinBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onSkinResetClick();
}
});
}
protected void onSkinResetClick() {
if (!isOfficalSelected) {
SkinManager.getInstance().restoreDefaultTheme();
Toast.makeText(getApplicationContext(), "切换成功", Toast.LENGTH_SHORT).show();
setOfficalSkinBtn.setText("官方默认(当前)");
setNightSkinBtn.setText("黑色幻想");
isOfficalSelected = true;
}
}
private void onSkinSetClick() {
if (!isOfficalSelected) return;
File skin = new File(LANGUAGE_PATH);
if (skin == null || !skin.exists()) {
Toast.makeText(getApplicationContext(), "请检查" + LANGUAGE_PATH + "是否存在", Toast.LENGTH_SHORT).show();
return;
}
SkinManager.getInstance().load(skin.getAbsolutePath(),
new ILoaderListener() {
@Override
public void onStart() {
L.e("startloadSkin");
}
@Override
public void onSuccess() {
L.e("loadSkinSuccess");
Toast.makeText(getApplicationContext(), "切换成功", Toast.LENGTH_SHORT).show();
setNightSkinBtn.setText("黑色幻想(当前)");
setOfficalSkinBtn.setText("官方默认");
isOfficalSelected = false;
}
@Override
public void onFailed() {
L.e("loadSkinFail");
Toast.makeText(getApplicationContext(), "切换失败", Toast.LENGTH_SHORT).show();
}
});
}
}
相关的方法和原理可以去看看它的方式,这里不细讲!
效果如下: