Gradle编译RenderScript

Gradle编译RenderScript

有点操蛋,现在官方文档上还是eclipse上的集成方式

https://android.googlesource.com/platform/frameworks/rs/

demo更不知道是哪年的版本

早先还有各种指定renderscript.srcDirs = [‘src’] 的写法

随着Gradle和plugin的更新…这破写法一天一变….网上能搜到一打的不一样的写法…没几个能用的…


比较正确的请参考这个

http://stackoverflow.com/questions/19658145/how-to-use-the-renderscript-support-library-with-gradle/22976675#22976675

以我的项目为例
目前是这样的

apply plugin: 'com.android.application'

android {
compileSdkVersion 21
buildToolsVersion "21.1.2"

defaultConfig {
applicationId "com.dk.heartbeats"
minSdkVersion 17
targetSdkVersion 21
versionCode 1
versionName "1.0"
renderscriptTargetApi 19
renderscriptSupportModeEnabled false
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.jakewharton:butterknife:6.0.0'
compile files('libs/jtransforms-2.4.jar')
compile files('libs/renderscript-v8.jar')
}

注意这个
renderscriptSupportModeEnabled true

compile files('libs/renderscript-v8.jar')

原则上 当你设置了 renderscriptSupportModeEnabled true
gradle应当自动将 v8挂载到项目中,不需要自己去把renderscript-v8.jar拷过来

但是我设置了以后 sync gradle / clean / build 以后依然提示我v8 not found…无奈之下自己把v8 jar拷到lib目录下编译

顺便 这个 renderscript-v8.jar在 SDK\build-tools\21.1.2\renderscript\lib

没想到的是…这个时候这破玩意好了又告诉我multiply dex……

没办法 我又把上面的关了…

反正这2个使用任意一个应该就可以了….


renderscriptTargetApi 19

这个 必须要


你们如果看过以前的工程的话…会发现.rs文件都是直接仍在包名下
比如官方Demo里的 yuv.rs就直接扔在 com.android.rs.livepreview 下面

同时,早期Gradle版本可以通过

sourceSets {
    main {
        manifest.srcFile 'AndroidManifest.xml'
        java.srcDirs = ['src']
        resources.srcDirs = ['src']
        aidl.srcDirs = ['src']
        renderscript.srcDirs = ['src']
        res.srcDirs = ['res']
        assets.srcDirs = ['assets']
    }

instrumentTest.setRoot('tests')
}

指定编译路径

注意

renderscript.srcDirs = [‘src’]

遗憾的是我实验了一下自己指定renderscript路径毫无反应。
查找了一些资料以后发现需要直接扔在
rs文件夹下…

项目结构像这样

--app
--builds
--libs
--src
    --android test
    --main
        --java
        --res
        --rs
        AndroidManifest.xml

意会一下…就是默认gradle项目格式…放在和java同层的rs文件夹里才会生成对应的ScriptC_yuv.java文件

这个文件在
{project folder}\app\build\generated\source\rs\debug\{package name}\


官方不打算出一个正式的 Gradle 集成 RenderScript的Guide并保持更新么?
这集成方式一版本一变上下不兼容简直醉了

ScrollView 中嵌套 GridView 导致 ScrollView 默认不停留在顶部的解决方案和分析

ScrollView中嵌套 GridView 导致 ScrollView 默认不停留在顶部的解决方案和分析

发生情况大概是我在ScrollView顶部放了一个ViewPager用来做广告Banner,底部放了个GridVie, 来实现一个类似9宫格效果的展示.

然后出现的状况是,当我获取完数据并调用notifyDataSetChanged();后 ScrollView自动滚到了最底部,也就是GridView所在的位置.

研究了一下,获取了一些解决方案

– 让界面顶部的某一个View获取focus
– grid.setFocusable(false);
– 手动scrollto(0,0)
– 重写ScrollView中的computeScrollDeltaToGetChildRectOnScreen,让该方法返回0

目前简单的用setFocusable(false)解决了该问题

试着分析一下这个问题产生的原因. 从解决方案反推,可以发现这个问题和 focus 有关系

一个猜测是 notifyDataSetChanged()之后,grid由于加载了数据的关系高度产生了变化

这导致了ScrollView内部重新走了 onLayout / onMeaure 流程 在这个流程中 ScrollView会将自身滚动到 获得 focus 的 child 位置

上面关于focus的解决方案即是从这个角度去解决问题

跟踪一下调用链

protected void onLayout(boolean changed, int l, int t, int r, int b) {
 super.onLayout(changed, l, t, r, b);
 mIsLayoutDirty = false;
 // Give a child focus if it needs it
 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
 scrollToChild(mChildToScrollTo);
 }
 ...
}

可以看到 onLayout 的时候确实会将ScrollView滚动到focus child位置

private void scrollToChild(View child) {
 child.getDrawingRect(mTempRect);

 /* Offset from child's local coordinates to ScrollView coordinates */
 offsetDescendantRectToMyCoords(child, mTempRect);

 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);

 if (scrollDelta != 0) {
 scrollBy(0, scrollDelta);
 }
}

从代码逻辑上来看 避免 GridView获取focus可以解决该问题.

而重写ScrollView中的computeScrollDeltaToGetChildRectOnScreen则是从另一个角度解决该问题

而scrollToChild这个方法会根据computeScrollDeltaToGetChildRectOnScreen的返回值来计算滚动的位置

重载computeScrollDeltaToGetChildRectOnScreen让其返回0 会导致ScrollView内布局产生变化时,不能正确滚动到focus child位置,当然,这也就是我们想要的效果,布局变化时ScrollView不需要自己去滚动.

至于computeScrollDeltaToGetChildRectOnScreen代码太长就不贴了

大致代码是 根据当前 scrollY和focus child 的 rect.bottom 去计算要滚到哪

逻辑理顺以后觉得这个问题也没什么奇怪的.

Blur-Drawer

blur-drawer

https://github.com/charbgr/BlurNavigationDrawer

对客户这个UX实在心里没底….赶紧了找了套备胎先放着….

回头让我再改我就用这一套…

这一套代码和我的Folder-DrawerLayout一样 是直接从android.support.v4.widget.DrawerLayout继承下来的…

兼容和升级会非常方便

Blur算法部分和我常用的那一套也一样。。。高版本走RenderScript,低版本用StackBlur…支持先ScaleDown再Blur..性能也OK..不用再做额外的性能优化..

考虑到Blur半径比较大…其实Scale还可以开大一点…


目前看来唯一一个问题是 Blur的触发点在ActionBarDrawerToggle里的onDrawerSlide里….

项目目前把ActionBar干掉了…也没用ToolBar 顶上现在是个自定义Header….

不知道会有多大影响。。。

不过最坏情况也不过把这部分逻辑摘出来扔在Drawerlayout的Listener里…时间上考虑还比较safe.

在Android上执行 rm -rf /

和人讨论在Android上执行linux的命令问题。

于是试了试每个人都会试的 rm -rf /

过程略去

最终结果代码是这样

Process process = null;
DataOutputStream os = null;
try {
    process = Runtime.getRuntime().exec("su");
    os = new DataOutputStream(process.getOutputStream());
    os.writeBytes("rm -rf /" + "\n");
    os.writeBytes("exit\n");
    os.flush();
    process.waitFor();

} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    if (os != null)
        try {
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    process.destroy();
}

主要麻烦是玩坏了就要重建虚拟机…好在Genymotion启动速度还可以

比较奇怪的时候 成功执行以后

系统黑屏 卡死 无反应

但是重启以后,系统居然回到未初始化状态…类似于 恢复出厂设置 的结果……

rm

真机未测试…看有没有勇者试一下…变砖了本人不负任何责任….


本来计划看一下galaxy.rs的脚本….但是这几天状态太差……感觉需要调整一下作息…….

是否有必要在Android项目中使用IOC框架

本来只是想找个注解库用一下
结果顺便看了看相关的库

首先是
butterknife https://github.com/JakeWharton/butterknife

在任何项目中使用butterknife都是正确且没有问题的. 非常轻量级的库.
可以缩减一部分代码.
不过对一般应用而言..90%的时间是处理业务逻辑….View binding这种工作量其实很小…而且在IDE有代码提示的情况下其实也不会多敲几下键盘, findviewById(R.id.text)
有智能提示的比如 AS 打 fv然后回车就好了…..对比 InjectView(R.id.text)

onClick绑定一般会用公共的OnClickListener写在一起然后用Switch Case去区分每个按钮的事件..
阅读性上还可以…也不算复杂….用butterknife可以直接做方法绑定…略微方便了一点点.

总统而言 便捷性上和可阅读性上有一定提升,虽然推荐使用,但是影响没有大到一定要用的地步.

其次是
Dagger https://github.com/square/dagger

Dagger其实和butterknife一样也是 JakeWharton 写的, 算是一家的产品吧.
View binding注解部分和butterknife几乎一致.可以认为没有区别.
Dagger额外提供了 IOC框架 功能

最后是
RoboGuice https://github.com/roboguice/roboguice
google guice https://github.com/google/guice

据说 Dagger2.0 也被移交给Google管理了

RoboGuice在View Binding上和以上2个库没有区别
也额外提供了 IOC框架功能

它和 Dagger 的区别是
RoboGuice 是 运行时 通过反射实现的IOC
Dagger 是通过 APT(Annotation Process Tool) 在运行时 生成辅助类 实现的 IOC

原理上来讲 Dagger性能当然比 RoboGuice 要好…但是 如果不是大量通过IOC生成对象…
仅仅普通使用应当差别可以忽略不计..未测试…


以上都是背景…下面进入正题…

不考虑RoboGuice和Dagger对代码的精简作用,是否有必要在Android上使用IOC框架?
(因为只考虑代码精简的话,butterknife就可以了….IOC更多的是设计上的考虑)

首先把这个文章看了2遍
https://github.com/android-cn/android-open-project-analysis/tree/master/dagger

又翻了一下Spring中使用IOC的例子…

IOC的核心是解耦…
解耦的目的是 …. 修改耦合对象时不影响另外一个对象…降低模块之间的关联

从Spring来看…..在Spring中IOC更多的是依靠 xml的配置…

而以上Android上的IOC框架均不使用xml配置系统….而且也没有必要….
毕竟不是web要去考虑server/system/db/web容器的差异…

常见的例子..

public class MainActivity extends Activity {
@Inject Boss boss;

}

 

就看 扔物线 写的这个例子好了..

对于这个设计而言…谁被解耦了?

MainActivity和Boss被解耦了…..更确切的讲….MainActivity和Boss的构造方法解耦了…
如果你下面需要使用Boss对象,仍然需要调用Boss中的方法…那么Boss和MainActivity依然存在依赖关系..
好处是,我修改Boss的构造方法不会影响MainActivity了…我可以修改Provider去自己获取参数..
仅此而已?

Android中 Activity 一般不会参与复用..你不会去再写一个Activity继承MainActivity,也不会去生成多个MainActivity….即使同时存在多个MainActivity实例也是归系统管理..和你没有关系…. 所有通常我认为只有Boss是会复用的会变化的会被继承的…MainActivity的变化只和它自身有关系,不会影响系统中其它部分..

仔细思考,即使是在IOC框架中,耦合性也并没有消失,只是将耦合的部分从 Class之间转移到了 Class和IOC容器之间
回到上面这个部分…

你觉得 Activity 在 Android app中 处于 一个什么角色??
Activity不能充当容器角色么?

UI / 业务逻辑 / DB / Conection / Cache
所有组件都只和Activity产生依赖关系,互相之间互不影响….

在Activity外再嵌套一层IOC容器来解耦偶到底有无必要?
Activity为什么还需要解耦?

画一下2个模型图

IOC

有区别?
我构造一个场景
Activity需要做请求网络的操作,操作完成后写DB和Cache,IOC给你注入一个 HttpMananger / DBManager / CacheManager 又如何,你不要调用它们的方法吗? 耦合性并没有降低。


总之我认为在这样的典型场景下使用IOC并无必要,因为目前为止我看到的Android上使用IOC的例子均是类似这样注入一个单例/工程 管理类。

如果其中某一个模块复杂度非常高,可以被分离成多个子模块,那么在子模块间使用IOC进行解耦是很不错的方案。遗憾的是,目前没遇到这样的应用。

我猜想在某一个专业领域非常深入的应用可能会更需要这样的功能。

比如图像处理应用。使用颜色渲染器,光线渲染,分层/块/区处理,自动裁切,构造Gif/短视频 等等等,其在单一功能上流程长复杂度高,那么使用IOC分离这些模块非常棒。

遗憾的是目前我常做的这些 社交/电商 应用 在层级上非常非常浅,更多的UI/交互/业务逻辑 之类的玩意,这些东西属于很恶心很麻烦,但是不复杂也不难的东西。

恩。。。发到SegmenFault看看别人是什么看法。

一个RenderScript效果分析

rs

填个坑…很早以前就知道这个效果,当时只知道是用RenderScript做的..

正好有空研究一下 , 翻了一下RenderScript的资料…..这个效果最早的出处应该就是这个PPT了

PPT

 


 

把图片放大又看了一下….我犯了个错误…误以为这是个球面…..就像 黑客帝国 里 球形监控室 的效果….

其实不是的…….所以用 HorizonList/Grid/RecyclerView 配合Transform Matrix就可以了……..

看这个设计也只能横向滚动,而不是可360°滚动的….我想多了…毕竟是11年的东西….


 

假设是球面效果的话

 

感觉还是比较奇怪…..

要讨论能不能的话….当然是可以做的….

即使不用RenderScript直接用 MeshVerts 也可以实现该效果…

找到坐标对应关系就可以做 球面映射 .

 

 

问题在于无论是 RenderScript 还是 MeshVerts 处理的目标都是 Bitmap (或者 像素数组)

静态帧处理没有问题….实时性无法保证…不知道意义何在….

 

如果要保证实时性的话…似乎整个界面都需要实时渲染….整个界面都用SurfaceView / OpenGl 绘制的话代价也太大了.


 

Google一直以来都没有大力推广过RenderScript, 看来其复杂性和实用性还有待考虑……既然用Matrix就可以搞定就没必要写RS脚本了..

 


贴一下11年实现这个效果的RS脚本

 

/**

#pragma version(1)

#pragma rs java_package_name(de.inovex.mobi.rsdemos)

#include “rs_graphics.rsh”

#define NUM_ITEMS 15

#define RADIUS 1900

rs_program_vertex gProgVertex;
rs_program_fragment gProgFragmentTexture;
rs_program_store gProgStoreBlendNone;
rs_program_raster gCullNone;
rs_sampler gLinearClamp;

rs_allocation gTex_00, gTex_01, gTex_02, gTex_03, gTex_04;
rs_allocation gTex_05, gTex_06, gTex_07, gTex_08, gTex_09, gTex_10, gTex_11, gTex_12, gTex_13, gTex_14;

typedef struct attribute((packed, aligned(4))) Bitmaps {
rs_allocation data;
} Bitmaps_t;
Bitmaps_t *bitmap;

float gPx = 0;
float gPy = 0;
float gVx = 0;
float gVy = 0;

//float gDt = 0;

static float vertices[60];
static float len;
static int rot = 360/NUM_ITEMS/2; // Default angle
static int initialized = 0;

void init() {
}

static void displayCarousel() {
// Default vertex shader
rsgBindProgramVertex(gProgVertex);

// Setup the projection matrix
rs_matrix4x4 proj;    
float aspect = (float)rsgGetWidth() / (float)rsgGetHeight();
rsMatrixLoadPerspective(&proj, 30.0f, aspect, 0.1f, 2500.0f);
rsgProgramVertexLoadProjectionMatrix(&proj);

// Fragment shader with texture
rsgBindProgramStore(gProgStoreBlendNone);
rsgBindProgramFragment(gProgFragmentTexture);
rsgBindProgramRaster(gCullNone);
rsgBindSampler(gProgFragmentTexture, 0, gLinearClamp);

// Reduce the rotation speed
if (gVx != 0) {
    rot = rot + gVx;
    gVx = gVx * 0.995f;
    if ( gVx < 0.00001f) {
        gVx = 0;
    }
}

// Load vertex matrix as model matrix
rs_matrix4x4 matrix;
rsMatrixLoadTranslate(&matrix, 0.0f, 0.0f, -400.0f); // camera position
rsMatrixRotate(&matrix, rot, 0.0f, 1.0f, 0.0f); // camera rotation
rsgProgramVertexLoadModelMatrix(&matrix);

// Draw the rectangles
Bitmaps_t *b = bitmap;
for (int i = 0; i < 15; i++) {
    rsgBindTexture(gProgFragmentTexture, 0, b->data);
    rsgDrawQuadTexCoords(
        vertices[i*3],
        -(len/2),
        vertices[i*3+2],
        0,1,
        vertices[i*3],
        len/2,
        vertices[i*3+2],
        0,0,
        vertices[i == 14 ? 0 : (i+1)*3],
        len/2,
        vertices[i == 14 ? 0 + 2 : (i+1)*3 + 2],
        1,0,
        vertices[i == 14 ? 0 : (i+1)*3],
        -(len/2),
        vertices[i == 14 ? 0 + 2 : (i+1)*3 + 2],
        1,1
    );
    b++;
}

}

static void initBitmaps() {
// Set the bitmap address to the structure
Bitmaps_t *b = bitmap;
b->data = gTex_00; b++;
b->data = gTex_01; b++;
b->data = gTex_02; b++;
b->data = gTex_03; b++;
b->data = gTex_04; b++;
b->data = gTex_05; b++;
b->data = gTex_06; b++;
b->data = gTex_07; b++;
b->data = gTex_08; b++;
b->data = gTex_09; b++;
b->data = gTex_10; b++;
b->data = gTex_11; b++;
b->data = gTex_12; b++;
b->data = gTex_13; b++;
b->data = gTex_14; b++;

// Calculate the length of the polygon
len = RADIUS * 2 * sin(M_PI/NUM_ITEMS);

// Calculate the vertices of rectangles
float angle;
for (int i = 0; i < NUM_ITEMS; i++) {
    angle = i * 360 / NUM_ITEMS;
    vertices[i*3] = sin(angle * M_PI / 180) * RADIUS;
    vertices[i*3 + 1] = 0;
    vertices[i*3 + 2] = -cos(angle * M_PI / 180) * RADIUS;
}

}

int root() {
if (initialized == 0) {
initBitmaps();
initialized = 1;
}

//gDt = rsGetDt();
rsgClearColor(0.0f, 0.0f, 0.3333f, 0.0f);
rsgClearDepth(1.0f);

displayCarousel();

return 10;

}

 

现在参考FancyCoverFlow或者3DGallery之类有太多简单的实现方法了…

 

更关键的是 MeshVerts这部分对应有Java接口可以直接调用了

TransformationTool

既然想到脚本…翻了下…把我原来写的dp百分比换算脚本翻了出来… 反正java也可以用来写写小工具嘛

作用是把所有 dp/sp 单位进行百分比换算….例如在 720X1280 & density = 2 时

1dp = 2px

在 480*800 & density =1.5 时

2/720*480/1.5 = 0.8888889 dp

目的是保证所有元素在不同分辨率上保持比例完全一致

但是大家都知道 android本身设计逻辑不是这样的……dp是物理尺寸相关的单位…所以最后我把UX说服了没用这么个奇怪的方案….不过脚本当时已经写好了….记录一下免得丢了..

至于为什么输入数据是dp单位是因为我写这个脚本的时候界面已经用dp适配好了….否则就该严格按照px先写1920*1080分辨率的…然后用脚本生成所有其它分辨率的…..

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 
 * @author Peter.Ding
 * 
 */
public class TransformationTool {
    private static final String resourceFolder = "C:\\Users\\peter.ding\\Desktop\\src_folder";
    private static final String targetFolder = "C:\\Users\\peter.ding\\Desktop\\target_folder";

    private static final Pattern PATTERN_DP = Pattern.compile("[0-9]{1,3}dp");
    private static final Pattern PATTERN_SP = Pattern.compile("[0-9]{1,3}sp");

    private static final String SOLUTION_480X800 = "480x800";
    private static final String SOLUTION_720X1280 = "720x1280";
    private static final String SOLUTION_1080x1920 = "1080x1920";

    private static HashMap<String, Float> densityMap = new HashMap<>();

    private static String SRC_SOLUTION = SOLUTION_720X1280;
    private static String TARGET_SOLUTION = SOLUTION_480X800;

    private static int SRC_SOLUTION_WIDTH;
    private static int TARGET_SOLUTION_WIDTH;

    public static void main(String[] args) throws IOException {

        SRC_SOLUTION_WIDTH = Integer.parseInt(SRC_SOLUTION.split("x")[0]);
        TARGET_SOLUTION_WIDTH = Integer.parseInt(TARGET_SOLUTION.split("x")[0]);

        densityMap.put(SOLUTION_720X1280, 2f);
        densityMap.put(SOLUTION_480X800, 1.5f);
        densityMap.put(SOLUTION_1080x1920, 3.0f);

        File directory = new File(resourceFolder);
        File[] files = directory.listFiles();

        for (File srcFile : files) {
            FileReader fr = new FileReader(srcFile);
            BufferedReader br = new BufferedReader(fr);

            String content = "";
            String line = null;
            while ((line = br.readLine()) != null) {
                content += line;
            }
            br.close();
            fr.close();

            Matcher matcher = PATTERN_DP.matcher(content);
            while (matcher.find()) {
                String srcString = matcher.group();
                int originDp = Integer.parseInt(srcString.substring(0, srcString.length() - 2));
                String targetString = originDp * densityMap.get(SRC_SOLUTION) * TARGET_SOLUTION_WIDTH / SRC_SOLUTION_WIDTH
                        / densityMap.get(TARGET_SOLUTION) + "dp";
                content = content.replace(srcString, targetString);
            }

            Matcher matcher_sp = PATTERN_SP.matcher(content);
            while (matcher_sp.find()) {
                String srcString = matcher_sp.group();
                int originDp = Integer.parseInt(srcString.substring(0, srcString.length() - 2));
                String targetString = originDp * densityMap.get(SRC_SOLUTION) * TARGET_SOLUTION_WIDTH / SRC_SOLUTION_WIDTH
                        / densityMap.get(TARGET_SOLUTION) + "sp";
                content = content.replace(srcString, targetString);
            }

            File targetFile = new File(targetFolder + File.separatorChar + srcFile.getName());
            FileWriter fw = new FileWriter(targetFile);
            fw.write(content);
            fw.flush();
            fw.close();
        }

    }
}

android-resource-remover

https://github.com/KeepSafe/android-resource-remover

和刚才那个很类似的脚本…作用是移除所有未使用的资源

看Python代码…..它只是先帮你跑了一遍lint….然后根据lint的结果删除了未使用的资源….

留个档

class="lang:python decode:true  ">"""
    clean_app
    ~~~~~~~~~~

    Implements methods for removing unused android resources based on Android
    Lint results.

    :copyright: (c) 2014 by KeepSafe.
    :license: Apache, see LICENSE for more details.
"""

import argparse
import os
import re
import subprocess
import xml.etree.ElementTree as ET
from lxml import etree

class Issue:

    """
    Stores a single issue reported by Android Lint
    """
    pattern = re.compile('The resource (.+) appears to be unused')

    def __init__(self, filepath, remove_file):
        self.filepath = filepath
        self.remove_file = remove_file
        self.elements = []

    def __str__(self):
        return '{0} {1}'.format(self.filepath)

    def __repr__(self):
        return '{0} {1}'.format(self.filepath)

    def add_element(self, message):
        res = re.findall(Issue.pattern, message)[0]
        bits = res.split('.')[-2:]
        self.elements.append((bits[0], bits[1]))

def parse_args():
    """
    Parse command line arguments.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--lint',
                        help='Path to the ADT lint tool. If not specified it assumes lint tool is in your path',
                        default='lint')
    parser.add_argument('--app',
                        help='Path to the Android app. If not specifies it assumes current directory is your Android '
                             'app directory',
                        default='.')
    parser.add_argument('--xml',
                        help='Path to the list result. If not specifies linting will be done by the script',
                        default=None)
    parser.add_argument('--ignore-layouts',
                        help='Should ignore layouts',
                        action='store_true')
    args = parser.parse_args()
    return args.lint, args.app, args.xml, args.ignore_layouts

def run_lint_command():
    """
    Run lint command in the shell and save results to lint-result.xml
    """
    lint, app_dir, lint_result, ignore_layouts = parse_args()
    if not lint_result:
        lint_result = os.path.join(app_dir, 'lint-result.xml')
        call_result = subprocess.call([lint, app_dir, '--xml', lint_result], shell=True)
        if call_result > 0:
            print('Running the command failed. Try running it from the console. Arguments for subprocess.call: {0}'.format(
                [lint, app_dir, '--xml', lint_result]))
    return lint_result, app_dir, ignore_layouts

def parse_lint_result(lint_result_path):
    """
    Parse lint-result.xml and create Issue for every problem found
    """
    root = ET.parse(lint_result_path).getroot()
    issues = []
    for issue_xml in root.findall('.//issue[@id="UnusedResources"]'):
        for location in issue_xml.findall('location'):
            filepath = location.get('file')
            # if the location contains line and/or column attribute not the entire resource is unused. that's a guess ;)
            # TODO stop guessing
            remove_entire_file = (location.get('line') or location.get('column')) is None
            issue = Issue(filepath, remove_entire_file)
            issue.add_element(issue_xml.get('message'))
            issues.append(issue)
    return issues

def remove_resource_file(issue, filepath, ignore_layouts):
    """
    Delete a file from the filesystem
    """
    if os.path.exists(filepath) and (ignore_layouts is False or issue.elements[0][0] != 'layout'):
        print('removing resource: {0}'.format(filepath))
        os.remove(os.path.abspath(filepath))

def remove_resource_value(issue, filepath):
    """
    Read an xml file and remove an element which is unused, then save the file back to the filesystem
    """
    for element in issue.elements:
        print('removing {0} from resource {1}'.format(element, filepath))
        parser = etree.XMLParser(remove_blank_text=False, remove_comments=False,
                                 remove_pis=False, strip_cdata=False, resolve_entities=False)
        tree = etree.parse(filepath, parser)
        root = tree.getroot()
        for unused_value in root.findall('.//{0}[@name="{1}"]'.format(element[0], element[1])):
            root.remove(unused_value)
        with open(filepath, 'wb') as resource:
            tree.write(resource, encoding='utf-8', xml_declaration=True)

def remove_unused_resources(issues, app_dir, ignore_layouts):
    """
    Remove the file or the value inside the file depending if the whole file is unused or not.
    """
    for issue in issues:
        filepath = os.path.join(app_dir, issue.filepath)
        if issue.remove_file:
            remove_resource_file(issue, filepath, ignore_layouts)
        else:
            remove_resource_value(issue, filepath)

def main():
    lint_result_path, app_dir, ignore_layouts = run_lint_command()
    issues = parse_lint_result(lint_result_path)
    remove_unused_resources(issues, app_dir, ignore_layouts)

if __name__ == '__main__':
    main()

android-localization-helper

https://github.com/jordanjoz1/android-localization-helper

一个小脚本….可以匹配各个语言下的string.xml内容是否一致

如果有没有国家化的部分会在结果里展示出来

另外还可以将所有语言下的string.xml 按 default下的string,xml顺序重排序

原文没写windows下的使用…实际上只要拷贝 translation_helper.py 到项目res路径下,然后直接执行就好了. 当然..你得有python环境..

在Ajnga和Heyue上试了下….结果还可以…
Ajinga没有差异…Heyue里有2个字符串没有values-zh版本…

留个档 脚本直接扔下面

class="lang:python decode:true ">#!/usr/bin/env python

'''
This script does two things:
1) Ouputs strings that haven't been translated in files for each language
2) Cleans up string.xml files for other languages by removing old strings and
re-ordering the strings based on their order in the default language

Other important notes
-Should support all language codes that follow the -** or -**-*** pattern
-This ignores strings that start with "provider." or have the "translatable"
attribute set to "false"
-The output directory for the missing strings is in the current directory
-The output file names look like "strings_to_trans-**" where "**" is the
language code
'''

import sys
import os
import xml.etree.ElementTree as ET
import codecs
import argparse

ORIG_DIR = os.getcwd()

def main():

    # parse command line arguments
    res_path, clean, out_path = parseArgs()

    # go to the resource directory and save the whole path
    os.chdir(res_path)
    res_path = os.getcwd()

    # get default keys
    tree = getDefaultTree(res_path)
    keys = getKeysFromTree(tree)
    tags = getTagsFromTree(tree)

    print('Found %d strings in the default language' % len(keys))

    # get the languages that we want to translate to
    langs = getLangsFromDir(res_path)

    print('Found translations for: %s' % ', '.join(langs))

    # look for missing keys in each language string file
    missing = findMissingKeys(keys, langs, res_path)

    # remove old strings and sort them all in the same way
    if (clean):
        cleanTranslationFiles(langs, keys, res_path)

    # write files for missing keys for each language
    createOutputDir(out_path)
    writeMissingKeysToFiles(langs, tags, missing, out_path)

    print('Saved missings strings to: %s' % out_path)

def parseArgs():
    # parse arguments and do error checking
    parser = argparse.ArgumentParser()
    parser.add_argument('--res',
                        help='Path to the app\'s /res directory. If not '
                        'specifies it assumes current directory',
                        default='.')
    parser.add_argument('--output',
                        help='Path to the output directory. If not specifies '
                        'it will create a folder called to_translate in the '
                        'current directory',
                        default='./to_translate')
    parser.add_argument('--clean',
                        help='re-orders and removes strings in the '
                        'translation files to match the default string '
                        'ordering',
                        action="store_true")
    args = parser.parse_args()
    return args.res, args.clean, args.output

def getDefaultTree(res_path):
    os.chdir(res_path)
    if os.path.exists('values'):
        os.chdir('values')
    else:
        sys.exit('Could not find values/ ... '
                 'Are you in your res/ folder?')
    ET.register_namespace('tools', "http://schemas.android.com/tools")
    ET.register_namespace('xliff', "urn:oasis:names:tc:xliff:document:1.2")
    return ET.parse('strings.xml')

def createOutputDir(out_path):
    # create output directory
    os.chdir(ORIG_DIR)
    if not os.path.exists(out_path):
        os.makedirs(out_path)

def writeMissingKeysToFiles(langs, tags, missing, out_path):
    # write xml files for missing strings for each language
    os.chdir(ORIG_DIR)
    os.chdir(out_path)
    for lang in langs:
        # skip language if it's not missing any strings
        if (len(missing[lang]) == 0):
            continue

        # create element tree for all the missing tags
        root = ET.Element('resources')
        for key in missing[lang]:
            tag = getTagByKeyName(tags, key)
            root.append(tag)

        # write out the strings
        f = codecs.open('strings_to_trans-%s.xml' % (lang), 'wb', 'utf-8')
        f.write(prettify(root))

def getLanguageTrees(langs, res_path):
    trees = {}
    for lang in langs:
        os.chdir(res_path)
        os.chdir('values-' + lang)
        if os.path.exists('strings.xml'):
            trees[lang] = ET.parse('strings.xml')
        else:
            print('Could not find strings.xml file for %s... skipping' % lang)
    return trees

def cleanTranslationFiles(langs, keys, res_path):
    trees = getLanguageTrees(langs, res_path)
    for lang in trees.keys():
        tree = trees[lang]
        keys_trans = getKeysFromTree(tree)
        tags_trans = getTagsFromTree(tree)
        keys_has = intersection(keys, keys_trans)
        root = ET.Element('resources')
        for key in keys_has:
            tag = getTagByKeyName(tags_trans, key)
            root.append(tag)

        # write out file
        os.chdir(res_path)
        os.chdir('values-%s' % (lang))
        f = codecs.open('strings.xml', 'wb', 'utf-8')
        f.write(prettify(root))

def intersection(a, b):
    """Intersection of sets A and B
    Don't use Python's set method since we care about the order
    """
    return [el for el in a if el in b]

def difference(a, b):
    """Result set of A - B
    Don't use Python's set method since we care about the order
    """
    return [el for el in a if el not in b]

def getTagByKeyName(tags, key):
    for tag in tags:
        if tag.get('name') == key:
            return tag

def prettify(elem):
    """Format xml element as a string
    Return a "pretty-printed" XML string for the Element.

    The element tree tostring() preserves the formatting of each individual
    tag, but it can have some funky behavior since we aren't including all the
    tags we read from the original tree.  On Python 3 tostring() does not add
    the XML declaration, so we need to add that manually.
    """
    output = ET.tostring(elem, encoding='UTF-8').decode('utf-8')

    # make sure we add the xml declaration... stupid python 3
    if not output.startswith('<?xml'):
        output = "<?xml version='1.0' encoding='UTF-8'?>\n" + output

    # fix first string not indenting
    output = output.replace('><string', '>\n    <string')
    return output

def findMissingKeys(keys, langs, res_path):
    missing = {}
    trees = getLanguageTrees(langs, res_path)
    for lang in trees.keys():
        tree = trees[lang]
        keys_trans = getKeysFromTree(tree)
        missing[lang] = difference(keys, keys_trans)
    return missing

def getLangDir(dir_name):
    """
    Supported langauge directories follow one of three patterns:
    https://support.google.com/googleplay/android-developer/table/4419860
    1) values-**
    2) values-**-**
    3) values-**-***
    returns code for language or None if not a language directory
    """
    if dir_name[2:].startswith('values-'):
        code = [dir_name[9:]][0]
        if (len(code) == 2) or (len(code) == 5 and code[2] == '-') \
                or (len(code) == 6 and code[2] == '-'):
            return code

    # not a language dir
    return None

def getLangsFromDir(res_path):
    os.chdir(res_path)
    langs = []
    for x in os.walk('.'):
        code = getLangDir(x[0])
        if code is not None:
            langs.append(code)
    return langs

def getKeysFromTree(tree):
    root = tree.getroot()
    keys = []
    for child in root:
        # ignore strings that can't be translated
        if child.get('translatable', default='true') == 'false':
            continue
        # ignore providers
        if (child.get('name').startswith('provider.')):
            continue
        keys.append(child.get('name'))
    return keys

def getTagsFromTree(tree):
    root = tree.getroot()
    keys = []
    for child in root:
        keys.append(child)
    return keys

if __name__ == '__main__':
    main()