Android实现夜间模式的方法(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。
最近整理了几篇在乐视实习时写的文章,都是一些简单的技术调研,Android夜间模式是当时做技术分享的内容,供大家参考,欢迎一起讨论~

一.夜间模式的简单介绍

随着人们夜间阅读的需求越来越扩大化,许多应用也都在设置中增加了“夜间模式”这个选项。关于夜间模式的实现,有很多种方法。在此介绍几种比较常见的夜间模式的实现方法。
首先,夜间模式是Android换肤的一种,如果应用中的夜间模式是多种皮肤的其中之一,则可以从apk外部加载皮肤资源,通过下载额外的apk文件,然后获取该apk文件中的资源文件。从实现难度上来讲,换肤的实现会比夜间模式更复杂些,但是实现方式思路也已经比较成熟。两个比较常见的应用的夜间模式实现效果如下:

知乎夜间模式.png
简书夜间模式.png

可以有一些APP是深蓝底色,有些则是黑色,字体一般为灰色。总体来说都要以暗色为底色,降低亮度和对比度。

二.夜间模式的实现方案——应用换肤技术

1.zip压缩包式皮肤

应用可设置一个默认路径。如果用户选择某个皮肤,则解压该皮肤.zip到这个文件夹中。该皮肤也可以自定义扩展名,但是都为zip格式(例如墨迹天气皮肤扩展名是mja,搜狗输入法的皮肤扩展名是sga)

实现技术: 该技术重点在于如何去读取zip文件中的资源以及皮肤文件存放策略。

实现方案:
如果每次启动都读取SD卡上的皮肤文件,就会影响APP执行速度。最好是提供设置皮肤的界面,把用户选择的皮肤文件解压缩到皮肤路径(例如”/data/data/[package name]/skin” )下,这样不需要跨存储器读取,速度较快,而且不需要每次都去zip压缩包中读取

下面是一些关于处理zip文件的方法

    public void doZip(String srcFile, String destFile) {
        // zipDirectoryPath:需要压缩的文件夹名  
        File zipDir;  
        String dirName;    
        zipDir = new File(srcFile);  
        dirName = zipDir.getName();  
        try {  
            this.zipOut = new ZipOutputStream(
    new BufferedOutputStream(  
                    new FileOutputStream(destFile)));  
            //设置压缩的注释  
            zipOut.setComment("comment");  
            //设置压缩的编码,如果要压缩的路径中有中文,就用下面的编码  
            zipOut.setEncoding("GBK");  
  //启用压缩   
            zipOut.setMethod(ZipOutputStream.DEFLATED);     
            //压缩级别为最强压缩            
    zipOut.setLevel(Deflater.BEST_COMPRESSION);                 
            handleDir(zipDir, this.zipOut,dirName);  
            this.zipOut.close();  
        } catch (IOException ioe) {  
            ioe.printStackTrace();  
        }  
    }

public void unZip(String unZipfile, String destFile) {
        FileOutputStream fileOut;  
        File file;  
        InputStream inputStream;    
        try {  
            this.zipFile = new ZipFile(unZipfile);  
  
            for (Enumeration entries = this.zipFile.getEntries(); 
      entries  .hasMoreElements();) {  
                ZipEntry entry = (ZipEntry) entries.nextElement();  
                file = new File(destFile+File.separator+entry.getName());  
  
                if (entry.isDirectory()) {  
                    file.mkdirs();  
                } else {  
                    // 如果指定文件的目录不存在,则创建之.  
                    File parent = file.getParentFile();  
                    if (!parent.exists()) {  
                        parent.mkdirs();  
                    }  
 inputStream = zipFile.getInputStream(entry);  
  
                    fileOut = new FileOutputStream(file);  
                    while ((this.readedBytes = inputStream.read(
this.buf)) > 0) {  
                        fileOut.write(this.buf, 0, this.readedBytes);  
                    }  
                    fileOut.close();  
  
                    inputStream.close();  
                }  
            }  
            this.zipFile.close();  
        } catch (IOException ioe) {  
            ioe.printStackTrace();  
        }  
    }    
 /**
     * 获得压缩文件内文件列表
     * 
     * @param zipFile 压缩文件
     * @return 压缩文件内文件名称
     * @throws ZipException 压缩文件格式有误时抛出
     * @throws IOException 当解压缩过程出错时抛出
     */
    public static ArrayList<String> getEntriesNames(File zipFile) throws ZipException, IOException {
        ArrayList<String> entryNames = new ArrayList<String>();
        Enumeration<?> entries = getEntriesEnumeration(zipFile);
        while (entries.hasMoreElements()) {
            ZipEntry entry = ((ZipEntry) entries.nextElement());
            entryNames.add(new String(getEntryName(entry).getBytes("GB2312"), "8859_1"));
        }
        return entryNames;
    }
 /**
     * 获得压缩文件内压缩文件对象以取得其属性
     * 
     * @param zipFile 压缩文件
     * @return 返回一个压缩文件列表
     * @throws ZipException 压缩文件格式有误时抛出
     * @throws IOException IO操作有误时抛出
     */
    public static Enumeration<?> getEntriesEnumeration(File zipFile) throws ZipException,
            IOException {
        ZipFile zf = new ZipFile(zipFile);
        return zf.entries();

    }

具体demo可见 https://github.com/luozheng1985/skin_demo
这种方式的优点是:皮肤资源的格式定义很随意可以是zip也可以是自定义的格式,只要程序中能够解析到资源就行,缺点是需要读取并解析文件,导致效率上会比较差。

2.apk文件换肤

这种方法通过获取其他程序的context来获取皮肤资源。我们知道android程序中要获取drawable、layout等资源,都要通过context.getResources().getXXX的方式。那么如果我们可以拿到其他程序的context,那么那个程序就可以作为皮肤程序来提供资源给主程序使用了。android中两个程序相互读取数据的条件是:两个程序的共享用户id相同,通过AndroidManifest.xml中的android:sharedUserId属性配置;两个程序签名相同。想要改变皮肤时,改变提供资源的context为皮肤程序的context,然后刷新即可。
具体实现步骤如下:

  1. 在主应用程序 和 皮肤资源程序的AndroidManifest.xml中配置相同sharedUserId。
    android:sharedUserId是指共用一个uid,也就是,凡是这个属性相同的工程,都会共用同一个uid
    *为了让用户无感知,需要安装后皮肤APk后,让自己不可以打开,且不生成桌面图标,因此要去掉AndroidManifest.xml的如下代码:
<intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
  1. 皮肤资源apk 与 主应用apk中对同一功能的皮肤资源文件名要一致。

  2. 在主程序中 获取到皮肤资源apk对应的Context,

Context skinContext = createPackageContext("皮肤资源包名",Context.CONTEXT_IGNORE_SECURITY);

通过返回的context对象就可以访问到皮肤资源apk中的任何资源
如 :

Drawable drawable =skinContext.getResources().getDrawable(R.drawable.bg_title)

(皮肤资源Apk 无需包含任何UI Activity,只需要包含需要更换皮肤的res资源文件)

优点:可定期提供换肤包供下载,换肤方式灵活,同时效率比较高。
缺点:如需使用某个皮肤,必须安装该皮肤。但其实现起来还是用代码的方式来提供置换的。同时,让两个工程来共享一个进程,这样做十分的危险。此外,安装了很多皮肤后,应用程序列表里面会有很多皮肤程序。在主程序卸载后,皮肤工程不能同样的卸载。如卸载腾讯微博之后,安装的皮肤apk没有被卸载。

推荐阅读更多精彩内容