1.活动(Activity)
Activity
是一种可以包含用户界面的组件,主要用于和用户进行交互。
手动创建活动,我们在创建的时候,选择Add No Activity
,这个时候app/src/main/java/com.example.activitytest
目录是空的。我们右键新建一个活动,但是不要勾选Generate Layout File和Launcher Activity.
Generate Layout File:
表示会自动为FirstActivity
创建一个对应的布局文件
Launcher Activity:
表示会自动将FirstActivity
设置为当前项目的主活动。
项目中的任何活动都应该重写Activity
的onCreate()
方法。
Android Studio
为我们提供了可视化布局编辑器,在窗口的最下方有两个切换卡,左边是Design
,是当前的可视化布局编辑器,右边是Text
,通过XML
文件的方式来编辑布局的。
setContentView()
方法给当前的活动加载一个布局,在方法中我们一般会传入一个布局文件的id
.项目中添加的任何资源都会在R
文件中生成一个相应的资源id
。
上图是手动配置的主活动,其中
android:label
指定活动中标题栏的内容,标题栏是显示在活动最顶部的,需要注意的是,给主活动指定的label
不仅会成为标题栏中的内容,还会成为启动器(Launcher)
中应用程序显示的名称。如果你的应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序,这种程序一般都是作为第三方服务供其它应用在内部进行调用的,如支付宝快捷支付服务等。
2. Toast
在活动中,可以通过
findViewById()
方法获取到在布局文件中定义的元素,findViewById()
方法,返回的是一个View
对象,我们需要向下转型将他转成Button
对象,调用setOnClickListener()
方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()
方法。通过静态方法
makeText()
创建出一个Toast
对象,然后调用show()
方法将Toast
显示出来就可以了。makeText()
方法需要传入三个参数,第一个参数是Context
对象,也就是Toast
要求的上下文,第二个参数是Toast
显示的文本内容,第三个参数是Toast
显示的时长。有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG
3.在活动中使用Menu
其中<item>
标签就是用来创建具体的某一个菜单项,然后通过android:id
给这个菜单项指定一个唯一的标识符,通过android:title
给这个菜单项指定一个名称。
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
getMenuInflater().inflate(R.menu.main,menu);
return true;
}
通过getMenuInflater()
方法能够得到MenuInflater
对象,在调用它的inflater()
方法,就可以给当前的活动创建菜单了,inflater()
方法接受两个参数,第一个参数用于指定我们通过哪一个资源文件来创建菜单,第二个参数用于指定我们的菜单项将添加到哪一个Menu
对象中去。这里直接使用onCreateOptionMenu()
方法中传入的Menu
参数,然后给这个方法换回true
,表示允许创建的菜单显示出来。如果返回false,创建的菜单将无法显示。
菜单响应事件
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.add_item:
Toast toast1 = Toast.makeText(FirstActivity.this,"You clicked Add",
Toast.LENGTH_LONG);
toast1.show();
break;
case R.id.remove_item:
Toast toast2 = Toast.makeText(FirstActivity.this,"You clicked Remove",
Toast.LENGTH_LONG);
toast2.show();
break;
default:
}
return true;
}
调用item.getItemId()
来判断我们点击的是哪一个菜单项。
4.Intent的使用
Intent
是Android
程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent
一般可被用于启动活动,启动服务以及发送广播等场景。
Intent
大致可以分为两种:显式Intent
和隐式Intent
.
显式Intent
Intent intent = new Intent(FirstActivity.this;SecondActivity.class);
startActicity(intent);
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext,Class<?> cls).这个构造函数接受两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动,通过这个构造函数就可以构造出Intent的意图.Activity类中提供了一个startActivity()方法,这个方法是专门用于启动活动的,他接受一个Intent参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标活动了。
使用这种方式来启动活动,Intent的意图非常明显,因此我们称之为显式Intent
隐式Intent
相比于显式Intent,隐式Intent则含蓄了很多,他并不明确指出我们想要启动哪一个活动,而是制定了一系列更为抽象的action和category等信息,然后交于系统去分析这个Intent,并帮我们找出合适的活动去启动。
<activity
android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
在<action>标签中我们声明了当前活动可以相应com.example.activitytest.ACTION_START这个action,而<category>标签则包含了一些附加信息,更精确的指明了当前活动能够响应的Intent中还可能带有category.只有<action>和<category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能够响应该Intent.
Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
我们使用了Intent的另一个构造函数,直接将action的字符串传了进去,表明我们想要启动能够响应com.example.activitytest_ACTION_START这个action的活动,android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候,会自动将这个category添加到Intent中。
每个Intent中只能指定一个action,但却能指定多个category.
Intent intent = new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
我们调用Intent中的addCategory()方法来添加一个category,我们指定了一个自定义的category,值为:com.example.activitytest.MY_CATEGORY.如果我们在运行的话会出现这样的错误:
Process: com.example.activitytest, PID: 12083
android.content.ActivityNotFoundException: No Activity found to handle
Intent { act=com.example.activitytest.ACTION_START cat=[com.example.activitytest.MY_CATEGORY] }
因为并没有活动去响应我们新增加的category.
<activity
android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.example.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>
我们增加一个category就可以了。
更多隐式Intent的用法
使用隐式Intent,我们不仅可以启动我们自己程序向内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
我们指定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置的活动,其常量值为android.intent.action.View.然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,在调用Intent的setData()方法将这个Uri对象传递进去。setData()方法接受一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常都是以字符串的形式传入到Uri,parse()方法中解析产生的。
我们还可以在<intent-filter>标签中再配置一个<data>标签,用于更精确的指定当前活动能够响应什么类型的数据。<data>标签中主要可以配置一下内容。
- android:scheme :用于指定数据的协议部分,如上面的http部分。
- android:host :用于指定数据的主机名部分,如上面的www.baidu.com
- android:port :用于指定数据的端口部分,一般紧随在主机名之后
- android:mineType:用于指定可以处理的数据类型,允许使用通配符的方式指定。
只有<data>标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent.不过一般在<data>中都不会指定过多的内容。如上面的浏览器例子中,只需要指定android:scheme为http,就可以响应所有的http协议的Intent了。
<activity
android:name=".ThirdActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
</intent-filter>
</activity>
我们添加一个活动,修改<intent-filter>的值,让他也可以响应浏览器的Intent.
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置,tel表示拨打电话。
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
这是调用系统拨号界面
指定了Intent的action是Intent.ACTION_DIAL,这又是系统的内置动作。然后data部分指定了协议是tel,号码是10086.
向下一个活动传递数据
Intent
中提供了一系列putExtra()
方法的重载。
String data = "hello SecondActivity";
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("extra_data",data);
startActivity(intent);
注意这里putExtra()
方法接收两个参数,第一个参数是键,用于后面从Intent
中取值,第二个参数才是真正要传递的数据。
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
Log.d(TAG, data);
通过getIntent()
方法获取到用于启动SecondActivity
的Intent
,然后调用getStringExtra()
方法,传入相应的键值,就可以得到传递的数据了。如果传递的是整形数据使用getIntExtra()
方法,如果传递的是布尔型数据,则使用getBooleanExtra()
方法,以此类推。
返回数据给上一个活动
Activity
中还有一个startActivityForResult()
方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。
startActivityForResult()
方法接收两个参数,第一个参数还是Intent
,第二个参数是请求码
,用于在之后的回调中判断数据的来源。
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivityForResult(intent,1);
使用startActivityForResult()
来启动活动。请求码只要是一个唯一值就可以了。 ```
Intent intent = new Intent();
intent.putExtra("data_return","hello FirstActivity");
setResult(RESULT_OK,intent);
finish();
我们构建了一个Intent
,只不过这个Intent
仅仅是用于传递数据而已,他没有指定任何的意图,接着我们把要传递的数据放入Intent
中,然后调用setResult()
方法,这个方法非常重要,是专门用于向上一个活动返回数据的。setResult()
方法接收两个参数,第一个参数用于向上一个活动返回处理的结果,一般只使用RESULT_OK
或RESULT_CANCELED
两个值,第二个参数则把带有数据的Intent
传递回去。
在SecondActivity
销毁后会回调上一个活动的onActivityResult()
方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode){
case 1:
if (resultCode == RESULT_OK) {
String returnedData = data.getStringExtra("data_return");
Log.d(TAG, "onActivityResult: "+returnedData);
}
break;
default:
}
}
onActivityResult()
方法带有三个参数,第一个参数requestCode
,即我们启动活动时的请求码,第二个参数resultCode
,即我们在返回数据时传入的处理结果,第三个参数data
,即携带者返回数据的Intent
,由于在一个活动中有可能调用startActivityForResult()
方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()
这个方法,因此我们首先要做的就是通过检查requestCode
的值来判断数据来源,然后再通过resultCode
的值来判断处理结果是否成功.
如果用户在SecondActivity
中并不是通过点击按钮,而是通过按下Back
键回到FirstActivity
,我们可以通过在SecondActivity
中重写onBackPressed()
方法。
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("data_return","hello FirstActivity2");
setResult(RESULT_OK,intent);
finish();
}
5.活动的生命周期
Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack).栈是一种后进先出的数据结构,在默认情况下,每当我我们启动了一个新的活动,他会在返回栈中入栈,并处于栈顶的位置,而每当我们按下Back键或调用finish()方法去销毁一个活动的时候,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。
返回栈的工作示意图:
5.1活动的状态
每个活动在其生命周期中最多可能会有4种状态。
1.运行状态
当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。
2.暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,这是活动就进入暂停状态。
3.停止状态
当一个活动不在处于栈顶位置,并且完全不可见的时候,就进入停止状态。
4.销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。
5.2活动的生存期
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节。
onCreate(): 在活动第一次被创建的时候调用,你应该在这个方法中完成活动的初始化操作,比如说加载布局,绑定事件等。
onStart(): 在活动由不可见变为可见的时候调用。
onResume(): 在活动准备好和用户进行交互的时候调用,此时的活动一定位于返回栈的栈顶,并且处于运行状态。
onPause(): 在系统准备去启动或恢复另一个活动的时候调用,我们通常会在这个方法中将一些小号CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
onStop(): 在活动完全不可见的时候调用,他和onPause()方法的主要区别是如果启动的新活动是一个对话框的活动,那么onPause()方法会得到执行,而onStop()方法并不会得到执行。
onDestory(): 在活动贝销毁之前调用,之后活动的状态将变为销毁状态。
onRestart(): 在活动有停止状态变为运行状态之前调用,也就是活动被重新启动了。
完整生存期
活动在onCreate()方法和onDestory()方法之间所经历的,就是完整生存期,一般情况下,一个活动会在onCreate()方法中完成各种初始化操作,而在onDestory()方法中完成释放内存的操作。
可见生存期
活动在onStart()方法和onStop()方法之间所经历的就是可见生存期。在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的活动不会占用过多的内存。
前台生存期
活动在onResume()方法和onPause()方法之间所经历的就是前台生存期。
活动的生命周期图
<activity
android:name=".DialogActivity"
android:theme="@android:style/Theme.Dialog">
</activity>
我们给他使用了一个android:theme属性,这是用于给当前活动指定主题的,Android系统内置有很多的主题可以选择,当然我们也可以定制自己的主题。而这里@android:style/Theme.Dialog,则毫无疑问是让DialogActivity使用对话框式的主题。
当MainActivity第一次被创建时会依次执行onCreate(),onStart()和onResume()方法。
NormalActivity把MainActivity完全遮挡住,onPause()和onStop()方法会得到执行。
按下Back键,之前的MainActivity已经进入停止状态,所以onRestart()方法会得到执行之后又会依次执行onStart()和onResume()方法。
点击进入DialogActivity,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。相应的,按下Back键返回MainActivity也应该只有onResume()方法会得到执行。
活动被回收了怎么办?
Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在活动被回收之前一定会被调用。因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。
onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整形数据。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。
@Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key",tempData);
}
onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据。
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState != null)
{
String tempData = savedInstanceState.getString("data_key");
}
}
取出值后再做相应的操作就可以了。
Intent还可以结合Bundle一起用于传递数据,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里,到了目标活动之后先从Intent中取出Bundle,在从Bundle中一一取出。
6.活动的启动模式
在实际项目中我们应该根据特定的需求为每个活动指定恰当的启动模式。启动模式一共有4种,分别是standard,singleTop,singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。
6.1 standard
standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。Android是使用返回栈来管理活动的在standard(即默认情况下),每当启动一个新的活动,他就会在返回栈中入栈,并处于栈顶的位置,对于使用standard面膜是的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
在代码中实践:
Log.d(TAG, this.toString());
Intent intent = new Intent(FirstActivity.this,FirstActivity.class);
startActivity(intent);
连续点击两次,可以看到打印出当前活动的实例。
01-06 21:47:26.409 18937-18937/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@f7397be
01-06 21:47:32.614 18937-18937/com.example.activitytest D/FirstActivity: com.example.activitytest.FirstActivity@6b88a94
6.2 singleTop
使用singleTop模式,当活动的启动模式指定为singleTop,在启动活动时,如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。
android:name=".FirstActivity"
android:launchMode="singleTop"
android:label="This is FirstActivity">
01-06 22:12:07.813 15130-15130/? D/FirstActivity: com.example.activitytest.FirstActivity@f7c25bd
无论点击多少次,都只会创建一次实例。
6.3 singleTask
使用singleTop模式可以很好地解决重复创建栈顶活动的问题,当活动的启动模式指定为singleTask模式,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
6.4 singleInstance
指定为singleInstance模式的活动会启用一个新的返回栈来管理这个活动(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例,在singleInstance模式下,会有一个单独的返回栈来管理这个活动不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
7.活动的最佳实践
7.1 知晓当前是在哪一个活动?
新建一个类:BaseActivity
public class BaseActivity extends AppCompatActivity
{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());
}
}
让所有的Activity不在继承AppCompatActivity,而是继承BaseActivity,getClass().getSimpleName()可以得到当前实例的类名。
7.2随时随地退出程序
我们需要用一个专门的集合类对所有的活动进行管理就可以了。
public class ActivityCollector
{
//添加活动
public static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity)
{
activities.add(activity);
}
//移除活动
public static void removeActivity(Activity activity)
{
activities.remove(activity);
}
//全部销毁活动
public static void finishAll()
{
for (Activity activity : activities)
{
if (!activity.isFinishing())
{
activity.finish();
}
}
}
}
通过一个List来暂存活动,然后提供了一个addActivity()方法用于向List中添加一个活动,提供了一个removeActivity()方法用于从List中移除活动,最后提供了一个finishAll()方法用于将List中存储的活动全部销毁掉.
public class BaseActivity extends AppCompatActivity
{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy()
{
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的活动添加到活动管理器里,然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明将一个马上要销毁的活动从活动管理器里移除。
从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了。
public class ThirdActivity extends BaseActivity
{
Button button;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
button = (Button) findViewById(R.id.button_3);
initEvent();
}
public void initEvent()
{
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
ActivityCollector.finishAll();
android.os.Process.killProcess(android.os.Process.myPid());
}
});
}
}
你还可以在销毁所有活动的代码后面再加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码:
android.os.Process.killProcess(android.os.Process.myPid());
killProcess()方法用于杀掉一个进程,它接受一个进程ID的参数,我们可以通过myPid()方法来获得当前程序的进程ID,需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,,我们不能使用这个方法去杀掉其他程序。
7.3 启动活动的最佳写法
在SecondActivity中写入下面的方法:
public static void actionStart(Context context,String data1,String data2)
{
Intent intent = new Intent(context,SecondActivity.class);
intent.putExtra("param1",data1);
intent.putExtra("param2",data2);
context.startActivity(intent);
}
在FirstActivity里面调用这个方法:
SecondActivity.actionStart(FirstActivity.this,"data1","data2");
这样写的好处是一目了然:SecondActivity所需要的数据在方法参数中全部体现出来了,这样即使不用阅读SecondActivity中的代码,不去询问负责编写SecondActivity的同事,你也可以非常清晰地知道启动SecodeActivity需要传递哪些数据,另外这样写还简化了启动活动的代码。0