Activity是Adnroid中唯一可视化的应用程序组件。
代码托管 Github
Activity的使用方法
Activity是Android中最核心的应用程序组件,也是大多数程序必须使用的用于显示界面的组件。
创建Activity
- 建立一个普通的Java类,该类必须从Activity类或者其子类中继承。
- 重写Activity类中的onCreate方法。
- 在onCreate方法中使用setContentView装载View。
实际上一个类只要继承Activity类就可以当成一个Activity来使用了,只是没有任何控件,只有屏幕顶端默认的标题栏。
想要在Activity中添加控件,最直接的方法就是在onCreate中装载xml布局文件或者使用Java代码添加控件。
如果要重写onCreate方法,必须要调用Activity类的onCreate()方法,也就是super.onCreate(savedInstanceState)
,否则显示Activity时会抛出异常。
这是因为Activity类中没有不带参数的onCreate().如果不显示的调用super.onCreate(savedInstanceState),系统会试图调用super.onCreate()方法,然而在Activity类中并没有此方法。
配置Activity
在AndroidManifest.xml中配置Activity。
<activity
android:name=".MainActivity_"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
每一个Activity都会对应AndroidManifest.xml问价中的一个<activity>
标签。
必选属性:android:name ,用于指定Activity类名。
指定android:name属性值有三种方式:
- 指定完整的类名(packagename+classname)
- 仅指定类名,例如 .Main , 其中前面的 . 是可选的。但是该类的包名需要在mainifest标签的package属性中指定。 例如 manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.turing.base” > - 指定相对类名,这种方式类似第二种,只是在activity标签的android:name属性中不仅指定列名,还包括部分包名。
其他一些常用的属性
android:label
AndroidMainfest.xml中在application和activity中都可以设置android:lable。
android:label用来设置应用名和标题名。
应用名:
当主activity和application中都设置此值后,应用名会优先使用主activity中的值。
标题名:
当application和activity中都设置android:label时,标题名会优先使用各个activity中的值。当存在activity中没有设置值时,会使用application中的值。
android:icon
android:icon 必须指定一个图像资源ID,用来作为应用程序列表中的程序图标。 如果没有在activity标签中指定,系统这会使用application标签中的android:icon属性值来代替。
intent-filter
intent-filter标签的作用就是对Activity进行分类,
intent-filter标签内部的
action标签表示Activity可以接收的动作
category表示Activity所属的种类
实际上,action和category标签中的android:name属性值只是一个普通的字符串。 在Android系统中预定了一些表中的动作和种类。
例如:
android.intent.action.Main 和android.intent.action.category.LAUNCHER
其中android.intent.action.Main需要定义在Main Activity类的activity标签中。
当Android系统运行时,会首先启动包含android.intent.action.Main的Activity。 作为MainActivity必须使用android.intent.category.LAUNCHER 作为其类别,表示该Activity显示在最顶层。
显示其他的Activity(Intent与Activity)
想要创建和显示Activity,必须使用android.content.Intent作为中间的代理,并使用startActivity或startActivityForResult方法创建并显示Activity。
显示调用Intent
Intent intent = new Intent(this,MyActivity.class);
startActivity(intent);
第一个参数指定了Context类型的对象,第二个参数指定需要显示的Activity类的class对象。
如果不想再Intent类的构造方法中指定这两个参数,也可以通过Intent类的setClass方法来指定,代码如下:
Intent intent = new Intent();
intent.setClass(this,MyActivity.class);
startActivity(intent);
隐式调用Intent
隐式调用仍然需要使用Intent,但是并不需要指定要调用的Activity,而只是要指定一个Action和相应的Category即可。 action和category这两个标签,不光是提供Android系统使用,我们也可以将他们应用到自定义的Activity中。
如果是自定义的种类(category),category标签的属性值至少要有一个android.intent.category.DEFAULT.
action标签的android:name属性,可以是任意字符串,但建议使用有意义的字符串,并要在程序中通过常量来引用。
一个intent-filter标签可以包含多个action和category标签。
一个Activity中可以包含多个intent-filter标签。
当intent-filter标签中,只有一个值为android.intent.action.category.DEFAULT的category时,并不需要在Intent对象中指定这个category。如果包含了其他的category,必须要使用intent.addCategory方法添加相应的category.
实例如下:
<!-- 该Activity未设置任何intent-filter,用显式的方式调用这个Activity -->
<activity
android:name=".activity.intentAct.XianSiDiaoyongAct"
android:label="XianSiDiaoyongAct" />
<!-- 隐式调用Activity -->
<activity
android:name=".activity.intentAct.YinSiDiaoyngAct"
android:icon="@drawable/flag_mark_red"
android:label="YinSiDiaoyngAct">
<intent-filter>
<action android:name="myAction1" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!--这个intent-filter和YinSiSelectAct的intent的相同-->
<intent-filter>
<action android:name="myAction2" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="mycategory"/>
</intent-filter>
</activity>
<activity
android:name=".activity.intentAct.YinSiSelectAct"
android:icon="@drawable/flag_mark_yellow"
android:label="YinSiSelectAct">
<!--这个intent-filter和YinSiDiaoyngAct的第二个intent-filter相同,使用这个intent-filter,屏幕会弹出一个列表,供用户选择-->
<intent-filter>
<action android:name="myAction2" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="mycategory"/>
</intent-filter>
</activity>
switch (position){
case 0: // 显示调用Activity
Toast.makeText(UI_Base.this,String.valueOf(position),Toast.LENGTH_SHORT).show();
// 第一种方式
Intent intent = new Intent(UI_Base.this, XianSiDiaoyongAct.class);
startActivity(intent);
// 第二种方式
// Intent intent1 = new Intent();
// intent1.setClass(UI_Base.this,XianSiDiaoyongAct.class);
// startActivity(intent1);
break;
case 1:// 隐式调用Activity
Intent intent2 = new Intent("myAction1");
startActivity(intent2);
break;
case 2: // 隐式调用两个符合过滤条件的Activity
Intent intent3 = new Intent("myAction2");
intent3.addCategory("mycategory");
startActivity(intent3);
break;
如果在intent-filter标签中使用了默认的category(android.intent.category.DEFAULT),在隐式调用中并不需要在Intent对象中使用addCategory方法指定。 如果非要指定,使用定义在Intent类中的常量 Intent.CATEGORY_DEFAULT.
intent.addCategory(Intent.CATEGORY_DEFAULT);
代码说明:
第一个显示调用,会根据指定的class来动态创建Activity对象实例。
第二个隐式调用,系统会查找包含myaction1的Activity,如果找到,显示。否则抛出异常。
第三个隐式调用符合过滤条件的Activity,由于有两个Activity都包含了名为myaction2的动作,并且都属于名为mycategory的种类,系统会弹出选择界面,用户可以选择其中一个运行,如果勾选了”Use by default for this action”复选框,下次运行会直接运行上次选择的Activity。
Activity的生命周期
整体描述
从Activity创建到销毁的过程中需要在不同的阶段调用7个生命周期方法。
procted void onCreate(Bundle savedInstanceState)
procted void onStart()
procted void onResume()
procted void onPause()
procted void onStop()
procted void onRestart()
procted void onDestory()
上述7个生命周期方法分别在4个阶段按照一定的顺序进行调用,4个阶段分别如下
- 开始Activity
onCreate—onStart—onResume - Activity失去焦点
onPause—onStop - Activity重新获得焦点
onRestart—onStart—onResume - 关闭Activity
onStop—onDestory
如果在这4个阶段执行生命周期方法的过程中不发生状态的改变,系统会按照上面的藐视依次执行这4个阶段的生命周期方法,但是如果执行过程中改变了状态,系统会按更加复杂的方法调用生命周期方法。
从上图中我们可以看出,
在执行的过程中可以改变系统的执行轨迹的生命周期方法是onPause和onStop。
如果在onPause的过程中Activity重新获得了焦点,然后又失去了焦点,系统将不会执行onStop方法,而是按照如下顺序执行相应的生命周期方法:
onPause—onResume—onPause
如果在执行onStop方法的过程中Activity重新获得了焦点,然后又失去了焦点,系统将不会执行onDestory方法,而是按照如下的顺序执行相应的生命周期方法:
onStop—onRestart—onStart—onResume—onPause—onStop
从官方给出的Activity生命周期图中不难看出,这个图中包含两层循环,第一层是:onPause—onResume—onPause。
第二层是onStop—onRestart—onStart—onResume—onPause—onStop.
第一层称为焦点生命周期
第二层称为可视生命周期
也就是说第一层循环在Activity焦点的获得与失去中循环,这一过程中Activity始终可见。
第二层循环在Activity可见与不可见的过程中循环,在这个过程中伴随着Activity焦点的获得与失去。也就是说Activity首先被显示,然后会获得焦点,接着失去焦点,最后由于弹出其他的Activity,使当前的Activity变得不可见。
因此Activity有如下三种生命周期:
- 整体生命周期:onCreate……..onDestory
- 可视生命周期:onStart…….onStop
- 焦点生命周期:onResume—onPause
演示
public class LifeCircleActivity extends Activity {
private static final String TAG = LifeCircleActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_life_circle);
Log.e(TAG, "onCreate");
}
@Override
protected void onStart() {
super.onStart();
Log.e(TAG, "onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.e(TAG, "onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.e(TAG, "onStop");
}
@Override
protected void onRestart() {
super.onRestart();
Log.e(TAG, "onRestart");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy");
}
}
启动应用程序: onCreate—onStart—onResume
按home键:(失去焦点) onPause—onStop
按home键后重新进入:(重新获得焦点)onRestart—onStart—onResume
按返回键:(退出) onPause–onStop—onDestory
在不同Activity之间传递数据
Activity之间切换时,不可避免的要进行数据传递,例如在单击列表中的某个列表项时,小需要编辑与这个列表项相关的数据,这个时候就需要在显示一个Activity,然后将原始数据传递个这个Activity,这就是一个典型的数据传递的过程。
在Android中传递数据的方法很多,介绍4中比较常用的数据传递方法
- 通过Intent传递数据
- 通过静态(static)变量传递数据
- 通过剪切板(Clipboard)传递数据
- 通过全局变量传递数据
使用Intent传递数据
这是最常用的一种数据传递方法。
通过Intent类的putExtra方法可以将简单类型的数据或者可序列化的对象保存在Intent对象中,然后在目标Activity中使用getXXX(getInt,getString。。。。)方法获得这些数据。
关键代码如下:
Intent intent5 = new Intent(UI_Base.this,GetIntentActivity.class);
//简单类型
intent5.putExtra("intent_string","通过Intent传递的字符串");
intent5.putExtra("intent_int", 20);
// 可序列化的对象
Data data = new Data();
data.setId(99);
data.setName("ZTE");
intent5.putExtra("intent_object", data);
startActivity(intent5);
// 获取String
String msg = getIntent().getStringExtra("intent_string");
LogUtils.d("String:" + msg);
// 获取Int
int value = getIntent().getExtras().getInt("intent_int");
LogUtils.d("int:" + value);
int vaule2 = getIntent().getIntExtra("intent_int" , 0);
LogUtils.d("第二种获取方式:" + vaule2 );
// 获取可序列化对象
Data data = (Data) getIntent().getSerializableExtra("intent_object");
LogUtils.d("Data: name" + data.getName() + ",id:" + data.getId());
StringBuffer sb = new StringBuffer();
sb.append("intent_string:" + msg);
sb.append("\n");
sb.append("intent_int:" + value);
sb.append("\n");
sb.append("intent_object: name-"+data.getName());
sb.append("\n");
sb.append("intent_object: id-"+data.getId());
tv_getIntent.setText(sb.toString());
Data implements Serializable
public class Data implements Serializable {
public int id;
public String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Data类是可序列化的,也就是实现了java.io.Serializable接口。
使用静态变量传递数据
虽然intent可以很方便的在Activity中间传递数据,这也是官方推荐的数据传递方式。
但是Intent也有其局限性,Intent无法传递不能序列化的对象,也即是没有实现java.io.Serializable接口的类的对象。
比如Canvas对象就无法通过Intent对象传递,如果传递自定义类的对象,也必须实现java.io.Serializable接口才可以。 如果没有源代码,而且还没有实现Serializable接口,使用Intent对象就无能为力了。
步骤:
1.startActivity之前,为静态变量赋值
2.目标类定义静态变量接收(也可以在其他类中定义)
Intent intent6 = new Intent(UI_Base.this,StaticTransmitActivity.class);
// 赋值
StaticTransmitActivity.msg="通过static变量来的";
StaticTransmitActivity.age = 88 ;
StaticTransmitActivity.data = new Data();
StaticTransmitActivity.data.setName("Jack");
StaticTransmitActivity.data.setId(77);
startActivity(intent6);
public class StaticTransmitActivity extends AppCompatActivity {
public static String msg ;
public static int age ;
public static Data data;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static_transmit);
StringBuffer sb = new StringBuffer();
sb.append("msg:" + msg);
sb.append("\n");
sb.append("age:"+age);
sb.append("\n");
sb.append("data name:"+data.getName());
sb.append("\n");
sb.append("data id:"+data.getId());
TextView textView = (TextView)findViewById(R.id.id_tv_static);
textView.setText(sb.toString());
}
}
使用剪切板传递变量(String类型和复杂对象)
在Activity之间传递对象还可以利用一些技巧。无论是windows 还是Linux,都会支持一种叫做剪切板的技术。
String类型
Intent intent7 = new Intent(UI_Base.this, ClipBoardTransActivity.class);
ClipboardManager clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
// api 11的方法 @TargetApi(Build.VERSION_CODES.HONEYCOMB)
clipboardManager.setText("通过Clipboard传递数据");
startActivity(intent7);
public class ClipBoardTransActivity extends AppCompatActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clip_board_trans);
ClipboardManager clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
String msg = clipboardManager.getText().toString();
TextView textView = (TextView) findViewById(R.id.id_tv_clipboard);
textView.setText(msg);
}
}
在上述的代码中使用了getSystemService方法获得了一个系统服务对象,也就是ClipboardManager对象,该对象用于管理系统剪切板,并使用ClipboardManager.setText方法向剪切板中保存了一个字符串,通过getText可以获取。
但是ClipboardManager对象只支持向剪切板读写字符串,并不支持其他的类型,更别提复杂的对象了。
当然了,如果是其他类型的数据,比如int ,可以将起转换成字符串。
复杂对象
如果是对象类型呢,比如之前的Data对象能否通过剪切板传递呢?答案是肯定的,只是需要通过Base64进行编码解码转换。
由于Data是可序列化的对象,因此完全可以将Data抓换成byte[]类型的数据,然后将byte[]类型的数据再进行Base4编码(通过Email发送附件就是将附件转换成为Base64格式的字符串发送的)转换成字符串。
代码演示,通过剪切板传递Data对象。
Intent intent8 = new Intent(UI_Base.this, ClipboardTransObjectDataAct.class);
ClipboardManager cbm = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
// 通过Clipboard传递复杂对象
Data data3 = new Data();
data3.setId(55);
data3.setName("Clipboard传递复杂对象");
// 将data2对象转换成Base64格式的字符串
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String base64Str = "";
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(data3);
// 使用Base64.encodeToString方法将byte[]数据转换为Base64字符串
base64Str = Base64.encodeToString(baos.toByteArray(),Base64.DEFAULT);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
// 向剪切板写入Base64格式的字符串
cbm.setText(base64Str);
startActivity(intent8);
解码
public class ClipboardTransObjectDataAct extends AppCompatActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clipboard_trans_object_data);
ClipboardManager cbm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// 从剪切板中获取Base64编码格式的字符串
String base64Str = cbm.getText().toString();
// 将Base64格式的字符串还原为byte[]格式的数据
byte[] buffer = Base64.decode(base64Str,Base64.DEFAULT);
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
try {
ObjectInputStream ois = new ObjectInputStream(bais);
// 将byte[]数据还原为Data对象
Data data = (Data)ois.readObject();
// 输出
TextView tv = (TextView)findViewById(R.id.id_tv_clipboard_trans_object);
tv.setText(base64Str + "\n\n data.id:" + data.getId() + "\ndata.name:"+data.getName());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
说明:
Base64类是从Android2.2开始支持的,2.1及其以下版本无法通过Android SDK API 进行Base64编码和解码,因此需要借助第三方的类库如common httpclient才可以。
使用全局对象传递变量
虽然使用静态变量可以传递任意类型的数据,但是官方并不建议这样做。如果在类中大量使用静态变量(尤其是很占用资源的变量,如Bitmap对象)可能会造成内存溢出异常,而且可能因为静态变量在很多类中出现而造成代码难以维护和混乱。
我们可以使用一种更优雅的数据传递方式–全局对象。这种方式可以完全取代静态变量。
步骤:
- 全局类 必须继承android.app.Application
- AndroidManifest.xml中的Application标签的android:name属性指定这个类
public class AppContext extends Application {
/**
* 演示使用全局变量传递数据 appName data
*/
public String appName;
public Data data = new Data();
@Override
public void onCreate() {
super.onCreate();
/**
* 支持直接打印数据集合,如List、Set、Map、数组等
全局配置log输出
不需要设置tag
准确显示调用方法、行,快速定位所在文件位置.
*/
// 配置日志是否输出(默认true)
LogUtils.configAllowLog = true;
// 配置日志前缀
LogUtils.configTagPrefix = "MrYang-";
}
}
Manifest.xml指定全局类
<application
android:name=".AppContext"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
AppContext context = (AppContext)getApplication();
context.appName = "ANDROID BASE";
context.data.setId(0000);
context.data.setName("通过全局变量来传递数据");
Intent intent9 = new Intent(UI_Base.this, ApplicationTransActivity.class);
startActivity(intent9);
public class ApplicationTransActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_application_trans);
// 获取全局变量
AppContext context = (AppContext)getApplication();
String name = context.appName;
Data data = context.data;
StringBuffer sb = new StringBuffer();
sb.append("AppContext appname:" + name );
sb.append("\n");
sb.append("AppContext data.id:" + data.getId());
sb.append("\n");
sb.append("AppContext data.name:" + data.getName());
TextView textView = (TextView) findViewById(R.id.id_tv_app_trans);
textView.setText(sb.toString());
}
}
全局对象所对应的类必须是android.app.Application的子类。
全局类中不需要定义静态变量,只需要定义成员变量即可,
而且全局类中必须要有一个无参的构造方法,或者不编写任何代码的构造方法(系统会自动的建立一个无参数的构造方法)。
这个类和Activity一样,由系统自动创建,因此,必须要有一个无参的构造方法。
只编写一个全局类是不会自动创建全局对象的,因为Android系统并不知道哪个是全局类,因此需要在AndroidManifest.xml中的application标签的android:name属性来执行这个类。
指定全局类后,在程序运行后,全局对象会被自动创建,而且会一直在内存中驻留,直到应用程序彻底退出内存。
四种方式比较
虽然上述始终方法在某些情况下可以相互取代,但是根据具体情况使用不同的数据传递方法会使程序更加便于维护。
对于向其他Activity中传递简单类型(int 、String、short、bool等)或者可序列化的对象时,建议使用Intent。
如果传递不可序列化的对象,可以采用静态变量或者全局对象的方式,不过按照官方的建议,最好是采用全局对象的方式。
另外如果想要使某些数据长时间驻留内存,以便程序随时的取用,最好采用全局对象的方式。当然如果数据不复杂,也可以采用静态变量的方式
至于剪切板,如果不是特殊情况,并不建议使用,因为这可能会影响到其他的程序(其他程序也可能使用剪切板)
返回数据到前一个Activity
在应用程序中,不仅要向Activity传递数据,同时也要从Activity中返回数据,一般建议采用Intent这种方式来返回数据,需要使用startActivityForResult方法来显示Activity。
代码如下
Intent intent = new Intent(this,XX.class);
startActivityForResult(intent,1);
其中startActivityForResult方法有2个参数,第二个参数是一个int类型的请求码,可以是任意的整数,只是为了区分请求的来源,以便处理返回结果。
大致步骤如下:
启动一个ForResult的意图:
Intent intent = new Intent(MainAcitvity.this,RequestActivity.class);
//发送意图标示为REQUSET=1
startActivityForResult(intent, REQUSET);
B Activity处理数据:
Intent intent=new Intent();
intent.putExtra(KEY_USER_ID, et01.getText().toString());
setResult(RESULT_OK, intent);
finish();
代码演示如下:
A类
Intent intent10 = new Intent(UI_Base.this, StarActivityForResultAct.class);
startActivityForResult(intent10, 1); // 请求码1 一定要>=0
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode){ // 请求码1
case 1:
switch (resultCode){ // 响应码
case 2:
Toast.makeText(UI_Base.this,data.getStringExtra("value"),Toast.LENGTH_SHORT).show();
break;
default:
break;
}
break;
default:
break;
}
}
B类
public class StarActivityForResultAct extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_star_activity_for_result);
}
/**
* android:onClick="doSomethingThenReturn"
* 对应xml中的属性,记得方法里面的参数,否则报错
* @param view
*/
public void doSomethingThenReturn(View view){
Intent intent = new Intent();
intent.putExtra("value","返回给前个Act的值");
// 通过intent对象返回结果,setResult的第一个参数是响应码
setResult(2, intent);
// 关闭当前Activity
finish();
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/id_btn_doSomethingThenReturn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doSomethingThenReturn"
android:text="doSomethingThenReturn"/>
</RelativeLayout>
在Button标签中有一个属性android:click,可以直接指定按钮单击事件的方法名。这样在Activity中就不用创建按钮对象而处理按钮单击事件了。 如果在程序中只处理单击事件,而不直接引用相应的对象,可以采用这种方法。
视图(View)
在Android系统中任何可视化控件都必须从android.view.View类继承。
两种方式创建View对象:
- 使用XML来配置View的相关属性,然后再装载这些View
- 完全使用Java代码的方式来创建View
视图简介
Android中视图类可以分为三种
- 布局类(Layout)
- 视图容器(View Container)
- 视图类 (例如TextView…)
使用xml布局文件定义视图
注意事项:
- XML布局文件的扩展名必须是xml
- XML布局文件名必须符合Java变量的命名规则(以为在R类中会自动生成一个变量),例如不能以数字开头
- XML布局文件的根节点可以是任意的控件标签,比如
<LinearLayout> <TextView>
- XML布局文件的根节点必须包含android命名控件,且必须是http://schemas.android.com/apk/res/android
- 为XML布局文件中的标签指定ID时需要使用这样的格式:@+id/value ,其中@+ 语法标识如果ID在R.id类中不出在,这产生一个与ID同名的变量,如果存在,则直接使用。 value表示ID的值。
- 视图ID的值也要符合java变量的命名规则
在获得XML布局文件中的视图对象需要注意如下几点
- finddViewById需要在setContentView之后使用。
- findViewById只能获得已经装载的XML布局文件中的视图对象。
- 在不同的XML布局文件中可以存在相同ID的视图,但是在同一个XML文件中,虽然也可以有相同ID的视图,但是通过ID获取视图时,只能够获取按照定义的顺序的第一个视图对象,其他相同ID值的视图对象将无法回去,因此在同一个XML布局文件中尽量使视图ID唯一。
在代码中控制视图
举个例子:
TextView tv = (TextView)findViewById(R.id.textView1);
tv.setText("UUUUU");
还可以使用字符串资源对TextView进行文本修改
tv.setText(R.string.hello);
注意:当seText方法的参数是int型时,会被认为这个参数值是一个字符串资源ID,因此,如果要将TextView的文本设置为一个整数,需要将这个整数抓换位String类型。例如 tv.setText(String.valueOf(200));将TextView的文本设置为200
在更高级的Android应用中,往往需要动态的添加视图,要实现这个功能,最重要的是要获得被添加的视图所在的容器对象,这个容器对象所对应的类需要继承ViewGroup类。
将其他的视图添加到当前的容器视图中的步骤如下:
- 获得当前容器视图对象
- 获得或者创建待添加的视图对象
- 将相对应的视图对象添加到容器视图中。
场景:
假设有两个xml布局文件:test1.xml test2.xml
这两个xml的根节点都是<LinearLayout>
, 目的获取test2.xml中的LinearLayout对象,并将该对象作为test1.xml文件中的<LinearLayout>
标签的子节点添加到test1.xml的LinearLayout对象中。
第一种方式:
// 获得test1.xml中的LinearLayout对象
LinearLayout l1 = (LinearLayout)getLayoutInflater().inflate(R.layout.test,null);
// 将test1.xml中的LinearLayout对象设置为当前容器视图
setContentView(l1);
// 获取test2.xml中的LinearLayout对象,并将该对象添加到test1.xml中的LinearLayout中
LinearLayout l2 = (LinearLayout)getInflater().inflater(R.layout.test2,l1);
参数解释: inflate()方法第一个参数标识XML布局资源文件的ID,
第二个参数标识获得容器对象后,要将该对象添加到哪个视图对象中。
如果不想添加到任何其他的容器中,设置为null即可。
第二种方式:
// 获得test1.xml中的LinearLayout对象
LinearLayout l1 = (LinearLayout)getLayoutInflater().inflate(R.layout.test,null);
// 将test1.xml中的LinearLayout对象设置为当前容器视图
setContentView(l1);
// 获取test2.xml中的LinearLayout对象,并将该对象添加到test1.xml中的LinearLayout中
LinearLayout l2 = (LinearLayout)getInflater().inflater(R.layout.test2,null);
l1.addView(l2);
inflate方法第二个参数设置为null, 通过addView方法添加
第三种方式
完全使用Java代码创建一个视图对象,并将该对象添加到容器视图中
TextView tv = new TextView(this);
l1.addView(tv)
注意事项:
- 如果使用setContentView方法将试图容器设置为当前视图后,还想要向试图容器中添加新的视图或者进行其他操作,setContentView方法的参数值应直接使用容器视图对象,因为这样可以向容器视图对象中添加新的视图。
- 一个视图只能有一个父视图。也就是说一个视图只能被包含在一个容器视图中。因此,在向容器视图中添加其他视图时,不能将XML布局文件中非根节点的视图对象添加到其他的容器视图中。
布局(Layout)
框架布局FrameLayout
最简单的布局方式,FrameLayout 以层叠放方式显示,第一个添加到框架布局中的视图显示在最底层,最后一个放在最顶层。
上一层视图会覆盖下一层视图,类似于堆栈,因此也被称为堆栈布局。
线性布局LinearLayout
最常用的布局方式。
线性布局可以分为水平线性布局和垂直先行布局。
android:orientation ,两个字 horizontal 、vertical。 默认horizontal。
一个非常重要的属性 gravity,用于控制布局中视图的位置。
设置多个属性,需要使用“|”分隔,在属性值和“|”之间不能有其他符号(例如空格和制表符等)。
属性值 | 描述 |
---|---|
top | 将视图放到屏幕顶端 |
bottom | 将视图放到屏幕底端 |
left | 将视图放到屏幕左侧 |
right | 将视图放到屏幕右侧 |
center_vertical | 将视图按垂直方向居中显示 |
center_horizontal | 将视图按水平方向居中显示 |
center | 将视图按垂直和水平方向居中显示 |
LinearLayout标签中的子标签还可以使用layout_gravity和layout_weight属性来设置每一个视图的位置
layout_gravity 属性的取值和gravity的取值相同,表示当前视图在布局中的位置。
layout_weight属性是一个非负整数,如果该属性值大于0,线性布局会根据水平或者垂直方向以及不同视图的layout_weight属性值占所有视图的layout_weight属性值之和的比例为这些视图分配自己说占用的区域,视图将按相应比例拉伸。
相对布局RelativeLayout
设置某一个视图相对于其他视图的位置。
表格布局TableLayout
一个表格布局由一个<TableLayout>
标签和若干个<TableRow>
标签组成
绝对布局AbsoluteLayout
android:layout_x 和android:layout_y设置横纵坐标。
重用XML布局
布局重用
<inclued>
include标签可以实现在一个layout中引用另一个layout的布局,这通常适合于界面布局复杂、不同界面有共用布局的APP中,比如一个APP的顶部布局、侧边栏布局、底部Tab栏布局、ListView和GridView每一项的布局等,将这些同一个APP中有多个界面用到的布局抽取出来再通过include标签引用,既可以降低layout的复杂度,又可以做到布局重用(布局有改动时只需要修改一个地方就可以了)。
inclued标签,首字母要小写,只有layout属性是必选的。
include标签的使用很简单,只需要在布局文件中需要引用其它布局的地方,使用layout=”@layout/child_layout”就可以了:
<include layout="@layout/titlebar"/>
如果要覆盖布局的尺寸,必须同时覆盖android:layout_weight和android:layout_height . 不能只覆盖一个,否则无效
建议将给include标签调用布局设置宽高、位置、ID等工作放在调用布局的根标签中,这样可以避免给include标签设置属性不当造成的各种问题(之前遇到过给include标签设置android:id属性后,程序实例化子布局中组件失败的现象):
应该这样:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bottomBarLayoutId"
android:layout_width="match_parent"
android:layout_height="61dp"
android:orientation="horizontal"
android:layout_alignParentBottom="true">
。。。
</LinearLayout>
<include layout="@layout/include_voice_ctrl_bar_layout" />
而不是这样:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
。。。
</LinearLayout>
<include
android:id="@+id/bottomBarLayoutId"
android:layout_width="match_parent"
android:layout_height="61dp"
android:layout_alignParentBottom="true"
layout="@layout/include_voice_ctrl_bar_layout"
/>
优化XML布局
减少视图层级
<merge />
<merge />
标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。<merge />
多用于替换FrameLayout或者当一个布局包含另一个时,<merge />
标签消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这是如果include布局使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用<merge />
标签优化
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/add"/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</merge>
现在,当你添加该布局文件时(使用<include />
标签),系统忽略<merge />
节点并且直接添加两个Button。更多<merge />
介绍可以参考《Android Layout Tricks #3: Optimize by merging》
需要时使用<ViewStub />
<ViewStub />
标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的布局想进度条、显示错误消息等可以使用<ViewStub />
标签,以减少内存使用量,加快渲染速度。<ViewStub />
是一个不可见的,大小为0的View。<ViewStub />
标签使用如下:
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/progress_overlay"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
当你想加载布局时,可以使用下面其中一种方法:
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
// or
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
当调用inflate()函数的时候,ViewStub被引用的资源替代,并且返回引用的view。 这样程序可以直接得到引用的view而不用再次调用函数findViewById()来查找了。
注:ViewStub目前有个缺陷就是还不支持 <merge />
标签。
更多<ViewStub />
标签介绍可以参考《Android Layout Tricks #3: Optimize with stubs》
查看APK文件中的布局
AXMLPrinter2工具