Android开发权威指南(第二版)
上QQ阅读APP看书,第一时间看更新

5.8 与Activity相关的技巧与特效

经常安装第三方程序的读者会发现,很多程序并不是默认的显示效果,例如,有的程序是全屏显示,有的程序拥有半透明的背景或绚丽的标题栏。那么这些特效是如何做到的呢?本节将会为读者揭晓答案。

5.8.1 全屏显示(隐藏标题栏和状态栏)

源代码目录:src/ch05/FullScreen

全屏是最常用的特效,主要用于游戏以及不需要在标题栏、状态栏显示信息的应用中。全屏需要隐藏两部分内容:标题栏和状态栏。其中标题栏是属于窗口的一部分,而状态栏是属于Android系统的一部分,通常会在状态栏中显示各种通知信息以及网络、电池等硬件状态、当前时间等信息。最容易想到的全屏解决方案就是在Activity.onCreate方法中通过requestWindowFeature和setFlags方法隐藏标题栏和状态栏,代码如下:

源代码文件:src/ch05/FullScreen/src/mobile/android/fullscreen/FullScreenActivity.java

public class FullScreenActivity extends Activity

{  

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    // 隐藏窗口标题栏

    requestWindowFeature(Window.FEATURE_NO_TITLE);

    setContentView(R.layout.activity_full_screen);

    // 隐藏系统状态栏

    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,

        WindowManager.LayoutParams.FLAG_FULLSCREEN);

  }

}

在编写上面的代码时要注意requestWindowsFeature和setFlags方法的位置。由于requestWindowsFeature方法控制的是窗口的标题栏,而标题栏属于窗口,窗口上的所有控件(包括标题栏中的TextView等控件)都是由setContentView方法处理的,因此,requestWindowFeature方法必须在setContentView方法之前调用。而由于setFlags方法只是控制系统的UI,所以该方法可以在代码的任何位置调用。

除了使用Java代码隐藏窗口标题栏和系统状态栏外,还可以在AndroidManifest.xml文件中声明窗口时就设置窗口为全屏状态,或单独隐藏标题栏和状态栏。这里涉及一种主题技术,关于这种技术会在介绍资源的章节详细讨论,这里只要知道主题实际上就是将设置一组属性的配置代码放到一起,如果窗口使用了该主题,这些属性就都会被设置。主题主要应用在风格需要大量重用的窗口中。如果不使用主题,每一个使用同样风格的窗口就必须再次设置同样的属性,这样不仅容易出错,而且不易维护。

每一个<activity>标签都有一个android:theme属性,该属性用于设置窗口的主题。当然,该属性可以使用自定义的主题。但由于现在的任务是隐藏窗口标题栏和系统状态栏,所以可以使用系统自带的主题。如果只想隐藏窗口标题栏,可以使用Theme.NoTitleBar主题,如果想连系统状态栏一起隐藏,可以使用Theme.NoTitleBar.Fullscreen主题(系统未提供只隐藏系统状态栏的主题)。如果使用系统的主题,android:theme属性值必须按如下形式设置。

@android:style/主题名

下面的代码是设置窗口主题的例子(现在可以将前面给出的Java代码注释掉了,因为不再需要它们了)。

只隐藏窗口标题栏。

<activityandroid:theme="@android:style/Theme.NoTitleBar"

  android:name="mobile.android.fullscreen.FullScreenActivity">

……

</activity>

隐藏窗口标题栏和系统状态栏(全屏状态)。

<activityandroid:theme="@android:style/Theme.NoTitleBar.Fullscreen"

  android:name="mobile.android.fullscreen.FullScreenActivity">

……

</activity>

如果应用程序中所有的窗口都采用了同一个主题,可以直接通过<application>标签的android:theme属性设置这个主题,代码如下:

<applicationandroid:theme="@android:style/Theme.NoTitleBar" >

……

</application>

如果不想使用系统提供的主题,还可以自定义主题。例如,可以在res/values目录建立一个styles.xml文件(新版的ADT在创建Android工程时会自动生成该文件),并在<resources>标签中加入如下的<style>子标签。

源代码文件:src/ch05/FullScreen/res/values/styles.xml

// Theme.FullScreen为自定义主题名

<style name="Theme.FullScreen" parent="android:Theme">

  <!-- 隐藏标题栏 -->

  <item name="android:windowNoTitle">true</item>

  <!-- 隐藏状态栏 -->

  <item name="android:windowFullscreen">true</item>

</style>

然后可以使用下面的形式设置<activity>标签的android:theme属性。

<activityandroid:theme="@style/Theme.FullScreen"

  android:name="mobile.android.fullscreen.FullScreenActivity">

……

</activity>

源码分析:为什么requestWindowFeature必须放在setContentView之前?

前面只说明了使用requestWindowFeature方法隐藏窗口标题栏时必须在调用setContentView之前完成,那么这是为什么呢?现在就从Android SDK源代码的角度为读者解开这个谜团。

首先要了解窗口上的控件(包括标题栏)是如何被放上去的。可能很多读者马上会想到,一定是setContentView方法搞的鬼,因为在onCreate方法中首先会通过setContentView指定在窗口上显示的根视图(View对象或布局资源ID)。那么现在就来看看setContentView方法是如何做的。

现在不妨跟踪进setContentView方法(需要Android SDK源代码)一探究竟。一开始会进入Activity.java文件的setContentView方法(按住Ctrl键,用鼠标单击要跟踪的方法即可定位到相应文件的指定源代码),代码如下:

public void setContentView(int layoutResID) {

  getWindow().setContentView(layoutResID);

  initActionBar();

}

setContentView方法有两行代码,但我们目前只关注如何将View与窗口关联,而第2行是初始化ActionBar的,先不去管它。现在继续跟踪第1行代码的setContentView方法,定位到Window.java的setContentView方法,代码如下:

public abstract void setContentView(int layoutResID);

可能很少分析面向对象语言代码的读者遇到这种情况下不知道如何办,因为Window是一个抽象类,而我们关注的setContentView方法恰巧是一个抽象方法,这样就无法再继续跟踪下去了。

解决的办法也很简单,就是在Android源代码中找到哪个Java类继承了Window抽象类。不过我想不会有人从Android源代码的上百万个文件中寻找这个类吧,所以就需要采用另外的方法来查找Window的子类。

有一种比较简单的方法就是直接在窗口类的任何位置使用下面的代码输出Window对象(因为getWindow方法返回了Window对象)。

Log.d("Window Sub Class", String.valueOf(getWindow()));

执行上面的代码后,会在LogCat视图中输出如下的信息。

com.android.internal.policy.impl.PhoneWindow@40ffbad0

后面的@和16进制数不需要管它,我们要找的就是PhoneWindow类。但从该类所在的包(com.android.internal.policy.impl)可以看出,PhoneWindow是一个内部类,我们不能直接在Eclipse中跟踪定位,当然更不能直接使用(内部类只有Android SDK自身以及内嵌的应用才能使用),所以可以利用操作系统的文件搜索功能来查找PhoneWindow.java文件。找到后很容易定位到PhoneWindow.java文件中的setContentView方法,代码如下。

public void setContentView(int layoutResID) {

  if (mContentParent == null) {

    // 设置窗口标题栏

    installDecor();

  } else {

    mContentParent.removeAllViews();

  }

  // 装载窗口根视图

  mLayoutInflater.inflate(layoutResID, mContentParent);

  ……

}

setContentView方法的代码主要分为两部分:installDecor和inflate方法,分别用来设置标题栏和装载窗口根视图。inflate方法在这里并不需要管它。这里只考虑installDecor方法。现在跟踪进installDecor方法,该方法的代码很多,这里我们就不给出installDecor方法的全部代码了,但我们却可以在该方法中找到如下的代码片段。从这段代码中可以看出会根据FEATURE_NO_TITLE标志来决定是否通过setVisibility方法隐藏窗口标题栏。从这些分析我们断定,setContentView方法不仅将根视图与窗口绑定,而且还根据某些特性设置了标题栏的各种状态。现在我们心里有底了,知道了setContentView方法的工作原理,以后在使用requestWindowFeature时会毫不犹豫地放在setContentView方法前面。

if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {

  View titleContainer = findViewById(com.android.internal.R.id.title_container);

  if (titleContainer != null) {

     // 隐藏窗口标题栏的顶层视图

    titleContainer.setVisibility(View.GONE);

  } else {

     // 隐藏窗口标题栏的顶层视图

    mTitleView.setVisibility(View.GONE);

  }

  ……

}

扩展学习:系统的主题到哪去找?

前面学习了如何使用系统主题使窗口全屏,那么我们怎么知道系统有哪些主题呢?其实获得这些主题信息要比前面分析Android源代码简单多了,当然,这里并不需要Android源代码,而只需要Android SDK即可。现在先进入如下的目录。其中android-17表示API Level = 17的Android版本(Android4.2),当然,也可以进入其他的Android版本,如android-16(Android 4.1.2)。由于常用的系统主题很早就提供了,所以版本之间的差异并不大。

<Android SDK根目录>/platforms/android-17/data/res/values

进入values目录后,在该目录中找到themes.xml文件,所有的系统主题都在该文件中。读者可以打开themes.xml文件,定位到Theme.NoTitleBar和Theme.NoTitleBar.Fullscreen主题,会看到如下的代码。如果读者对主题的概念和使用还有些模糊,可以阅读11.2节的内容。

<style name="Theme.NoTitleBar">

  <item name="android:windowNoTitle">true</item>

</style>

<!-- Theme.NoTitleBar.Fullscreen主题继承自Theme.NoTitleBar主题 -->

<!-- 主题的继承通常是通过“.”的方式,如a.b.c继承至a.b -->

<style name="Theme.NoTitleBar.Fullscreen">

  <item name="android:windowFullscreen">true</item>

  <item name="android:windowContentOverlay">@null</item>

</style>

在themes.xml文件中还可以找到更多的主题,例如,Theme.Light、Theme.Light.NoTitleBar等,具体用法可以看看其中是如何设置窗口属性的。

5.8.2 定制窗口标题栏

源代码目录:src/ch05/CustomTitle

如果对Android默认的窗口标题栏不满意,那就换上我们自己的标题栏吧,这一过程被称为定制窗口标题栏。定制标题栏的方法很多,在本节将给出如下4种定制标题栏的方式。

直接修改标题栏上的控件。

修改窗口主题。

修改标题栏的布局文件。

隐藏标题栏,完全在窗口布局中模拟标题栏。

在讨论这3种定制标题栏的方法之前先看一下本例的主界面,如图5-40所示。单击界面的4个按钮后可以分别测试上述4种定制标题栏的方式。

 

▲图5-40 程序主界面

1.直接修改标题栏上的控件

这种方法可能是最容易想到的,因为标题栏本身也是一个布局,既然是布局,就很容易获取其中的控件。例如,要修改标题栏文本字体颜色、文本背景颜色、字体大小等属性,就需要直接获取显示标题文本的TextView控件由于Android SDK API并不能直接设置这些属性,所以只能直接操作相应的控件了。,不过还有一个问题,就是Android SDK未提供获取标题栏中控件的API,所以就需要一些技巧。

由于Window.getDecorView方法可以获取窗口最顶层的视图对象,标题布局和窗口中控件的布局都在该视图中,所以理论上通过控件的ID就可以获取相应的视图对象(类型强行转换后就可以变成相应控件类的对象),不过对于显示标题文本的TextView控件的ID我们并不知道,就算知道了也不建议使用,因为这是Android内部使用的常量,很有可能不同Android版本的ID不一样,所以需要使用其他方法来获取该TextView控件。

本例采用的方法是通过递归扫描窗口的视图结构树获取TextView控件。由于标题栏的布局会在窗口布局的前面,所以发现的第一个TextView控件就是用于显示标题文本的TextView控件。当然,如果成功获取了TextView控件,接下来的工作就没什么悬念了,使用常规方法直接设置TextView控件的相应属性即可。递归搜索TextView控件及设置该控件相应属性的代码如下:

源代码文件:src/ch05/CustomTitle/src/mobile/android/custom/title/ChangeTitleBarActivity.java

public class ChangeTitleBarActivity extends Activity

{  

  // 显示标题文本的TextView控件

  private TextView mTextView;

  // 递归查找显示标题文本的TextView控件,如果找到,将该TextView对象保存在mTextView变量中

  private void getTextView(ViewGroup vg)

  {

    // 获取当前容器视图中子视图的个数

    int count = vg.getChildCount();

    // 扫描所有子视图

    for (int i = 0; i < count; i++)

    {

      // 获取当前子视图

      View view = vg.getChildAt(i);

      // 如果当前视图是容器视图,继续递归搜素TextView 

      if (view instanceof ViewGroup)

      {

         // 递归调用getTextView方法

        getTextView((ViewGroup) view);

      }

      // view不是容器视图

      else if (view instanceof View)

      {

        // 判断view是否为TextView对象,并且是第一个TextView

        if (view instanceof TextView && mTextView == null)

           // 将TextView对象保存起来

          mTextView = (TextView) view;

      }

    }

  }

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_change_title_bar);

    Window w = getWindow();

    // 获取窗口的顶层视图对象

    ViewGroup v = (ViewGroup) w.getDecorView();

    // 获取显示标题文本的TextView对象

    getTextView(v);

    if (mTextView != null)

    {

      // 下面的代码设置了TextView控件的相应属性

      mTextView.setText("标题文本已变化");

      mTextView.setTextColor(Color.RED);

      mTextView.setBackgroundColor(Color.BLUE);

      mTextView.setClickable(true);

      // 为标题文本添加单击事件

      mTextView.setOnClickListener(new OnClickListener()

      {

        @Override

        public void onClick(View v)

        {

          Toast.makeText(ChangeTitleBarActivity.this, "单击标题文字",

              Toast.LENGTH_LONG).show();

        }

      });

    }

  }

}

现在运行程序,单击“直接修改标题栏”按钮,会显示如图5-41所示的窗口标题栏,单击标题文本,会显示Toast信息框。

 

▲图5-41 直接设置窗口标题栏的效果

2.修改窗口主题

主题资源需要在res/values或其他相关的本地化目录中的任何xml文件中定义。本例在res/values/styles.xml文件定义主题资源,代码如下:

源代码文件:src/ch05/CustomTitle/res/values/styles.xml

<resources>

  <!-- 设置标题的背景色(以一个图像作为背景) -->

  <style name="WindowTitleBackground">

  <item name="android:background">@drawable/bg</item>

  </style>

  <!-- 要设置到窗口的主题,该主题继承自android:Theme -->

  <!-- 主题继承除了使用“.”外,还可以通过parent属性指定父主题 -->

  <style name="MyTheme" parent="android:Theme">

  <item name="android:windowTitleSize">50dp</item>

  <item name="android:textColor">#FF0000</item>

  <item name="android:textSize">30sp</item>

  <item name="android:windowTitleBackgroundStyle">@style/WindowTitleBackground</item>

  </style>

  ……

</resources>

从这段代码可以看出,一个主题是由一个<style>标签定义的,其中主题可以继承。每一个<item>属性负责设置一个属性。本例设置了如下4个窗口属性。

标题栏尺寸(高度):android:windowTitleSize。

文本颜色:android:textColor(包括标题文本和窗口布局区域的文本颜色)。

文本尺寸:android:textSize(包括标题文本和窗口布局区域的文本尺寸)。

标题背景:android:windowTitleBackgroundStyle。

在主题中还涉及了两个长度单位:dp和sp。这两个长度单位是与像素无关的,关于它们的细节将在10.4.2小节详细介绍。

为了在窗口类(ThemeTitleActivity)中使用MyTheme主题,需要在AndroidManifest.xml文件中声明窗口类时指定MyTheme主题,代码如下:

源代码文件:src/ch05/CustomTitle/AndroidManifest.xml

<activity

  android:name=".ThemeTitleActivity"

  android:label="修改窗口主题"

  android:theme="@style/MyTheme" />

ThemeTitleActivity类不需要编写一行代码就可以修改标题背景、标题文本的大小和颜色,现在运行程序,单击“改变窗口主题”按钮,会显示如图5-42所示的效果。

 

▲图5-42 修改窗口主题

不仅可以在声明窗口时指定主题(静态设置主题),还可以动态设置主题,代码如下:

源代码文件:src/ch05/CustomTitle/src/mobile/android/custom/title/ThemeTitleActivity.java

public class ThemeTitleActivity extends Activity

{  

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    // 动态设置主题

    setTheme(R.style.MyTheme);

    setContentView(R.layout.activity_theme_title);

  }

}

设置窗口主题应注意如下几点。

动态设置主题的优先级高于静态设置主题。

setTheme方法必须在setContentView之前调用。

3.修改标题栏的布局文件

直接通过主题修改窗口属性的方式修改标题栏还不是太灵活,功能也有限,而我们即将介绍的是通过完全定制标题栏布局的方式修改标题栏,这种方式可以在标题栏上摆放任何类型的控件,控件的属性也可任意设置。因为标题栏的布局已经完全在我们的掌握之中。

下面先看一下定制标题栏的效果,如图5-43所示。

 

▲图5-43 修改标题栏的布局文件

这个标题栏的布局文件中只包含了两个控件:TextView和ImageView,并且水平排列。下面的按钮并不属于标题栏。

标题栏布局文件是new_title_bar.xml,代码如下:

源代码文件:src/ch05/CustomTitle/res/layout/new_title_bar.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="match_parent"

  android:layout_height="wrap_content"

  android:orientation="horizontal" >

  <TextView

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="哈哈,终于把图标放标题右边了" android:textColor="#FFF"/>

  <ImageView

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:src="@drawable/ic_launcher"

    android:text="设置标题" />

</LinearLayout>

标题栏为了使用新的布局文件,需要在窗口类(ChangeTitleLayoutActivity)中设置窗口的Feature,并指定布局文件的资源ID,代码如下:

源代码文件:src/ch05/CustomTitle/src/mobile/android/custom/title/ChangeTitleLayoutActivity.java

public class ChangeTitleLayoutActivity extends Activity

{  

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    // 通知窗口要使用定制的标题布局,必须在setContextView方法之前设置

    requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);

    setContentView(R.layout.activity_change_title_layout);

    // 为标题栏设置新的布局文件

    getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,

        R.layout.new_title_bar);

  }

}

由于从Android 3.0开始引入了ActionBar(会第18章会详细介绍)的概念,并且ActionBar和定制标题无法同时使用,所以要修改窗口的默认主题。

首先在styles.xml文件中定义如下的主题资源(该主题并未使用Action样式),在这些资源中设置了文本尺寸、标题栏尺寸(高度)和标题栏背景颜色。

源代码文件:src/ch05/CustomTitle/res/values/styles.xml

<style name="WindowTitleBackground1">

  <item name="android:background">#000</item>

</style>

<style name="MyTheme1" parent="android:Theme">

  <item name="android:textSize">30sp</item>

  <item name="android:windowTitleSize">50dp</item>

  <item name="android:windowTitleBackgroundStyle">@style/WindowTitleBackground1</item>

</style>

最后在声明ChangeTitleLayoutActivity类时需要指定MyTheme1主题,代码如下:

源代码文件:src/ch05/CustomTitle/AndroidManifest.xml

<activity

  android:name=".ChangeTitleLayoutActivity"

  android:label="@string/title_activity_change_title_layout"

  android:theme="@style/MyTheme1" />

现在运行程序,然后单击“修改标题的布局文件”按钮,就会显示带有如图5-43所示的标题栏的窗口。

4.隐藏标题栏,完全在窗口布局中模拟标题栏

如果不想使用窗口本身的特性定制标题栏,干脆用5.8.1小节的方式将原来的标题栏隐藏起来,完全使用窗口布局文件来模拟标题栏,效果如图5-44所示。

 

▲图5-44 模拟标题栏

这种定制标题栏的方式没有什么悬念,其核心就是在窗口类(HideOldTitleBarActivity)中将标题栏隐藏,然后在窗口布局尽管现在还没有详细介绍布局,但已经多次涉及了布局的使用,即使读者目前还无法理解布局也不要紧,在第8章会详细介绍布局的实现。读者也可以直接阅读第8章的内容来了解布局的细节。的顶端放一个一定高度的子布局来模拟标题栏。本例的布局文件代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

  android:orientation="vertical" >

  <!-- 下面的线性布局相当于标题栏 -->

  <LinearLayout

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:background="@drawable/bg"

    android:orientation="horizontal" >

    <TextView

      android:layout_width="wrap_content"

      android:layout_height="match_parent"

      android:text="哈哈,终于把图标放标题右边了"

      android:textColor="#00F"

      android:textSize="16sp" android:gravity="center_vertical"/>

    <ImageView

      android:layout_width="wrap_content"

      android:layout_height="wrap_content"

      android:src="@drawable/ic_launcher"

      android:text="设置标题" />

  </LinearLayout>

  <!-- 下面的按钮相当于窗口中的控件 -->

  <Button

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:text="我的按钮" />

</LinearLayout>

现在运行程序,然后单击“隐藏原有的标题栏,添加新的标题栏”按钮,就会显示带有图5-44所示标题栏的窗口。

注意

如果使用模拟标题栏的方式定制标题栏,将无法使用setTitle方法设置标题文本,而要直接访问显示标题文本的控件才可以设置标题文本。读者可根据需要选择合适的定制标题栏的方式。

5.8.3 为程序添加Splash

源代码目录:src/ch05/Splash

有很多Android程序在启动时首先会显示一个封面(Splash窗口),过一会才进入主界面。Splash窗口通常是全屏的,一般会显示与程序相关的Logo,公司标识等内容。实现这个功能的方法很多,本节将给出一个比较简单且灵活的实现方法。

基本的实现方法是将Splash单独作为一个窗口,可以在该窗口中处理程序要装载的数据,或人为地延迟一定的时间,然后显示主窗口,并关掉Splash窗口。

由于Splash窗口的特征是隐藏标题栏,并且全屏,所以需要在res/values/styles.xml文件中定义一个主题,代码如下:

源代码文件:src/ch05/Splash/res/values/styles.xml

<style name="Splash">

  <item name="android:windowBackground">@drawable/splash</item>

  <item name="android:windowNoTitle">true</item>

  <item name="android:windowFullscreen">true</item>

</style>

在Splash主题中除了使窗口全屏外,还设置了窗口的背景图,该图像就是Splash窗口要显示的图像。当然,还可以在窗口上添加更复杂的元素,如动画。但这些都不是本节的重点(学完本书的所有内容,这些特效就都很容易完成了),本节主要讨论如何使Splash窗口停留指定的时间后显示另外一个窗口。

本例的关键是Splash窗口类(SplashActivity)的实现。最简单的方法就是在onCreate方法中加一个延迟,在一定时间(本例是2秒)后执行另一段代码,代码如下:

源代码文件:src/ch05/Splash/src/mobile/android/splash/SplashActivity.java

public class SplashActivity extends Activity

{  

  // 延迟2秒

  private final int SPLASH_DISPLAY_LENGHT = 2000;

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    // 使用Handler.postDelayed时代码延迟2秒后执行,Handler对象会在后面多线程的章节详细介绍

    new Handler().postDelayed(new Runnable()

    {

      // 2秒后系统会调用run方法

      @Override

      public void run()

      {

        Intent mainIntent = new Intent(SplashActivity.this,

            MainActivity.class);

        // 显示主窗口

        SplashActivity.this.startActivity(mainIntent);

        // 关闭当前窗口

        SplashActivity.this.finish();

      }

    }, SPLASH_DISPLAY_LENGHT);

  }

}

现在运行程序,首先会显示2秒钟的Splash窗口(全屏),然后会显示主窗口。

注意

如果读者不想无缘无故使Splash延迟,而只在Splash窗口显示时异步装载数据,数据装载完毕后立刻关闭Splash窗口,并进入主窗口。也可以直接开启一个线程,在线程中装载数据,然后执行上面run方法中的内容即可。

5.8.4 改变窗口大小、位置和透明度

源代码目录:src/ch05/ChangeActivityProperty

在Android系统中窗口不仅仅可以充满整个屏幕,还可以像对话框一样在屏幕的某个区域显示(改变窗口的大小和位置),而且还可以设置窗口的透明度。例如,图5-45所示的效果就是改变了窗口的尺寸(宽度:200像素,高度:300像素),而图5-46所示的是在滑动滑杆后使窗口半透明的效果。

 

▲图5-45 改变窗口的尺寸和位置

 

▲图5-46 半透明窗口

本例使用了一个SeekBar控件来控制窗口从不透明到透明的过程,该控件会在后面控件详解部分进行介绍。在这里只是简单使用了该控件。SeekBar又称滑杆控件,可以左右滑动控制一个整数值,本例的滑动范围是0至100。0表示完全透明,100表示不透明。放置SeekBar控件的布局文件是activity_change.xml,具体代码如下:

源代码文件:src/ch05/ChangeActivityProperty/res/layout/activity_change.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

  android:orientation="horizontal" >

  <!-- SeekBar的最大值是100,当前位置是100,也就是滑杆的初始位置在最右侧 -->

<SeekBar

  android:id="@+id/seekbar"

  android:layout_width="match_parent"

  android:layout_height="wrap_content"

  android:layout_gravity="center_vertical"

  android:max="100" android:progress="100" />

</LinearLayout>

下面在窗口类的onCreate方法中改变窗口的尺寸和位置,并在SeekBar控件的滑杆变化事件方法中改变窗口的透明度,代码如下:

源代码文件:src/ch05/ChangeActivityProperty/src/mobile/android/change/activity/property/ ChangeActivity.java

public class ChangeActivity extends Activity implements OnSeekBarChangeListener

{  

  private SeekBar seekBar;

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_change);

    // 获取用于设置窗口属性的LayoutParams的对象

    WindowManager.LayoutParams layoutParams = getWindow().getAttributes();

    // 设置窗口的高度(200像素)

    layoutParams.height =200;

    // 设置窗口的宽度(300像素)

    layoutParams.width = 300;

    // 设置窗口垂直方向的位置

    layoutParams.y = 50;

    // 设置窗口垂直位置时必须设置Gravity.TOP或Gravity.BOTTOM

    layoutParams.gravity = Gravity.TOP;

    // 设置窗口属性

    getWindow().setAttributes(layoutParams);

    seekBar = (SeekBar)findViewById(R.id.seekbar);

    // 设置SeekBar控件的滑动监听器

    seekBar.setOnSeekBarChangeListener(this);

  }

  // 当滑杆滑动时调用该方法

  @Override

  public void onProgressChanged(SeekBar seekBar, int progress,

      boolean fromUser)

  {

    // 获取用于设置窗口属性的LayoutParams的对象

    WindowManager.LayoutParams layoutParams = getWindow().getAttributes();

    //设置窗口的透明度。alpha为0表示窗口完全透明,为1表示窗口不透明

    layoutParams.alpha = (float)progress/100;

    // 设置窗口属性

    getWindow().setAttributes(layoutParams);

  }

  @Override

  public void onStartTrackingTouch(SeekBar seekBar)

  {

  }

  @Override

  public void onStopTrackingTouch(SeekBar seekBar)

  {

  }

}

设置窗口属性时要注意如下几点。

Window. setAttributes方法在setContentView前后调用都可以。

LayoutParams.y会根据LayoutParams.gravity的值来确定具体的含义,如果LayoutParams.gravity的值是Gravity.TOP,LayoutParams.y表示窗口顶边到顶端的距离;如果为Gravity.BOTTOM,LayoutParams.y表示窗口底边到底端的距离。

如果LayoutParams.y表示窗口顶边到顶端的距离,这个顶端并不包括状态栏,也就是说窗口不会覆盖状态栏。

最后一步是通过主题设置窗口透明度前面的alpha实际上只是设置了窗口中View的透明度,也就是说在窗口的顶层视图下面还有一层容器,这里就是设置了这个容器的透明度。也就是使容器完全透明,否则窗口外部的区域将一片漆黑,无法看到窗口后面的内容。,主题的代码如下:

源代码文件:src/ch05/ChangeActivityProperty/res/values/styles.xml

<style name="Transparent" >

  <item name="android:windowBackground">@android:drawable/toast_frame</item>

  <!-- 设置窗口是透明的 -->

  <item name="android:windowIsTranslucent">true</item>

</style>

在声明窗口类时需要使用Transparent主题,代码如下:

源代码文件:src/ch05/ChangeActivityProperty/AndroidManifest.xml

<activity android:name=".ChangeActivity"android:label="@string/app_name"

    android:theme="@style/Transparent">

 ... ...

</activity>

现在运行程序,就会看到图5-45所示的效果,滑动滑杆,窗口和SeekBar控件都会逐渐变得透明,直到消失。

5.8.5 按两次关闭窗口

源代码目录:src/ch05/ActivityFinished

现在有很多程序在退出时(关闭主窗口)会弹出一个对话框确认或要求再按一次Back键才退出,这个功能是通过实现Activity.finish方法实现的。所以只要在自己的窗口类中在满足一定条件时调用Activity.finish方法,就可以在一定条件下阻止窗口的关闭。实现代码如下:

源代码文件:src/ch05/ActivityFinished/src/mobile/android/activity/finished/FinishedActivity.java

public class FinishedActivity extends Activity

{  

  // 按Back键关闭窗口的计数,本例要求按两次关闭窗口,所以只有count=2时才关闭窗口

  private int count = 1;

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_finished);

  }

  // 实现finish方法

  @Override

  public void finish()

  {

    // 已经按了第2次Back键

    if (count == 2)

    {

      // 调用父类的finish方法关闭窗口,如果不调用super.finish方法,窗口是不会关闭的

      super.finish();

    }

    else

    {

      Toast.makeText(this, "再按一次退出程序", Toast.LENGTH_LONG).show();

      // 按第1次Back键,计数器增1

      count++;

    }

  }

}

现在运行程序,然后按Back键,会显示Toast信息框,要求再按一次Back键,再一次按Back键后程序退出。也可以在finish方法中通过对话框确认的方式决定是否调用super.finish方法关闭当前窗口。

5.8.6 关闭所有的窗口

源代码目录:src/ch05/CloseAllActivity

有一些程序要求从某一个窗口返回主窗口,并且关闭其他所有的窗口。从表面上看这个功能并不复杂,但由于无法直接获取窗口的实例,所以还需要费一番周折其实实现这个功能并不麻烦,只是现在还没有讲到Activity的高级功能,所以只能用笨办法实现,在7.2.4小节介绍窗口的singleTask模式时会给出另外一种比较简便地关闭其他窗口的方法。

既然无法获取窗口的实例,就在窗口初始化时将当前窗口的实例保存在静态变量中,当需要关闭某个窗口时,直接通过这些静态变量获取窗口实例即可。下面看一下本例的完整实现代码。

源代码文件:src/ch05/CloseAllActivity/src/mobile/android/close/all/activity/CloseAllActivity.java

public class CloseAllActivity extends Activity

{  

  // 用于保存已显示窗口的实例的List对象

  private static List<Activity> activities = new ArrayList<Activity>();

  // 表示已显示的窗口个数

  private static int count = 0;

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_close_all);

    if (count == 0)

    {

      // 显示主窗口(第一个窗口),设置标题

      setTitle("主窗口");

      count++;

    }

    else

    {

      // 设置其他窗口的标题

      setTitle("窗口" + count);

      count++;

    }

    // 将当前窗口实例添加到List对象中

    activities.add(this);

  }

  // “显示新窗口”按钮的单击事件方法

  public void onClick_ShowNewActivity(View view)

  {

    Intent intent = new Intent(this, CloseAllActivity.class);

    startActivity(intent);

  }

  // "返回主窗口,并关闭除了主窗口外的所有窗口"按钮的单击事件方法

  public void onClick_CloseAllActivity(View view)

  {

    // 扫描除了主窗口的所有窗口实例,并关闭这些窗口

    for (int i = 1; i < activities.size(); i++)

    {

      if (activities.get(i) != null)

      {

         // 关闭窗口

        activities.get(i).finish();

      }

    }

    // 重置计数器

    count = 1;

  }

}

现在运行程序,然后单击几次“显示新窗口”按钮,例如,图5-47是单击4次该按钮的效果,表明已经显示了4个窗口(不包括主窗口)。现在单击“返回主窗口,并关闭除了主窗口外的所有窗口”按钮,会关闭这4个窗口,并返回主窗口。

 

▲图5-47 关闭除主窗口外的所有窗口

5.8.7 窗口截屏

源代码目录:src/ch05/CaptureScreen

Android SDK还支持一个很有趣的功能,就是可以对当前界面截屏,并保存到Bitmap对象中。图5-48是本例的主界面,单击“屏幕截图(保存到SD卡根目录)”按钮,就会将当前界面(不包括状态栏)保存到SD卡的某个jpg图像文件中。

截屏并保存成图像文件的基本步骤如下。

第1步:获取存储屏幕图像的Bitmap对象。

第2步:获取状态栏高度。由于无法获取状态栏图像,所以必须将状态栏从屏幕图像文件中去除,否则图像上方的状态栏位置会显示一条白色的区域。

第3步:获取屏幕图像的高度和宽度。

第4步:建立一个新的Bitmap对象(已去除状态栏的高度),将屏幕图像中除了状态栏部分的其他区域复制到该图像中。

第5步:将新建立的Bitmap对象保存到SD卡的根目录。

 

▲图5-48 截屏主界面

实现截屏的代码如下:

源代码文件:src/ch05/CaptureScreen/src/mobile/android/capture/screen/CaptureScreenActivity.java

public class CaptureScreenActivity extends Activity

{  

  @Override

  protected void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_capture_screen);

  }

  public void onClick_CaptureScreen(View view)

  {

    // 本例使用了延迟运行,以防止单击按钮时还没抬起就截取屏幕图像

    new Handler().postDelayed( new Runnable()

    {

      public void run()

      {

        View v = getWindow().getDecorView();

        v.setDrawingCacheEnabled(true);

        v.buildDrawingCache();

        // 第1步,获取保存屏幕图像的Bitmap对象

        Bitmap srcBitmap = v.getDrawingCache();

 

        Rect frame = new Rect();

        getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);

        // 第2步:得到状态栏的高度

        int statusBarHeight = frame.top;

 

        // 第3步:获取屏幕图像的高度

        Point outSize = new Point();

        getWindowManager().getDefaultDisplay().getSize(outSize);

        int width = outSize.x;

        int height = outSize.y;

 

        // 第4步:创建新的Bitmap对象,并截取除了状态栏的其他区域

        Bitmap bitmap = Bitmap.createBitmap(srcBitmap, 0, statusBarHeight, width,height - statusBarHeight);

        v.destroyDrawingCache();

 

 

        FileOutputStream fos = null;

        try

        {

          // 为了使每次保存图像的文件名不同,这里使用临时文件来生成相应的文件名

          File file = File.createTempFile("capture", ".jpg", new File(

              "/sdcard"));

          fos = new FileOutputStream(file);

          if (null != fos)

          {

            // 第5步:将屏幕图像保存到SD卡的根目录

            bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);

            fos.flush();

            Toast.makeText(CaptureScreenActivity.this, "已成功截屏,截屏文件名:"+ file.getName(),Toast.LENGTH_LONG).show();

          }

          fos.close();

        }

        catch (Exception e)

        {

        }

      }

    }, 2000); // 延迟2秒截屏

 

  }

}

现在运行程序,单击“屏幕截图(保存到SD卡根目录)”按钮,在SD卡根目录就会出现相应的JPG图像文件。读者可以自行查看这些图像文件的效果。