Android

国内 Android 应用市场对个人开发者并不友好

根据以往的经验,把一个 Android 应用在国内的应用市场上架简直容易得很--审核?他们真的会审核吗?看一眼就会点击”审核通过“按钮吧!只是国内的应用市场太多,对各种资料的要求不尽相同,所以会很繁琐。但通过审核从来都不会是什么问题。

所以我从来都不担心 Android 应用的上架。

但这次还真就通不过了,原因是--这次是个人开发者身份。国内这些 Android 应用市场对个人开发者并不友好。

在申请个人开发者账号时,就遇到了一些麻烦,有个市场要求必填固定电话,还有一个要求提供网络文化经营许可证,这直接就是把个人开发者拒之门外的意思。

提交应用后,被应用宝拒绝,理由:该应用内容包含支付功能,暂不支持个人开发商提交,请修改企业资质或删除支付相关功能后重新提交。

被 360 开放平台拒绝,理由:亲爱的开发者您好,应用名称(内容)体现为公司性质,名称:XX集团,请注册为企业开发者,并提供著作权或该公司授权证明后提交。

应用宝不允许个人开发者的应用带有支付功能,不知道这是出于什么考虑。但无法理解的是,提交的这个应用里并没有在线支付功能。360 提出的理由,这个应用的内容的确有公司性质,但是,好吧我也没什么好说的。

再次尝试依然得到了同样的结果,可能与相关的管理政策有关吧。还是不得不说,苹果对个人开发者非常友好,这可能也是 App Store 如此有生命力,诞生了这么多优秀作品的原因之一。

最后决定放弃在市场上线,直接发布 apk。

Android 代码混淆

Android Studio 工程中,默认就创建了 proguard-rules.pro 文件,可以在其中编写 Proguard 的混淆规则。

但是,默认是不打开混淆的,需要在 build.gradle 文件中,找到这一段:

build.gradle
1
2
3
4
5
6
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

minifyEnabled 从 false 改为 true,即可开启在 release 打包时进行混淆。

为什么混淆开关的名字是 minify enabled ?所谓的”混淆“,其实也就是用无意义的符号(如 a,b,c)去代替代码中有意义的符号(如 title, imagePath, url 等),那么代码即使被反编译后,依然很难直接看懂其含义。从另一个角度看,其实也是让代码精简化的过程。(另外,混淆代码的英文是 Shrink Code)

打开混淆后,在 proguard-rules.pro 文件中编辑混淆规则即可。这个文件其实就是之前用 Eclipse 时的 proguard.cfg,混淆规则的写法是一样的,需要在其中写明哪些类不要进行混淆。

具体可参考 Android 文档 Shrink Your Code and Resources

需要注意的是,不要对第三方库进行混淆,否则调用第三方库的地方,将找不到对应方法,应该不难理解。LeanCloud 给出了一个混淆规则,可以参考:

Blank activity and Empty activity in Android Studio

Android Studio 新建 Activity 的模板里,有一个 Blank Activity,还有一个 Empty Activity。这两个是不同的。

Blank Activity,会为你新建这样一个 Activity:含有导航栏,导航栏上有返回按钮,并设置了导航栏标题,右下角有一个浮动按钮。

可以发现工程中新建了一个 NewActivity.java 的类,同时新建了两个布局文件,一个是 activity_new.xml,另一个是 content_new.xml。activity_new.xml 中是一个 CoordinatorLayout,并且 include 了 content_new.xml。这个 Activity 显示的内容实际在 content_new.xml 中,它是一个 RelativeLayout。

Empty Activity,会为你新建这样一个 Activity:含有导航栏,没有返回按钮,导航栏标题没有设置,其他都是空白。

工程中新建了 NewActivity.java 和 activity_new.xml,里面是一个 RelativeLayout。

要想实现 Blank Activity 的效果,并希望不要有多余的东西,在一个 xml 布局中实现,可以按照以下框架(下面以 EditPasswordActivity 为例):

Android layout 背景图片不显示的奇怪问题

遇到一个奇怪问题。给一个 Activity 设置了全屏的背景图片,方法是给这个 Activity 最父层的 layout 设置 background 属性为这个图片的 drawable。运行后发现图片没有显示,背景全部是白色。

但是在 Android Studio 的 xml 布局渲染调试器里,是正常显示的,在模拟器(API23)里运行,也是正常显示的。

更奇怪的是,把 background 属性换成一个颜色值,可以正常显示。换成另外的一张图,也可以正常显示。

http://www.tc5u.com/android/1371608.htm 问题中,有人提到:

如果楼主出现的是有的图片可以,有些图片不可以,请检查:
1、图片格式必须为png和jpg,也就是文件名称扩展名必须为.png或者.jpg
2、图片的分辨率宽度和高度都要控制在1000像素以内;
3、用Photoshop打开不能显示的图片,看是否为RGB格式,如果是CYMK格式的图片是不能显示的;
4、图片文件名称必须全部小写,不能有大写的文件名称。

发现是由于图片尺寸过大(宽、高超过了1000像素),缩小尺寸后就可以正常显示出来了。

还没调查清楚这是不是 Android 的约束,如果真的有这样的约束,那实在理解不能--现在的手机 1080p 的屏幕满大街都是,还动不动就上 2K,1000px 实在是不够啊。

Android application display name

Android 应用的显示名称,在 AndroidManifest.xml 中进行修改。

AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".LoginActivity"
android:label="@string/title_activity_login">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
...
</activity>
</application>

我们会很显然的认为,修改最外层的 android:label 即可,也就是修改 @string/app_name 的值。但经过实践会发现并不是这样的,Launcher 里显示的竟然是 @string/title_activity_login,也就是 Login Activity 的标题。

实际上,在 Launcher 中显示的应用名称,是被标记为首页面的 Activity 的标题。也就是被标记为:

AndroidManifest.xml
1
2
3
4
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

的 Activity。

那 application 标签的 android:label 属性是用来做什么的?它的确如它的名字一样,代表了应用的名称。在应用程序管理器中看到的名称,就是这个名称。但是这个名称并不是 Launcher 用的名称,所以用户在手机的桌面(应用程序列表)中,看到的不是这个名称。

这着实让人感到不可理解。自认为还是 iOS 的处理方式更科学--在 info.plist 文件中有一个属性 Bundle display name,在这里面存储了应用的显示名称,这是一个应用全局的设置。

那如果首页面 Activity 的标题,跟应用的显示名称不能保持一致,怎么办?那就在 AndroidManifest.xml 里,把首页面 Activity 的 label 设置成应用名称,然后在首页面 Activity 的 onCreate 里,用 setTitle 方法再把标题改掉就好了,如:

1
2
3
4
5
6
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle("登录");
setContentView(R.layout.activity_login);
...
}

Android 收回软键盘

在 iOS 里,如果要收回软键盘,只需要调用当前持有焦点的 TextField 的 resignFirstResponder 方法即可。所以很自然的,在 Android 里,就会想到调用 EditText 的 clearFocus() 方法。但发现这样并没有让键盘收回,这与 iOS 不同。

在 Android 中,应借助 InputMethodManager 可隐藏键盘,方法如下:

1
2
3
4
5
6
7
private void hideKeyboard() {
View viewFocus = this.getCurrentFocus();
if (viewFocus != null) {
InputMethodManager imManager = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imManager.hideSoftInputFromWindow(viewFocus.getWindowToken(), 0);
}
}

要注意,就像刚才所说,虽然有 viewFocus.clearFocus() 这个方法,但调用它是无法让软键盘收回的。

另外,用上面这个方法能够收回键盘,但输入框依然持有着焦点。也就是说,焦点与软键盘是否显示,并无关系。

Android 单选框

与 iOS 不同,Android 提供了单选框,含有两个组件:RadioButtonRadioGroup

把若干个 RadioButton 放到一个 RadioGroup 内,则这个 RadioGroup 内的 RadioButton 自动完成只能选择一个的功能。

但是需要注意,如果想要提供默认值,如默认选择第一个 RadioButton,不能在布局文件中把第一个 RadioButton 的 checked 属性设为 true,否则它会一直处于被选中状态。如果要实现默认值,在 java 代码中实现,如可以在 onCreate 时调用 RadioGroup 的 check 方法,传入第一个 RadioButton 的 id 即可,如:

1
radioGroup.check(R.id.radioButtonFirst);

另外,如果要取得 RadioGroup 中被选择的项,可通过 RadioGroup 的 getCheckedRadioButtonId 方法得到被选择的 RadioButton 的 id。然后可通过 findViewById 得到这个 RadioButton,再以此得到它的 tag 确定选择了第几项。

Android pop to parent activity with status preserving

用 Android Studio 创建的 Blank Activity,在新建向导里会问你它是否有上级页面,可以在里面填上它上一级的 Activity,那么生成的 Activity 顶部的导航栏左侧就有返回按钮,并且点击后可以返回上一级页面。

但是这个返回按钮是有问题的。正常情况下,点击返回应该直接返回到之前创建的上层 Activity,也就是直接 pop 到上一层。但是这个返回按钮,是把上一层 Activity 重新创建了一遍显示出来的,导致无法显示之前的状态。

比如上层 Activity 有一个 ViewPager,里面有两个 Fragment。在第二个 Fragment 里点击进入了详情的 Activity,然后点击返回按钮,发现返回到了上层 Activity 的初始状态-默认选择的第一个 Fragment。

可以在 AndroidManifest.xml 中看到,实现方式是定义了一个 PARMENT_ACTIVITY:

AndroidManifest.xml
1
2
3
4
5
6
7
8
<activity android:name=".ProductListActivity"
android:label="@string/title_activity_product_list"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBar" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.stoneark.doorsbylin.MainActivity" />
</activity>

解决方法也很简单,在 AndroidManifest.xml 中把上一层 Activity 的属性 android:launchMode 改为 singleTop 即可,即:

AndroidManifest.xml
1
2
3
4
5
6
<activity
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleTop" >
</activity>

launchMode 的默认值是 standard,每次都会强制重新创建一个 activity 的实例,所以导致了问题。根据 Android 文档中的描述,
android:launchMode 的取值有:standardsingleTopsingleTasksingleInstance,默认值是 standard:

  • standard 允许该 activity 有多个实例,并且每当显示这个 activity 时就总是重新创建一个。
  • singleTop 允许该 activity 有多个实例,但如果它当前在栈顶,则不重新创建。
  • singleTask 只允许该 activity 有一个实例,并且它只能存在于栈底。
  • singleInstance 与 singleTask 相同,另外还要求栈中只能有它自己,而不能再启动另外任何 activity。

Android 用 ID 字符串取到对应的 View

我们经常会需要在一个 for 循环中,取到多个 View,这时就需要用拼接得到的字符串作为 ID 来取 View。
但 findViewById 的参数是一个 Int 类型,一般用 R.id.xxx 来直接得到这个 Int 值。但如何用字符串来取 View 呢?

这时需要先用 getResources().getIdentifier 由 String 得到对应的 Int,比如下面将会获取 btnItem0 到 btnItem5 这六个按钮:

1
2
3
4
5
6
for (int i=0;i<=5;i++) {
String strButtonID = "btnItem" + i;
int buttonID = getResources().getIdentifier(strButtonID,"id","org.stoneark.packagename");
Button btnItem = (Button) findViewById(buttonID);
...
}

Android Button with both text and image

最近帮朋友做一个不大不小的项目,开发上需要完成 iOS、Android、后台管理三部分。Android 和后台管理的开发虽然之前也都了解一些,但一直没有真正做过什么东西。于是决定把 iOS 交给 Ran,Android 和后台管理也不再找别人做了,自己尝试一下。

Android 的开发在有些地方还是比 iOS 更加方便的。比如实现一个同时有文字和图片的按钮,在 Android 上,Button 有这样四个属性:
drawableLeft, drawableRight, drawableTop, drawableBottom
分别可以在文字的左侧、右侧、顶部、下部放置图片,简直不能再简单。

当然,还是那句话,不存在 iOS 开发更简单或者 Android 开发更简单这样的说法。它们各自都有实现起来相对另一个更方便的地方。所谓简单与难,其实只是精通与知道的差别。永远不要说别人的工作比自己的简单--只是别人比你知道的更多。