1、概述html
因爲Android 沒有提供一套統一的換膚機制,我猜多是由於國外更注重功能和體驗的緣由java
因此國內若是要作一個漂亮的換膚方案,須要本身去實現。android
目前換膚的方法大概有三種方案:api
(1)把皮膚資源文件內置於應用程序Apk的資源目錄下,這種方案最簡單,可是致使apk安裝包比會比比較大,並且很差管理app
(2)將皮膚資源文件打包成zip的資源文件方式提供,該方法也比較多被採用。ide
(3)將皮膚圖片資源以獨立的Apk安裝包的方式提供,作成插件化的方式。便於管理。佈局
本文主要討論第三種實現。this
2、效果演示spa
首先看看實現的效果吧:.net
3、換膚功能的實現
如今把 皮膚資源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 int CONTEXT_IGNORE_SECURITY Added in API level 1 Flag 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 int CONTEXT_INCLUDE_CODE Added in API level 1 Flag 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 class loader. 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後,讓本身不能夠打開,且不生成桌面圖標,
以下圖:
其實這裏有個小竅門就是 不設置其
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張背景圖片爲例)
package com.czm.myskindemo; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; import android.widget.Button; import java.util.List; public class MainActivity extends Activity { private Button mButton; private Context mSkinContext; private int[] mResId; private int mCount = 0; private View mTopbar; private View mBottomBar; private List<View> mSkinWidgetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initSkinContext(); setListener(); } private void initSkinContext() { 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 void setListener() { mButton = (Button)findViewById(R.id.btn_install_skin); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(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>
4、源碼下載: