在Android早期,系统在屏幕顶部显示一个状态栏(StatusBar),但在Android的Honeycomb版本上,状态栏移到了屏幕底部。
这里我们在不同的版本中实现全屏效果也就是熄灯模式。
熄灯模式:
在特定情况下,通过减少或隐藏导航栏、动作栏以及系统UI等控件,为用户提供全屏无干扰的视觉体验就是熄灯模式。熄灯模式可以保证用户更好的关注屏幕内容。如果用户需要操作内容,可以通过触摸屏幕等方式退出熄灯模式。
代码实现如下:
public void onCreate(...){
...
//移除标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
mContentView = findViewById(R.id.content);
mContentView.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
Window w = getWindow();
if(mUseFullscreen){
w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
w.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}else{
w.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
w.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
mUseFullscreen = !mUseFullscreen;
}
});
}
在这个版本中,标题栏被动作栏(Action Bar)替代,仍然位于屏幕上方,但状态栏被移到屏幕底部。
还有一个重要改变是没有物理按键了,按键都被放置到状态栏中,因此一般只是将其暗化处理。
代码实现如下:
public void onCreate(...){
...
mContentView = findViewById(R.id.content);
//隐藏或显示动作栏
mContentView.setOnSystemUiVisibilityChangeListener(new OnSystemUiVisiblilityChange(int visibility){
ActionBar actionBar = getActionBar();
if(actionBar != null){
mContentView.setSystemUiVisibility(visibility);
//是否可视的参数
if(visibility == View.STATUS_BAR_VISIBLE){
actionBar.show();
}else{
actionBar.hide();
}
}
});
mContentView.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
if(mContentView.getSystemUiVisibility() == View.STATUS_BAR_VISIBLE){
mContentView.setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
}else{
mContentView.setSystemUiVisibility(View.STATUS_BAR_VISIBLE);
}
}
});
}
代码实现如下:
Class<?> activity = null;
//坚持Android版本
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
activity = MainActivity2x.class;
}else{
activity = MainActivity3x.class;
}
//启动不同Activity
startActivity(new Intent(this,activity));
finish();
这里我们进行在应用程序中使用Android新API并能运行在老设备上。
演示程序:
第一个API是Android v9中为SharedPreferences.Editor类新添加的apply()方法。
第二个API是在Android API Level 8中引入的,用于在manifest文件中声明是否允许讲应用程序安装在SD卡上。
要操作SharedPreferences类,需要获取一个Editor类,然后调用该类提供的方法来修改SharedPreferences的值。所有相关修改都完成时,需要调用commit()方法。
从Android v9版本开始,SharedPreferences.Editor提供了apply()方法用于替代commit()方法。
官方文档上的解释:
“commit()方法会同步地将偏好值(Preference)直接写入持久化存储设备,与其不同的是,apply()方法会立即把修改内容提交到SharedPreferences内容缓存中,然后开始异步地将修改提交到存储设备上,在这个过程中,开发者不会察觉到任何错误问题。”
所以,如果不需要用到操作的返回值,开发者就应该用apply()方法代替commit()方法。
代码中“…”是那些基础的一般都有的代码。
Activity类中代码如下:
public class MainActivity extends Activity{
private static final String PREFS_NAME = "main_activity_prefs";
private static final String TIMES_OPENED_KEY = "times_opened_key";
private static final String TIMES_OPENED_FMT = "Times opened:%d";
private TextView mTextView;
private int mTimesOpened;
@Override
public void onCreate(...){
...
mTextView = (TextView) findViewById(R.id.times_opened);
}
@Override
protected void onResume(){
...
SharedPreferences prefs = getSharedPreferences(PREFS_NAME,0);
mTimesOpened = prefs.getInt(TIMES_OPENED_KEY,1);
mTextView.setText(String.format(TIMES_OPENED_FMT,mTimesOpened));
}
@Override
protected void onPause(){
...
Editor editor = getSharedPreferences(PREFS_NAME,0).edit();
editor.putInt(TIMES_OPENED_KEY,mTimesOpened+1);
SharedPreferencesCompat.apply(editor);
}
}
SharedPreferencesCompat类代码如下:
public class SharedPreferencesCompat{
private static final Method sApplyMethod = findApplyMethod();
private static Method findApplyMethod(){
//检查apply()方法是否可用
try{
Class cls = SharedPreferences.Editor.class;
return cls.getMethod("apply");
}catch(NoSuchMethodException unused){}
return null;
}
public static void apply(SharedPreferences.Editor editor){
if(sApplyMethod != null){
try{
//真正调用Editor的apply()方法
sApplyMethod.invoke(editor);
return;
}catch (InvocationTargetException unused) {}catch (IllegalAccessException unused) {}
}
editor.commit();//否则调用commit()
}
}
SharedPreferencesCompat类通过java反射机制检查SharedPreferences.Editor类的apply()方法是否可用。如果可用就将其存放到一个静态变量中。当调用我们创建的apply()方法时,便通过传入的Editor参数真正触发该参数的apply()方法。如果调用失败就调用commit()方法。
从Android v8开始,可以向AndroidManifest中添加一个名为android:installLocation的属性。
该属性开发文档的解释:
“这是一个可选特性,你可以通过在Manifest文件中声明android:installLocation属性来使用这项特性。如果你没有声明,应用程序就会安装到内部存储器上,并且你也不能把它移到外部存储器中。”
要使用上述属性,我们需要在AndroidManifest.xml文件中修改下面几个代码:
//设置android:installLocation属性值为preferExternal
<manifest ...
package="com.manning.androidhacks.hack045"
android:versionCode="1"
android:versionName="1.0"
android:installLocation="preferExternal">
//设置minSdkVersion属性值为8
<uses-sdk android:minSdkVersion="8"/>
我们将android:installLocation属性值设为preferExternal,有SD卡的时候,应用程序就会安装到SD卡上。要使用这个特性,需要将minSdkVersion属性值设置为8。在代码中如果指定上述内容,用户就无法在API 8下级别的Android上安装该应用。(这个现在已经没什么意义了),我们现在开发已经不会兼容8以下的版本了。
这里要注意的一个是:
强行在高版本API Level上编译,会导致类和方法不能向后兼容,如:想在当前运行版本上调用一个该版本中没有提供的方法,会出现java.lang.VerifyError异常。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5
外链地址6
Android的Jelly Bean版本发布时引入了新的通知(notification)API。通过新API,可以为通知添加动作。通过这些动作,可以在不需要进入应用程序的情况下,对通知做出响应。
要处理通知的所有点击事件,需要使用PendingIntent。PendingIntent与Intent类的最大区别是前者用于延迟执行。
开发文档:
“通过向其他应用程序传送一个PendingIntent,开发者可以为该应用程序赋予执行指定操作的权限,这样其他应用程序就像是你自己的一样(具有相同的权限和身份信息)。PendingIntent本身而言,开发者需要注意构建PendingIntent的方式:通常对于基本的Intent会显示指定某个自己的组件(component),以确保其最终能够发送到指定的目的地,而不会发送到其他地方。”
使用PendingIntent的局限是,开发者无法做出类似“运行着几行代码”这样的操作,只能启动一个Activity、Service或BroadcastReceiver。
在示例程序中,需要涵盖两种类型的操作——一种操作不需要显示UI界面(删除、取消、发送短信),另一种操作需要显示UI界面(读短信、回复短信)。不需要显示UI界面的操作可以用后台逻辑实现,因此我们创建一个名为NotificationHelper的静态类,用于实现所有处理通知的逻辑以及PendingIntent的创建,代码如下:
public class NotificationHelper{
//由MainActivity调用,以显示通知
public static void showMsgNotification(Context ctx){
final NotificationManager mgr;
mgr = (NotificationManager)ctx.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx).setSmallIcon(android.R.drawable.sym_def_app_icon).setTicker("New msg!").setContentTitle("This is the msg title").setContentText("content...").setContentIntent(getPendingIntent(ctx));
//添加回复动作
builder.addAction();
builder.addAction();
}
private static PendingIntent getDeletePendingIntent(Context ctx){
//与删除短信相关的PendingIntent会用到MsgService
Intent intent = new Intent(ctx,MsgService.class);
intent.setAction(MsgService.MSG_DELETE);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getService(ctx,0,intent,0);
}
private static PendingIntent getReplyPendingIntent(Context ctx){
//与回复短信相关的PendingIntent会用到ReplyActivity
Intent intent = new Intent(ctx,ReplyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(ctx,0,intent,0);
}
private static PendingIntent getPendingIntent(Context ctx){
//点击通知时,会通过MsgActivity显示短信
Intent intent = new Intent(ctx,MsgActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(ctx,0,intent,0);
}
public static void dismissMsgNotification(Context ctx){
//取消通知的辅助方法
final NotificationManager mgr;
mgr = (NotificationManager)ctx.getSystemService(Context.NOTIFICATION_SERVICE);
mgr.cancel(R.id.activity_main_receive_msg);
}
}
通过NotificationHelper类,可以处理所有与通知相关的操作。现在,我们来分析MsgService的一部分代码。因为MsgService继承自IntentService,其onHandleIntent()方法的代码如下:
@Override
protected void onHandleIntent(Intent intent){
if(MSG_RECEIVE.equals(intent.getAction())){
handleMsgReceive();
}else if(MSG_DELETE.equals(intent.getAction())){
handleMsgDelete();
}else if(MSG_REPLY.equals(intent.getAction())){
handleMsgReply(intent.getStringExtra(MSG_REPLY_KEY));
}
}
handleMsgDelete()方法代码分析如下:
private void handleMsgDelete(){
//这里是移除短信而不是创建日志
Log.d(TAG,"Removing msg...");
//关闭通知
NotificationHelper.dismissMsgNotification(this);
}
当点击MsgActivity的删除按钮时,我们让MsgService全权处理。如MsgActivity中的删除按钮的点击事件处理器:
public void onDeleteClick(View v){
Intent intent = new Intent(this,MsgService.class);
intent.setAction(MsgService.MSG_DELETE);
startService(intent);
finish();
}
我们在之前开发会用到TabActivity类,该类允许开发者在应用程序中创建Tab,这样用户就可以通过点击Tab按钮在不同Activity之间切换。TabActivity类最大的问题是在定制其外观时,会遇到很多问题,而且该类在Fragment发布后已经废弃。
尽管Android SDK提供了TabHost和TabWidget等类用于处理Tab,但还是没有使用自己实现的更灵活。
首先,我们需要为Tab创建UI界面。
代码如下:
<LinearLayout ...
android:background="@null"
android:orientation="horizontal">
<Button
android:id="@+id/tab_red"
.../>
<Button
android:id="@+id/tab_green"
.../>
<Button
android:id="@+id/tab_blue"
.../>
</LinearLayout>
为了避免每个Activity中都复制/粘贴Tab的布局文件,所以我们使用include标签。MainActivity的XML布局代码如下:
<FrameLayout ...>
<!-- Fragment的容器 -->
<FrameLayout
android:id="@+id/main_fragment_container"
.../>
<!-- 将Tab的布局文件添加到Activity的视图中 -->
<include layout="@layout/tabs"/>
</FrameLayout>
我们将include标签放到底部是为了让其显示在Fragment容器上方。
至此,UI界面已经完成,下面是Activity的处理逻辑,代码如下:
//继承FragmentActivity
public class MainActivity extends FragmentActivity{
@Override
public void onCreate(...){
...
setContentView(R.layout.main);
//为按钮设置点击监听器,进而通过新建一个Fragment实例调用switchFragment()方法
findViewById(R.id.tab_red).setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
switchFragment(ColorFragment.newInstance(Color.RED,"Red"));
}
});
...
}
//处理在容器内显示Fragment的逻辑
private void switchFragment(Fragment fragment){
FragmentTransaction ft;
ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.main_fragment_container,fragment);
ft.commit();
}
}
开发者可以使用Apache Maven处理依赖关系。
要理解Maven的工作原理,我们需要分析pom.xml文件的各个不同片段。pom.xml文件是需要在项目中配置的唯一一个与Maven相关的文件。在该文件中,我们需要向Maven提供应用程序的名称、构建时所需的依赖关系、测试相关的依赖关系以及如何创建APK。Maven首先检查这些依赖关系是否存在于本地仓库(local repository)。默认情况下,本地仓库的路径是~/.m2/repository。如果在本地仓库中没有找到指定的依赖关系,Marven会从中心仓库(central repository,是Maven默认的远程仓库)中下载依赖关系。
pom.xml文件中第一段代码如下:
<project xmlns="http://Maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://Maven.apache.org/POM/4.0.0http://Maven.apache.org/Maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.roboguice</groupId>
<artifactId>calculator</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>apk</packaging>
<name>calculator</name>
最终更多构建结果会存在于$MVN_REPO/groupId/artifactId/version中。通常情况下,示例程序会使用groupId作为项目名,使用artifactId作为模块名。在本例中,Manfred之所以使用org.roboguice作为项目名,是因为本例是基于roboguice(一种依赖注入库)项目开发的。在该项目中,artifactId和calculator用于标识当前示例程序。
最后两个属性是packaging和name。packaging为Maven指定最终输出文件的类型,默认值为jar。因为我们需要创建一个Android程序,所以Manfred将该值指定为apk。name和version共同决定输出文件的名字。
pom.xml第二部分的部分依赖关系代码如下:
<dependencies>
<dependency>
<groupId>org.roboguice</groupId>
<!-- roboguice依赖 -->
<artifactId>roboguice</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
...
<dependency>
<groupId>com.google.android</groupId>
<!-- Android依赖 -->
<artifactId>android</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
...
<dependency>
<groupId>com.pivotallabs</groupId>
<!-- robolectric依赖 -->
<artifactId>robolectric</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
每个依赖关系都有四个重要属性,即groupId、artifactId、version和scope。第一个依赖关系是roboguice,定义了groupId、artifactId、version属性,用于关联某个Maven仓库中依赖关系的一个发布版本。
尽管roboguice依赖关系中没有包含scope属性,但我们应该知道其实使用了默认值compile。因为编译范围的依赖关系(Compile Dependencies)会被打包到APK中,因此这种依赖关系在项目所有类路径(classpath)中可用。
下一个依赖关系是Android本身。使用Maven编译Android应用程序时,必须将Android视为一种依赖关系,这种依赖关系的scope属性值为provided。provided与compile有很多相似之处,但provided表示希望在运行时由JDK或容器提供依赖关系。本例由运行Android的设备提供依赖关系。
最后一种依赖关系是robolectric。robolectric是一个测试框架,因此我们只在编译/运行测试代码的时候需要这种依赖关系。这就是将scope设置为test的目,在正常使用应用程序时,不需要这种测试范围的依赖关系,只有在编译或执行测试代码的阶段才会用到这种依赖关系。
pom.xml文件中,配置build部分代码如下:
<build>
<plugins>
<plugin>
<groupId>
com.jayway.Maven.plugins.android.generation2
</groupId>
<artifactId>
android-Maven-plugin
</artifactId>
<version>3.0.0-SNAPSHOT</version>
<configuration>
<androidManifestFile>
${project.basedir}/AndroidManifest.xml
</androidManifestFile>
<assetsDirectory>
${project.basedir}/assets
</assetsDirectory>
<resourceDirectory>
${project.basedir}/res
</resourceDirectory>
<sdk>
<platform>10</platform>
</sdk>
<undeployBeforeDeploy>true</undeployBeforeDeploy>
</configuration>
<extensions>true</extensions>
...
</plugin>
</plugins>
</build>
构建插件与依赖关系的工作原理相似。上面代码表示android-Maven-plugin插件是如何配置的。如果配置依赖关系,则需要提供groupId、artifactId和version属性。
我们可以注意到,Apache Maven遵循“约定优于配置”(convention-over-configuration)的规范,这样做的原因是减少软件开发人员所做决定的数量,应用简单而又不失灵活性。
pom.xml文件准备就绪后,开发者就可以把Android应用程序作为一个Maven构件。如果运行mvn package命令,会生成一个存放APK的目标路径。如果想在连接的设备上安装应用程序,可以运行mvn android:deploy命令。
如果理解了该工具的运行原理,只需生成pom.xml文件就可以创建一个项目。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5
外链地址6
外链地址7
在将应用程序安装到设备之前,与java虚拟机兼容的.class字节码文件会被转换为与Dalvik虚拟机兼容的.dex文件。
Android支持添加JAR包作为依赖库,但每次构建代码时,都需要先将JAR包中的.class文件转化为.dex文件,这样消耗了大量的时间。
这里我们演示如何在开发设备上安装这些依赖库,以避过依赖库的dex转换阶段,节省构建时间。
我们这里只是把依赖库安装在已经root过的设备上,这意味着这种方式无法应用于最终产品上,所以使用该方法的目的是为了节省开发阶段构建应用程序的时间。
第一步是对依赖库进行dex预处理,就是首先将JAR文件转化为Dex文件。可以通过ANDROID_SDK/tools目录下提供的dx工具完成上述操作。
示例:如果依赖库是dep.jar,我们需要运行以下命令
dx -JXmx1024M -JXms1024M -JXss4M
--no-optimize --debug --dex
--output=./dep_dex.jar dep.jar
我们会把生成的dep_dex.jar文件上传到设备中。
第二步是创建XML文件,为每个依赖库指定权限,代码如下:
<permissions>
<!-- 这里指定库命,指定需要dex预处理的文件路径 -->
<library name="dep" file="/data/data/com.dep.package/files/dep_dex.jar"/>
</permissions>
首先需要指定库命,该库命便是use-library标签中使用的库名。此外,还需要指定已经经过dex预处理的文件在设备上的存储路径。可以使用adb命令或通过一个android应用把经过dex预处理的文件上传到设备上。
最后一步是修改AndridManifest.xml文件,以便使用安装在设备上的依赖库。使用上面的dep库方法如下:
<uses-library name="dep"/>
还有要记得修改构建工具的配置,以避免编译这些依赖库。如:在Apache Maven中,可以将scope属性设置为provided。
这个方法的缺点:
1.需要一个root过的设备,但很遗憾并不是所有Android设备都经过root。
2.需要修改构建脚本以避免运行于目标产品上。
在这种情况下,Apache Maven可以作为处理不同类型构建过程的好工具。
外链地址1
外链地址2
外链地址3
在这里我们会用到一款名为Jenkins的软件——一种流行的开源可持续化集成服务器,并附带一个Android模拟器插件。
在Jenkins中,点击New Job后输入Job名称,选择Build Multiconfiguration Project(也称为Matrix Job)并点击OK按钮。
在Job配置信息中,首先要输入源码管理信息以便Jenkins可以签出(check out)应用程序并测试代码库。
为了Jenkins能够监控代码库的变化,需要开启周期性构建选项,输入cron风格的语句。
接下来,在构建过程中,点击“Run an Android Emulator during build”,并在”Run Emulator with Properties”中输入以下值:
1.Android操作系统版本:${os}
2.屏幕密度:240
3.屏幕分辨率:WVGA
都配置完成后点击Save按钮结束Job的配置过程。这时我们就拥有了一个Jenkins job。
在job页面的左侧面板点击Build Now,几秒后,就会看到一些球开始闪烁,这预示着一些“Configurations”正在构建。
同时,我们也可以通过点击某个闪烁的球,然后点击左侧面板的蓝色进度条来观察构建过程。
当Jenkins的侧边栏中的进度条消失时,构建过程就完成了。
外链地址1
外链地址2
外链地址3
外链地址4
外链地址5