Activity是Android组件中最基本也是最常用的组件之一,Activity是一个应用程序组件,提供一个屏幕,用户可以用来交互来完成某项任务。Activity中的所有操作都与用户密切相关,是一个负责与用户交互的组件,在一个Android应用中,一个Activity通常就是一个单独的屏幕,它上面可以显示一些控件也可以监听并处理用户的事件做出响应,这段定义出自百度百科。本节主要是针对Activity的生命周期做个简单的介绍。
说到Activity就不得不提下其生命周期过程,不过估计每个学安卓的都知道,在其生命周期内,activity在运行、暂停、停止三种状态间进行转换,而我们所说的onCreate()等方法就是activity在状态切换时由系统调用的,在这里我们需要注意的是,onCreate()方法调用时期对用户来说是不可见的,其调用是在创建activity实例后,但在此实例出现在屏幕之前。
还有一点就是当ActivityA跳转到ActivityB的时候,两者的生命周期过程我们也应该清楚
在我们的实际开发中,经常会需要考虑用户横竖屏切换之后是否会对程序产生影响,下面是横竖屏切换后生命周期的过程
如果你不想关闭横竖屏切换功能,但又不想Activity在屏幕旋转的时候重新创建,那么我们可以给Activity指定configChanges属性
android:configChanges="orientation|keyboardHidden|screenSize"
其中orientation表示屏幕方向发生了改变;keyboardHidden表示键盘的可访问性发生了改变,比如用户调出了键盘;screenSize表示当编译时minSdkVersion和targetSdkVersion不低于或不全低于13时,当屏幕的尺寸信息发生了改变时,会导致Activity重建。
在这里我们看下当存在对话框的情况下,Activity的生命周期过程,所以这里简单实现一个AlterDialog,实现代码也很简单
public class ActivitySummary3 extends AppCompatActivity {
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_summary3);
findViewById(R.id.tv_hw).setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
createDialog().show();
}
});
}
private Dialog createDialog() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_show_helloworld, null);
return new AlertDialog.Builder(this)
.setView(view)
.setTitle("Hello World Dialog").setPositiveButton(android.R.string.ok, null)
.create();
}
}
我们点击按钮弹出对话框,我们会发现,生命周期…哦?其实生命周期并没有变化,有点意外啊,并不是自己所想的Activity会失去焦点onPause()。当取消对话框的时候,Activity生命周期的几个方法同样没有调用。当存在对话框的Activity按Home键回到桌面随后再启动App后(未销毁),其生命周期与没有对话框时一样。
AlterDialog的出现与否对于Activity的生命周期来说并没有影响,我随后又拿系统的权限对话框测试了一下,情况又有所不同:当系统的提示框出现时,Activity失去焦点调用onPause()方法,当系统的提示框消失时,Activity获得焦点调用onResume()方法,所以系统框出现的时候,对于Activity的生命周期还是有影响的,这和AlterDialog不同。
当ActivityA跳转至ActivityB的时候,如果ActivityB设置为透明主题,那么跳转成功后虽然此时处于B界面,但我们仍能看到B界面之下的ActivityA界面布局,所以这就对于ActivityA的生命周期产生了影响,接下来我们来看一个具体的例子:
首先我们需要将ActivitySummary7的主题设置为透明背景,半透明效果也一样,在styles.xml中代码如下:
<style name="myTransparent" parent="@android:style/Theme.Translucent.NoTitleBar">
<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
在我们的colors.xml中设置透明颜色
<color name="transparent">#00000000</color>
随后在我们的清单中,给ActivitySummary7设置主题
<activity android:name=".ActivitySummary7"
android:theme="@style/myTransparent"
>
我们先来看下,从启动ActivitySummary4 到 跳转至ActivitySummary7的过程中 两个Activity所经历的生命周期过程
由图示所知,由于ActivitySummary7的主题透明导致跳转后ActivitySummary4的界面仍然可见,所以造成跳转后的ActivitySummary4并不会执行onStop()方法,按返回键的时候ActivitySummary4同样也就不会执行onRestart()和onStart()方法了。正是由于这种情况,也就造成了一个很好玩的现象:
值得注意的是,不仅透明主题如此,Theme.AppCompat.Light.Dialog等窗口主题同样如此,其实我们想想其中缘由也就明白了,当窗口Activity出现后,我们依然能够看到上一个Activity
启动模式在多个Activity的跳转过程中有着重要的作用,它包括standard、singleTop、singleTask、singleInstance四种
standard启动模式是我们默认的Activity启动模式,所以我们的清单中android:launchMode=”standard”可写可不写,不是必须的
<activity android:name=".ActivitySummary3"
android:launchMode="standard"
/>
在这种模式下,当我们每启动一个Activity,系统都会创建一个相应的activity实例,并把它们按照”先进后出”的原则放入任务栈中,当我们按back键之后,系统就会按照”后进先出”(也就是先进后出)的原则从任务栈中不断移除掉相应activity,直到任务栈为空,随后系统回收该任务栈。同时我们还需要注意,每个Activity都会被放入其相应任务栈之中,不会游离其外,刚进入App后的第一个Activity同样也不例外,不仅如此,在这种模式下,谁启动了一个Activity,那么那个Activity就运行在启动它的那个Activity所在栈中。我们在代码中打印一下信息
Log.d("TAG",getClass().getSimpleName()+" 内存地址:"+this+",taskId:"+getTaskId());
结果如下图:
从上面我们不难看出,每当我们启动了一个Activity时,系统都会创建一个新的对象,即使是自身跳转到自身也同样不例外,并本着先进后出的入栈出栈原则。
在这种模式下,如果待启动的Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,直接会被系统复用;如果待启动的Activity在栈内不存在或者存在实例但不位于栈顶,那么系统就会创建一个新的此Activity实例,下面我们就来具体的看下Activity的入栈出栈情况,其中我们设置ActivitySummary6的启动模式为singleTop:
<activity android:name=".ActivitySummary6"
android:launchMode="singleTop"
/>
或者我们也可以在代码中设置Flags,而且此种方式优先级高于在清单activity中设置
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
我们同样打印一些信息,具体看下:
当我们待启动的Activity不在栈顶时,如ActivitySummary4>>ActivitySummary6>>ActivitySummary5>>ActivitySummary6,具体情况与standard启动模式情况一致。还有一点需要注意的是,当我们发现Activity位于栈顶并直接复用时,就会调用onNewIntent(Intent intent)方法,我们可以在此方法中获得Intent中携带的数据。onNewIntent(Intent intent)在onResume()之前调用。
在这种模式下,只要待启动的Activity在一个栈中存在,那么系统就不会再重新创建此Activity的实例,而是直接复用,在这种情况下,如果此Activity当前位于栈顶,则操作与singleTop一样,直接复用即可,而如果此Activity当前没有位于栈顶,那么系统会强制让当前栈内此Activity上面的其他Activity全部出栈,最后的结果就是此Activity位于了栈顶能被直接复用,因为别的Activity都被出栈了;还存在一种情况,即如果待启动的Activity在栈内不存在,那么系统就正常的创建一个Activity实例即可。
同时需要注意的是,singleTask为栈内复用模式,假设现在有两个任务栈分别为任务栈A和任务栈B,其中任务栈A中存在ActivityA,任务栈B中存在ActivityB,现在我需要启动一个singleTask模式的ActivityA放在任务栈B内,那么系统就会检查任务栈B中是否存在ActivityA,如果不存在,就会立即新建一个ActivityA对象放入任务栈B之内,并不会复用任务栈A中的ActivityA。下面就来看下Activity具体的入栈出栈情况,其中ActivitySummary6为singleTask启动模式:
<activity android:name=".ActivitySummary6"
android:launchMode="singleTask"
/>
或者我们也可以在代码中设置Flags
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
我们同样打印一些信息,具体看下:
上图说明ActivitySummary5在启动ActivitySummary6的时候,由于ActivitySummary6不在栈顶,所以系统移除了ActivitySummary6栈上面的ActivitySummary5,使ActivitySummary6来到了栈顶,所以当我们再次点击返回键移除掉ActivitySummary6对象时,就会直接回到ActivitySmmary4界面。还有一点我们需要注意的是,当待启动Activity存在时,不管位于栈顶与否,当复用此Activity时,都会调用onNewIntent(Intent intent)方法,且生命周期与singleTop一样。
在这种模式下,其除了具有singleTask模式的所有特性外,还有一点那就是,如果待启动的Activity不存在,那么系统会为它创建一个新的任务栈,然后将Activity独自放在这个任务栈中,由于singleInstance也具备栈内复用原则,所以只要新的任务栈不被系统所销毁就不会重新创建Activity的实例,下面来看下Activity具体的入栈出栈情况,其中ActivitySummary6为singleInstance启动模式,需要注意的是singleInstance模式我们只能在清单中设置,系统不支持在代码中直接设置:
<activity android:name=".ActivitySummary6"
android:launchMode="singleInstance"
/>
我们同样打印一些信息,具体看下:
隐式启动一个Activity需要我们的Intent能够匹配待启动Activity的IntentFilter中所设置的过滤信息,如果与之不匹配则无法启动该Activity。IntentFilter中的过滤信息包括action、categoty、data三种,下面我们来具体分析一下。
这是我们的ActivitySummary6在清单中的< activity >代码,注意当不想设置category特定值时,也需要将其设置为android.intent.category.DEFAULT,否则报错
<activity android:name=".ActivitySummary6">
<intent-filter>
<action android:name="zmj.componentssummary.06"/>
//当不想添加category时,也得添加DEFAULT,否则报错,原因如下
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
我们在ActivitySummary5界面启动上面的ActivitySummary6界面,代码如下
Intent intent = new Intent();
intent.setAction("zmj.componentssummary.06");
startActivity(intent);
当我们没有调用addCategory()方法时,系统会默认调用intent.addCategory(“android.intent.category.DEFAULT”),这也是我们在清单中不加category为什么会报错的原因。当然在清单< activity >中,我们也可以定义多个action值,匹配规则即当我们Intent所携带的action值能与其中一个action值完全相同即可算匹配成功。
这是我们的ActivitySummary5在清单中的< activity >代码,注意在< intent-filter >标签里,< action > 和 < category >标签需要同时存在,否则会报错
<activity android:name=".ActivitySummary5">
<intent-filter>
//必须存在action,当只有categary时报错
<action android:name="zmj.componentssummary.05"/>
//这是我们自己定义的category标签
<category android:name="zmj.componentssummary.category05"/>
//DEFAULT这个category标签不能去掉,否则会报错
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
在我们的ActivitySummary6中启动我们的ActivitySummary5,代码如下
Intent intent = new Intent();
intent.setAction("zmj.componentssummary.05");
intent.addCategory("zmj.componentssummary.category05");
startActivity(intent);
当然我们也可以在清单中添加多个自定义category,匹配规则即如果我们的Intent含有category,那么我们所有的category都必须和过滤规则中的其中一个category相同。
不同于action和category的字符串格式,data的数据格式稍微有点复杂
<data
android:mimeType="媒体类型,例如image/*、image/jpeg、audio/mpeg4-generis、video/*"
android:scheme="URI的模式,例如http、file、content"
android:host="主机名,例如www.baidu.com"
android:port="端口号,例如8080"
android:path="/完整的路径信息"
android:pathPattern="完整的路径信息,可以包含通配符"
android:pathPrefix="/路径的前缀信息"/>
不过其实常用的也很简单,例如这是咱们的ActivitySummary7,需要注意的是,< data >必须和< action >和< category >共同作用,自己不能单独使用,否则会报错
<activity android:name=".ActivitySummary7">
<intent-filter>
<action android:name="zmj.componentssummary.07"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:mimeType="image/png"
/>
</intent-filter>
</activity>
这是我们启动ActivitySummary7的代码
Intent intent = new Intent();
intent.setAction("zmj.componentssummary.07");
// 未指定过滤规则中的URI模式,即scheme的值,URI就会取默认值content和file
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
startActivity(intent);
还有一点需要注意的是,当我们没有指定过滤规则中的URI模式,即scheme的值时,URI就会取默认值content和file,所以我们启动的时候Intent必须携带URI模式类型才能与之匹配,同时,< data >也支持添加多个,匹配规则即Intent所携带的data必须能够和过滤规则中的其中一个完全匹配。
横竖屏切换后,界面效果错乱的问题,android已经为我们提供了较完美的解决方案,那就是新建备选资源,让横竖屏切换后的activity分别对应不同的xml文件(但资源id相同)。
如果对于横屏屏有着不同的布局要求,那么就需要新建一个备选资源,首先在res目录下新建一个文件夹并命名为layout-land,随后在layout-land目录下新建一个相同名字的xml布局,一个技巧就是可以直接将activity_summary.xml布局从layout目录下复制到layout-land目录之下,随后再根据需求更改xml布局,本例中src目录结构如下:
这是layout目录下activity_summary.xml的普通布局。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_hw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我在中间位置"
android:textSize="18sp"
android:textColor="#787878"
/>
</RelativeLayout>
而这是layout-land目录下的activity_summary.xml的布局,注意横屏和竖屏的两个布局文件名字必须一致,这样才能保证它们能以同一个资源ID被引用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_hw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="我来顶部哈"
android:textSize="18sp"
android:textColor="#787878"
/>
</RelativeLayout>
运行看下效果,效果图1是正常居中布局,而图2是我们适配的横向居中效果。
第一种方法就是在清单中在相应的activity中指定特定方向,其中portrait为竖向,landscape为横向
<activity android:name=".ActivitySummary2"
android:screenOrientation="portrait"
/>
第二种方法就是可以在代码中动态添加
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
//竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
//横屏
setContentView(R.layout.activity_summary);
}
如果你不想关闭横竖屏切换功能,但又不想Activity在屏幕旋转的时候重新创建,那么我们可以给Activity指定configChanges属性
android:configChanges="orientation|keyboardHidden|screenSize"
其中orientation表示屏幕方向发生了改变;keyboardHidden表示键盘的可访问性发生了改变,比如用户调出了键盘;screenSize表示当编译时minSdkVersion和targetSdkVersion不低于或不全低于13时,当屏幕的尺寸信息发生了改变时,会导致Activity重建。