天天看点

Android插件化开发之动态加载本地皮肤包进行换肤

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 
    buildToolsVersion '25.0.0'
    defaultConfig {
        applicationId "com.example.android_skin_laoder_skin"
        minSdkVersion 
        targetSdkVersion 
        versionCode 
        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.=========================")
    }
}
           

然后进行编译:

Android插件化开发之动态加载本地皮肤包进行换肤
Android插件化开发之动态加载本地皮肤包进行换肤

当编译完成时这时我们的主项目assets目录下会出现一个BlackFantacy.skin的文件,这就是我们要换肤的文件:

Android插件化开发之动态加载本地皮肤包进行换肤

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[];
            int len = ;
            while ((len = is.read(bytes)) != -) {
                os.write(bytes, , 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();
                    }
                });
    }
}

           

相关的方法和原理可以去看看它的方式,这里不细讲!

效果如下:

Android插件化开发之动态加载本地皮肤包进行换肤

Demo下载地址