Android 换肤功能的实现(Apk插件方式)

摘要:
将皮肤图片资源以独立的Apk安装包的方式提供,做成插件化的方式。

一、概述

由于Android 没有提供一套统一的换肤机制,我猜可能是因为国外更注重功能和体验的原因

所以国内如果要做一个漂亮的换肤方案,需要自己去实现。

目前换肤的方法大概有三种方案:

(1)把皮肤资源文件内置于应用程序Apk的资源目录下,这种方案最简单,但是导致apk安装包比会比比较大,而且不好管理

(2)将皮肤资源文件打包成zip的资源文件方式提供,该方法也比较多被采用。

(3)将皮肤图片资源以独立的Apk安装包的方式提供,做成插件化的方式。便于管理。

本文主要讨论第三种实现。

二、效果演示

首先看看实现的效果吧:

Android 换肤功能的实现(Apk插件方式)第1张

三、换肤功能的实现

现在把 皮肤资源apk叫做皮肤Apk,把需要换肤的应用程序叫做主程序APK吧。

基本原理主要是:

(1)新建一个Android项目-MySkin,把皮肤资源文件放在把项目的资源目录下,改包名为:com.czm.myskin

(2)新建一个主程序Apk应用Android项目-MySkinDemo,通过皮肤Apk的包名,获取其Context:

方法如下:

mSkinContext= this.getApplicationContext().createPackageContext("com.czm.myskin",
                    Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);

为什么要用Context.CONTEXT_IGNORE_SECURITY,且看api文档吧:

public static final intCONTEXT_IGNORE_SECURITY

Added in API level 1Flag for use with createPackageContext(String, int): ignore any security restrictions on the Context being requested, allowing it to always be loaded. For use with CONTEXT_INCLUDE_CODE to allow code to be loaded into a process even when it isn't safe to do so. Use with extreme care!
Constant Value: 2 (0x00000002)
public static final intCONTEXT_INCLUDE_CODE

Added in API level 1Flag for use with createPackageContext(String, int): include the application code with the context. This means loading code into the caller's process, so that getClassLoader() can be used to instantiate the application's classes. Setting this flags imposes security restrictions on what application context you can access; if the requested application can not be safely loaded into your process, java.lang.SecurityException will be thrown. If this flag is not set, there will be no restrictions on the packages that can be loaded, but getClassLoader() will always return the default system classloader.

Constant Value: 1 (0x00000001)

拿到皮肤Apk的context后,我们就可以拿到里面的皮肤资源文件和图片了

当然了,这里为了实现运行在同一个进程,需要将皮肤Apk-MySkin 的android:sharedUserId 这个属性配置为 主程序MySkinDemo的包名:即:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.czm.myskin"android:sharedUserId="com.czm.myskindemo"
    >

至于android:sharedUserId 这个的作用和意义,还是看官方api文档吧:

android:sharedUserId
The name of a Linux user ID that will be shared with other applications. By default, Android assigns each application its own unique user ID. However, if this attribute is set to the same value for two or more applications, they will all share the same ID — provided that they are also signed by the same certificate. Application with the same user ID can access each other's data and, if desired, run in the same process.

(3)为了让用户无感知,需要安装后皮肤APk后,让自己不可以打开,且不生成桌面图标,

如下图:

Android 换肤功能的实现(Apk插件方式)第2张

其实这里有个小窍门就是 不设置其

category的 Launcher : 即 把 
<intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

这个过滤器去掉即可

如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.czm.myskin"android:sharedUserId="com.czm.myskindemo"
    >

    <application
        android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"android:label="@string/app_name"
           >
        </activity>
    </application>

</manifest>

到此为止,Apk插件换肤功能方案已经完成实现。

下面是主程序的完整实例代码:(这里以换 2张背景图片为例)

packagecom.czm.myskindemo;

importandroid.app.Activity;
importandroid.content.Context;
importandroid.content.pm.PackageManager;
importandroid.graphics.drawable.Drawable;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.widget.Button;

importjava.util.List;

public class MainActivity extendsActivity {

    privateButton mButton;
    privateContext mSkinContext;
    private int[] mResId;
    private int mCount = 0;
    privateView mTopbar;
    privateView mBottomBar;
    private List<View>mSkinWidgetList;
    @Override
    protected voidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initSkinContext();
        setListener();
    }
    private voidinitSkinContext() {
        mResId = new int[]{
                R.drawable.bg_topbar0,
                R.drawable.bg_topbar1,
                R.drawable.bg_topbar2,
        };
        try{
            mSkinContext= this.getApplicationContext().createPackageContext("com.czm.myskin",
                    Context.CONTEXT_IGNORE_SECURITY |Context.CONTEXT_INCLUDE_CODE);

        } catch(PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        mTopbar =findViewById(R.id.tv_topbar);
        mBottomBar =findViewById(R.id.tv_bottombar);
    }

    private voidsetListener() {
        mButton =(Button)findViewById(R.id.btn_install_skin);
        mButton.setOnClickListener(newView.OnClickListener() {
            @Override
            public voidonClick(View view) {
                Drawable drawable =mSkinContext.getResources().getDrawable(mResId[mCount]);
                mTopbar.setBackground(drawable);
                mBottomBar.setBackground(drawable);
                mCount++;
                if(mCount >2){
                    mCount = 0;
                }
            }
        });
    }
}

其对于的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.czm.myskindemo.MainActivity"tools:showIn="@layout/activity_main">

    <TextView
        android:id="@+id/tv_topbar"android:layout_width="match_parent"android:layout_height="50dp"android:layout_alignParentTop="true"android:background="#000"android:gravity="center"android:textColor="#FFF"android:text="Top Bar" />
    <TextView
        android:id="@+id/tv_bottombar"android:layout_width="match_parent"android:layout_height="50dp"android:layout_alignParentBottom="true"android:textColor="#FFF"android:gravity="center"android:background="#000"android:text="Bottom Bar" />
    <Button
        android:id="@+id/btn_install_skin"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:text="Install Skin"/>
</RelativeLayout>

四、源码下载:

源码下载:http://www.demodashi.com/demo/14679.html

真题园网http://www.zhentiyuan.com

免责声明:文章转载自《Android 换肤功能的实现(Apk插件方式)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇按键中断消抖--2Web Magic 总体架构下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

ASP.NET Core WebApi + EF Core(实现增删改查,使用Swagger测试API)

EF有两个不同版本,即Entity Framework Core 和 Entity Framework 6 EF Core:轻量级,可扩展,跨平台,参考EF6,全新平台,学习曲线小,引入一些新功能(批量删除) EF 6 :笨重,稳定,微软已经不打算进行大版本升级,无法跨平台。 如何实现EF Core功能 1、创建ASP.NET Core Web应用程序 ...

多渠道打包工具Walle源码分析

一、背景 首先了解多渠道打包工具Walle之前,我们需要先明确一个概念,什么是渠道包。 我们要知道在国内有无数大大小小的APP Store,每一个APP Store就是一个渠道。当我们把APP上传到APP Store上的时候,我们如何知道用户在那个渠道下载我们的APP呢?如果单凭渠道供应商自己给的话,那无疑会带来不可知的损失,当然除了这个原因,我们还有别的...

获取Android设备唯一标识码

概述 有时需要对用户设备进行标识,所以希望能够得到一个稳定可靠并且唯一的识别码。虽然Android系统中提供了这样设备识别码,但是由于Android系统版本、厂商定制系统中的Bug等限制,稳定性和唯一性并不理想。而通过其他硬件信息标识也因为系统版本、手机硬件等限制存在不同程度的问题。 下面收集了一些“有能力”或“有一定能力”作为设备标识的串码。 DE...

android开发学习 ------- 自定义View 圆 ,其点击事件 及 确定当前view的层级关系

我需要实现下面的效果:   参考文章:https://blog.csdn.net/halaoda/article/details/78177069 涉及的View事件分发机制 https://www.jianshu.com/p/38015afcdb58  (最全面的原理性文章)    https://www.jianshu.com/p/e99b5e8bd6...

Android混淆、反编译以及反破解的简单回顾

=========================================================================虽然反编译很简单,也没下面说的那么复杂,不过还是转了过来。 Android混淆、反编译以及反破解的简单回顾          搜索下,发现文章相关文档好多好多。就简单点,不赘述了==   一、Android...

ZeroMQ示例(C/C++/PHP)详解三种模式

源自:https://blog.csdn.net/qq_16836151/article/details/521081521、应答模式2、均衡分配模式(推拉模式)3、发布订阅模式(天气预报) 提问-回答 让我们从简单的代码开始,一段传统的Hello World程序。我们会创建一个客户端和一个服务端,客户端发送Hello给服务端,服务端返回World。下文是...