Android应用主题动态切换

首先介绍两个实现的方式,这两种也是网上大多数博客写过的:
1. 在应用中配置多种主题,让用户可以选择在不同的场景下切换不同的主题,最常用的就是白天和黑夜两种主题的切换。这种方式的缺点是,不够灵活,应用一旦发布,主题的种类就定死了,如果要新加主题,就需要重新发版。
2. 主题包。把应用中的资源打成一个.apk文件,这个文件中没有activity,只有资源文件,且和主应用中的相同资源的id一样,使用主题包中的资源替换主应用中的资源。主题包可以放在服务器端,通过网络进行下载。这样,我们不同重新发布应用就可以替换主题。但是一般的做法将主题包和主应用的sharedUserId设置为相同的,以便于他们之间可以相互访问,并且需要安装主题apk文件,使用createPackageContext(packageName, flags)方法获取主题包的Context,然后访问主题包中的资源。但是,对于用户来讲,安装一些不可见的文件是很不好的,所以用户体验上可能并不是很好。

今天,我们讲的是第二种方式的主题切换,但是我们的方案并不需要安装主题包。(其实这个方案是在网上看一哥们写的,只是这个哥们只贴出了部分代码,大概讲解了一下,我只是完善了一下,最后会贴出源码下载地址), 先上图说话:
效果图

我们大致的先说一下思路:
1. 首先需要去服务器下载主题包,然后存放到SD卡的指定路径下,当然,我们这里没有服务器,所以直接将两个主题的apk文件拷到sd卡的根目录下
2. 然后通过主题包的解析类是解析主题包,这个过程只有个异步的过程。解析成功后,发送一个广播去通知所有的activity去更换主题。
3. 每一个activity需要实现一个更新主题的接口ISkinUpdate来更新主题。
4. 将当前使用主题的路径保存在XML文件中,以便于下次启动应用是继续加载此主题。

好了,接下来,Let’s do it ~
首先,新建两个主题包,删除里边默认建的activity,在values下新建color.xml, 我们这里的主题只是简单改一下应用中的文字和背景颜色,所有颜色的值都在color.xml中。
theme包中的color.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="activity_bg">#E61ABD</color>
    <color name="text_bg">#FF3366</color>
    <color name="btn_bg">#FF3366</color>
    <color name="text_color">#FFFFFF</color>
</resources>

theme2包中的color.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="activity_bg">#4CAA75</color>
    <color name="text_bg">#DDE179</color>
    <color name="btn_bg">#AA79E1</color>
    <color name="text_color">#FFFFFF</color>
</resources>

将上面的两个主题包用签名打包成apk文件,并拷到SD卡根目录下。
接下来,新建我们的主应用,在values文件下同样新建一个color.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="activity_bg">#FFFFFF</color>
    <color name="text_bg">#EBF3FD</color>
    <color name="btn_bg">#EBF3FD</color>
    <color name="text_color">#000000</color>
</resources>

可以看到,我们主应用中的color.xml和主题包中的color的name都是一样的,只是值不一样。

自定义应用的application:

package com.example.update_theme;

import java.util.ArrayList;

import com.example.update_theme.SkinPackageManager.LoadSkinCallBack;

import android.app.Application;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;

public class SkinApplication extends Application {
    private static SkinApplication mInstance = null;

    public ArrayList<ISkinUpdate> mActivitys = new ArrayList<ISkinUpdate>();

    @Override
    public void onCreate() {
        // TODO Auto-generated method stub s
        super.onCreate();
        mInstance = this;
        SkinBroadCastReceiver.getInstance(this);
        String skinPath = SkinConfig.getInstance(this).getSkinResourcePath();
        if (!TextUtils.isEmpty(skinPath)) {
            // 如果已经换皮肤,那么第二次进来时,需要加载该皮肤
            SkinPackageManager.getInstance(this).loadSkinAsync(skinPath,
                    new LoadSkinCallBack() {
                        @Override
                        public void startloadSkin() {
                            Log.d("yzy", "startloadSkin");
                        }

                        @Override
                        public void loadSkinSuccess() {
                            Log.d("yzy", "loadSkinSuccess");
                            mInstance.sendBroadcast(new Intent(
                                    SkinBroadCastReceiver.SKIN_ACTION));
                        }

                        @Override
                        public void loadSkinFail() {
                            Log.d("yzy", "loadSkinFail");
                        }
                    });
        }

        SkinBroadCastReceiver.registerBroadCastReceiver();

    }

    public static SkinApplication getInstance() {
        return mInstance;
    }

    @Override
    public void onTerminate() {
        // TODO Auto-generated method stub
        SkinBroadCastReceiver.unregisterBroadCastReceiver();
        super.onTerminate();
    }

    /**
     * 更新所有界面的主题
     */
    public void changeSkin() {
        for (ISkinUpdate skin : mActivitys) {
            skin.updateTheme();
        }
    }
}

在SkinApplication 中主要是注册了一个广播接收器,以便于主题解析成功后去通过所有activity更换主题,还有就是检查我们XML文件中是否存有主题的路径,如果有,说明这个主题就是当前主题,去加载这个主题。

接下来我们主题的解析类SkinPackageManager,我们看这个类中最主要的方法:

    public void loadSkinAsync(String dexPath, final LoadSkinCallBack callback) {
        new AsyncTask<String, Void, Resources>() {

            protected void onPreExecute() {
                if (callback != null) {
                    callback.startloadSkin();
                }
            };

            @Override
            protected Resources doInBackground(String... params) {
                try {
                    if (params.length == 1) {
                        String dexPath_tmp = params[0];
                        PackageManager mPm = mContext.getPackageManager();
                        PackageInfo mInfo = mPm.getPackageArchiveInfo(
                                dexPath_tmp, PackageManager.GET_ACTIVITIES);
                        mPackageName = mInfo.packageName;

                        AssetManager assetManager = AssetManager.class
                                .newInstance();
                        Method addAssetPath = assetManager.getClass()
                                .getMethod("addAssetPath", String.class);
                        addAssetPath.invoke(assetManager, dexPath_tmp);

                        Resources superRes = mContext.getResources();
                        Resources skinResource = new Resources(assetManager,
                                superRes.getDisplayMetrics(),
                                superRes.getConfiguration());
                        SkinConfig.getInstance(mContext).setSkinResourcePath(
                                dexPath_tmp);
                        return skinResource;
                    }
                    return null;
                } catch (Exception e) {
                    return null;
                }

            };

            protected void onPostExecute(Resources result) {
                mResources = result;

                if (callback != null) {
                    if (mResources != null) {
                        callback.loadSkinSuccess();
                    } else {
                        callback.loadSkinFail();
                    }
                }
            };

        }.execute(dexPath);
    }

使用异步去解析主题,重点都在doInBackground方法中,在此方法中,我们通过反射去得到了主题包的Resouces对象,之后,我们就可以通过这个对象去访问主题包中的资源了。

继续来看MainActivity文件:

public class MainActivity extends Activity implements ISkinUpdate,
        OnClickListener {

    private RelativeLayout mLayout;
    private TextView mTextView;
    private Button mButton, mTheme1Btn, mTheme2Btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SkinApplication.getInstance().mActivitys.add(this);

        mLayout = (RelativeLayout) findViewById(R.id.id_layout);
        mTextView = (TextView) findViewById(R.id.id_text1);
        mButton = (Button) findViewById(R.id.id_btn);
        mTheme1Btn = (Button) findViewById(R.id.id_btn_theme1);
        mTheme2Btn = (Button) findViewById(R.id.id_btn_theme2);
        mButton.setOnClickListener(this);
        mTheme1Btn.setOnClickListener(this);
        mTheme2Btn.setOnClickListener(this);

    }

    /**
     * 解析主题包
     * @param themeName 主题名
     * @return
     */
    private boolean parseTheme(String themeName) {
        File skin = new File(Environment.getExternalStorageDirectory(),
                themeName + ".apk");
        if (skin.exists()) {
            SkinPackageManager.getInstance(MainActivity.this).loadSkinAsync(
                    skin.getAbsolutePath(), new LoadSkinCallBack() {
                        @Override
                        public void startloadSkin() {
                            Log.d("yzy", "startloadSkin");
                        }

                        @Override
                        public void loadSkinSuccess() {
                            Log.d("yzy", "loadSkinSuccess");
                            MainActivity.this.sendBroadcast(new Intent(
                                    SkinBroadCastReceiver.SKIN_ACTION));
                        }

                        @Override
                        public void loadSkinFail() {
                            Log.d("yzy", "loadSkinFail");
                        }
                    });
            return true;
        }
        return false;
    }

    @Override
    protected void onResume() {
        updateTheme();
        super.onResume();
    }

    @Override
    protected void onDestroy() {
        SkinApplication.getInstance().mActivitys.remove(this);
        super.onDestroy();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.id_btn:
            Intent intent = new Intent(MainActivity.this, OtherActivity.class);
            startActivity(intent);
            break;
        case R.id.id_btn_theme1:
            if (!parseTheme("theme")){
                Toast.makeText(MainActivity.this, "该主题不存在", Toast.LENGTH_SHORT).show();
            }
            break;
        case R.id.id_btn_theme2:
            if (!parseTheme("theme2")){
                Toast.makeText(MainActivity.this, "该主题不存在", Toast.LENGTH_SHORT).show();
            }
            break;

        }

    }

    @Override
    public void updateTheme() {
        Resources mResource = SkinPackageManager.getInstance(this).mResources;
        if (mResource != null) {
            try {
                String packageName = SkinPackageManager.getInstance(this).mPackageName;
                Log.d("yzy", "start and mResource is null-->"
                        + (mResource == null));
                int activityBackgroud = mResource.getIdentifier("activity_bg", "color",
                        packageName);
                int textBackgroud = mResource.getIdentifier("text_bg", "color",
                        packageName);
                int TextColor = mResource.getIdentifier("text_color", "color",
                        packageName);
                int ButtonBackgroud = mResource.getIdentifier("btn_bg", "color",
                        packageName);
                mLayout.setBackgroundColor(mResource.getColor(activityBackgroud));

                mTextView.setBackgroundColor(mResource.getColor(textBackgroud));
                mTextView.setTextColor(mResource.getColor(TextColor));

                mButton.setBackgroundColor(mResource.getColor(ButtonBackgroud));
                mButton.setTextColor(mResource.getColor(TextColor));
                mTheme1Btn.setBackgroundColor(mResource.getColor(ButtonBackgroud));
                mTheme1Btn.setTextColor(mResource.getColor(TextColor));
                mTheme2Btn.setBackgroundColor(mResource.getColor(ButtonBackgroud));
                mTheme2Btn.setTextColor(mResource.getColor(TextColor));
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }
}

在parseTheme(String themeName)方法中我们调用SkinPackageManager类中的loadSkinAsync这个方法去解析指定路径下的主题包,解析成功后,在回调接口的loadSkinSuccess()方法中发送一个广播,通知所有界面更新。
注意,每一个activity需要实现ISkinUpdate接口,此接口中只有一个方法public void updateTheme(),在解析主题成功后,在这个方法中我们去设置需要更改的控件的颜色值。

到此,基本所有的功能就讲解完了,以下是源码:
完整源码下载
说明:请将解压后三个项目导入eclipse中,然后将theme和theme2打包成apk,并拷到sd卡根目录下,否则切换主题不会成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值