Android 換膚功能的實現(Apk插件方式)


技術:Android+Java
 

概述

由於Android 沒有提供一套統一的換膚機制,我猜可能是因為國外更注重功能和體驗的原因 所以國內如果要做一個漂亮的換膚方案,需要自己去實現。 目前換膚的方法大概有三種方案: (1)把皮膚資源文件內置於應用程序Apk的資源目錄下,這種方案最簡單,但是導致apk安裝包比會比比較大,而且不好管理 (2)將皮膚資源文件打包成zip的資源文件方式提供,該方法也比較多被采用。 (3)將皮膚圖片資源以獨立的Apk安裝包的方式提供,做成插件化的方式。便於管理。

詳細

一、概述

由於Android 沒有提供一套統一的換膚機制,我猜可能是因為國外更注重功能和體驗的原因

所以國內如果要做一個漂亮的換膚方案,需要自己去實現。

目前換膚的方法大概有三種方案:

(1)把皮膚資源文件內置於應用程序Apk的資源目錄下,這種方案最簡單,但是導致apk安裝包比會比比較大,而且不好管理

(2)將皮膚資源文件打包成zip的資源文件方式提供,該方法也比較多被采用。

(3)將皮膚圖片資源以獨立的Apk安裝包的方式提供,做成插件化的方式。便於管理。

本文主要討論第三種實現。

二、效果演示圖

首先看看實現的效果吧:

01.gif

 

三、換膚功能的實現

現在把 皮膚資源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后,讓自己不可以打開,且不生成桌面圖標,

如下圖:

02.gif

其實這里有個小竅門就是 不設置其

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>

四、項目代碼目錄結構圖:

03.jpg

注:本文著作權歸作者,由demo大師發表,拒絕轉載,轉載需要作者授權

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM