《第一行代码Android》笔记

第1章 开始启程,你的第一行Android代码

1.1 了解全貌,Android王国简介

  1. Android系统是基于Linux 2.6内核的,这一层为Android设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。
  2. 另外Android运行时库还包含了Dalvik虚拟机,它使得每一个Android应用都能运行在独立的进程当中,并且拥有一个自己的Dalvik虚拟机实例。
  3. 2011年2月,谷歌发布了Android 3.0系统,这个系统版本是专门为平板电脑设计的,但也是Android为数不多比较失败的版本,推出之后一直不见什么起色,市场份额也少的可怜。
  4. 广播接收器可以允许你的应用接收来自各处的广播消息,比如电话、短信等,当然你的应用同样也可以向外发出广播消息。内容提供器则为应用程序之间共享数据提供了可能,比如你想要读取系统电话簿中的联系人,就需要通过内容提供器来实现。

1.2 手把手带你搭建开发环境

  1. 但我觉得Eclipse最吸引人的地方是超强的插件功能。Eclipse支持极多的插件工具,使得它不仅仅可以用来开发Java,还可以很轻松地支持几乎所有主流语言的开发,当然也非常适合Android开发。

1.3 创建你的第一个Android项目

  1. 接着Package Name代码项目的包名,Android系统就是通过包名来区分不同应用程序的,因此包名一定要有唯一性,这里我们填入com.test.helloworld。
  2. Target SDK是指你在该目标版本上已经做过了充分的测试,系统不会再帮你在这个版本上做向前兼容的操作了,这里我们选择最高版本Android 4.4。
  3. 点击Eclipse导航栏中的Window->Open Perspective->DDMS,这时你会进入到DDMS的视图中去。DDMS中提供了很多我们开发Android程序时需要用到的工具,不过目前你只需要关注Devices窗口中有没有Online的设备就行了。
  4. 如果你的Devices窗口中虽然有设备,但是显示Offline,说明你的模拟器掉线了,这种情况概率不高,但是如果出现了,你只需要点击Reset adb就好了。
  5. gen这个目录里的内容都是自动生成的,主要有一个R.java文件,你在项目中添加的任何资源都会在其中生成一个相应的资源id。这个文件永远不要手动去修改它。
  6. AndroidManifest.html是你整个Android项目的配置文件,你在程序中定义的所有四大组件都需要在这个文件里注册。另外还可以在这个文件中给应用程序添加权限声明,也可以重新指定你创建项目时指定的程序最低兼容版本和目标版本。
  7. 其中intent-filter里的两行代码非常重要,<action android:name=”android.intent.action.MAIN” />和<category android:name=”android.intent.category.LAUNCHER” />表示HelloWorldActivity是这个项目的主活动,在手机上点击应用图标,首先启动的就是这个活动。
  8. 当然这只是理想情况,更多的时候美工只会提供给我们一份图片,这时你就把所有图片都放在drawable-hdpi文件夹下就好了。
  9. 比如刚刚在strings.xml中找到的Hello world!字符串,我们有两种方式可以引用它:1. 在代码中通过R.string.hello_world可以获得该字符串的引用;2. 在XML中通过@string/hello_world可以获得该字符串的引用。

1.4 行前必备,掌握日志工具的使用

  1. 点击Eclipse导航栏中的Window->Show View->Other,会弹出一个Show View对话框。你在Show View对话框中展开Android目录,会看到有一个LogCat的子项。
  2. Log.d方法中传入了两个参数,第一个参数是tag,一般传入当前的类名就好,主要用于对打印信息过滤。第二个参数是msg,即想要打印的具体的内容。
  3. 如果你的LogCat中并没有打印出任何信息,有可能是因为你当前的设备失去焦点了。这时你只需要进入到DDMS视图,在Devices窗口中点击一下你当前的设备,打印信息就会出来了。
  4. 不知道你有没有体会到使用过滤器的好处,可能现在还没有吧。不过当你的程序打印出成百上千行日志的时候,你就会迫切地需要过滤器了。
  5. 当前我们选中的级别是verbose,也就是最低等级。这意味着不管我们使用哪一个方法打印日志,这条日志都一定会显示出来。而如果我们将级别选中为debug,这时只有我们使用debug级别以上方法打印的日志才会显示出来,依此类推。Log.v() verbose;Log.d() debug;Log.i() info;Log.w() warn;Log.e() error

第2章 先从看得到的入手,探究活动

2.2 活动的基本用法

2.2.1 手动创建活动

  1. 目前ActivityTest项目的src目录应该是空的,你应该在src目录下先添加一个包。点击Eclipse导航栏中的File->New->Package,在弹出窗口中填入我们新建项目时使用的默认包名com.example.activitytest,点击Finish。

2.2.2 创建和加载布局

  1. 右击res/layout目录->New->Android XML File,会弹出创建布局文件的窗口。我们给这个布局文件命名为first_layout,根元素就默认选择为LinearLayout。
  2. 如果你需要在XML中引用一个id,就使用@id/id_name这种语法,而如果你需要在XML中定义一个id,则要使用@+id/id_name这种语法。

2.2.3 在AndroidManifest文件中注册

  1. 由于最外层的<manifest>标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册活动时这一部分就可以省略了,直接使用.FirstActivity就足够了。
  2. 另外需要注意,如果你的应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这种程序一般都是作为第三方服务供其他的应用在内部进行调用的,如支付宝快捷支付服务。

2.2.4 隐藏标题栏

  1. 隐藏标题栏:
// 隐藏标题栏
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.first)layout);
}

2.2.5 在活动中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间:

// 定义一个弹出Toast的触发点
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(r.layout.first_layout);
    Button button1 = (Button) findViewById(R.id.button_1);
    button1.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(FirstActivity.this, "You clicked Button 1",
                        Toast.LENGTH_SHORT).show();
        }
    });
}

findViewById()方法返回的是一个View对象,我们需要向下转型将它转成Button对象。

makeText(0方法需要传入三个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动本身就是一个Context对象,因此这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG。

2.2.6 在活动中使用Menu

首先在res目录下新建一个menu文件夹,右击res目录->New->Folder,输入文件夹名menu,点击Finish。接着在这个文件夹下再新建一个名叫main的菜单文件,右击menu文件夹->New->Android XML File:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/add_item"
        android:title="Add" />
    <item
        android:id="@+id/remove_item"
        android:title="Remove" />
</menu>
// 打开FirstActivity,重写onCreateOptionsMenu()方法
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}
// 仅仅让菜单显示出来是不够的,还要再定义菜单响应事件,重写onOptionsItemSelected()方法
public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
        case R.id.add_item:
            Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
            break;
        case R.id.remove_item:
            Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
            break;
        default:
    }
    return true;
}

可以看到,菜单默认是不会显示出来的,只有按下了Menu键,菜单才会在底部显示出来,这样我们就可以放心地使用菜单了,因为它不会占用任何活动的空间。

2.2.7 销毁一个活动

只要按一下Back键就可以销毁当前的活动了。Activity类提供了一个finish()方法,我们在活动中调用一下这个方法就可以销毁当前活动了。

2.3 使用Intent在活动之间穿梭

2.3.1 使用显式Intent

  1. 由于SecondActivity不是主活动,因此不需要配置<intent-filter>标签里的内容,注册活动的代码也是简单了许多。
  2. Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务、以及发送广播等场景。
  3. Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)。这个构造函数接收两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent的“意图”。
// 用intent启动Activity
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
        startActivity(intent);
    }
});

如果你想要回到上一个活动怎么办呢?很简单,按下Back键就可以销毁当前活动,从而回到上一个活动了。

2.3.2 使用隐式Intent

相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。

<!-- 通过在<activity>标签下配置<intent-filter>的内容,可以指定当前活动能够响应的action和category -->
<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>和<category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。

// 隐式的intent
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("com.example.activitytest.ACTION_START");
        startActivity(intent);
    }
});
// 每个Intent中只能指定一个action,但却能指定多个category
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("com.example.activitytest.ACTION_START");
        intent.addCategory("com.example.activitytest.MY_CATEGORY");
        startActivity(intent);
    }
});

2.3.3 更多隐式Intent的用法

使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如说你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了。

// intent启动浏览器
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        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。

与此对应,我们还可以在<intent-filter>标签中再配置一个<data>标签,用于更精确地指定档期那活动能够响应什么类型的数据。<data>标签中主要可以配置以下内容。

android:scheme 用于指定数据的协议部分,如http
android:host 用于指定数据的主机名部分,如www.baidu.com
android:port 用于指定数据的端口部分,一般紧随在主机名之后
android:path 用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容
android:mimeType 用于指定可以处理的数据类型,允许使用通配符的方式进行指定

不过一般在<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的所有程序。点击Browser还会像之前一样打开浏览器,并显示百度的主页,而如果点击了ActivityTest,则会启动ThirdActivity。需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要去做这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。

除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。

// 在程序中调用系统拨号界面
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_DIAL);
        intent.setData(Uri.parse("tel:10086"));
        startActivity(intent);
    }
});

2.3.4 向下一个活动传递数据

// 用intent发送数据
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String data = "Hello SecondActivity";
        Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
        intent.putExtra("extra_data", data);
        startActivity(intent);
    }
});
// 取出数据
public class SecondActivity extends Activity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.second_layout);
        Intent intent = getIntent();
        String data = intent.getStringExtra("extra_data");
        Log.d("SecondActivity", data);
    }
}

这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传递的数据,如果传递的是整型数据,则使用getIntExtra()方法,传递的是布尔型数据,则使用getBooleanExtra()方法,以此类推。

2.3.5 返回数据给上一个活动

Activity中还有一个startActivityForResult()方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。

// 使用startActivityForResult启动intent
button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
        startActivityForResult(intent, 1);
    }
});

// 添加返回数据
public class SecondActivity extends Activity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.second_layout);
        Button button2 = (Button) findViewById(R.id.button_2);
        button2.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.putExtra("data_return", "Hello FirstActivity");
                setResult(RESULT_OK, intent);
                finish();
            }
        });
    }
}

// 由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁
// 之后会回调上一个活动的onActivityResult方法,因此我们需要在FirstActivity中重写这个方法来得到
// 返回的数据
@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("FirstActivity", returnedData);
            }
            break;
        default:
    }
}

// 这时候你会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity
// 这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,重写onBackPressed()
@Override
public void onBackPressed() {
    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    finish();
}

2.4 活动的生命周期

2.4.1 返回栈

  1. 其实Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。
  2. 系统总是会显示处于栈顶的活动给元素。

2.4.2 活动状态

  1. 当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域。

2.4.3 活动的生存期

  1. onCreate()会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。
  2. onStart()方法在活动由不可见变为可见的时候调用。
  3. onPause()方法在系统准备去启动或者恢复另一个活动的时候调用。
  4. onStop()方法在活动完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。
  5. 可见生存期:活动在onStart()方法和onStop()方法之间所经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法,合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中队资源进行释放,从而保证处于停止状态的活动不会占用过多内存。
  6. 前台生存期:活动在onResume()方法和onPause()方法之间所经历的,就是前台生存期。

2.4.4 体验活动的生命周期

<!--注册普通活动和对话框式的活动-->
<activity android:name=".NormalActivity" >
</activity>
<activity android:name=".DialogActivity" android:theme="@android:style/Theme.Dialog" >
</activity>

由于NormalActivity已经把MainActivity完全遮挡住,因此onPause()和onStop()方法都会得到执行。

可以看到,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。

2.4.5 活动被回收了怎么办

想象以下场景,应用中有一个活动A,用户在活动A的基础上启动了活动B,活动A就进入了停止状态,这个时候由于系统内存不足,将活动A回收掉了,然后用户按下Back键返回活动A,会出现什么情况呢?其实还是会正常显示活动A的,只不过这时并不会执行onRestart()方法,而是会执行活动A的onCreate()方法。

Activity中还提供了一个onSaveInstanceState()回调方法,这个方法会保证一定在活动被回收之前调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。

onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法来保存字符串,使用putInt()方法保存整型数据,以此类推。

// 保存临时数据到Bundle中
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    String tempData = "Something you just typed";
    outState.putString("data_key", tempData);
}

数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是当活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可:

// 从Bundle中取出保存临时数据
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate");
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null) {
        String tempData = savedInstanceState.getString("data_key");
        Log.d(TAG, tempData);
    }
    ...
}

Intent还可以结合Bundle一起用于传递数据的,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。

2.5 活动的启动模式

2.5.1 standard

  1. 启动模式一共有四种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。
  2. 对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
  3. 从打印信息中我们可以看出,每点击一次按钮就会创建出一个新的FirstActivity实例。此时返回栈中也会存在三个FirstActivity的实例,因此你需要连按三次Back键才能退出程序。

2.5.2 singleTop

  1. 当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。
  2. 不过当FirstActivity并未处于栈顶位置时,这时再启动FirstActivity,还是会创建新的实例的。

2.5.3 singleTask

  1. 当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
  2. 其实从打印信息中就可以明显看书了,在SecondActivity中启动FirstActivity时,会发现返回栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶活动,因此FirstActivity的onRestart()方法和SecondActivity的onDestroy()方法会得到执行。

2.5.4 singleInstance

  1. 不同于以上三种启动模式,指定为singleInstance模式的活动会启动一个新的返回栈来管理这个活动(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。
  2. 想象以下场景,假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?使用前面三种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
  3. 可以看到,SecondActivity的Task id不同于FirstActivity和ThirdActivity,这说明SecondActivity确实是存放在一个单独的返回栈里的,而且这个栈中只有SecondActivity这一个活动。
  4. 然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序。(郭霖还是没有举一个共享Activity的例子,他只是证明了用singleInstance启动的Activity放在一个不同的返回栈里。)

2.6 活动的最佳实践

2.6.2 随时随地退出程序

// 新建一个ActivityCollector类作为活动管理器
public class ActivityCollector {
   
    public static List<Activity> activities = new ArrayList<Activity>();
    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();
            }
        }
    }
}
// 接下来修改BaseActivity中的代码
public class BaseActivity extends Activity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("BaseActivity", getClass().getSimpleName());
        ActivityCollector.addActivity(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        ActivityCollector.removeActivity(this);
    }
}
// 从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了
public class ThirdActivity extends BaseActivity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("ThirdActivity", "Task id is " + getTaskId());
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.third_layout);
        Button button3 = (Button) findViewById(R.id.button_3);
        button3.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ActivityCollector.finishAll();
            }
        });
    }
}

第3章 软件也要拼脸蛋,UI开发的点点滴滴

3.1 该如何编写程序界面

  1. 不过以上的方式我都不推荐你使用,因为使用可视化编辑工具并不利于你去真正了解界面背后的实现原理,通常这种方式制作出的界面都不具有很好的屏幕适配性,而且当需要编写较为复杂的界面时,可视化编辑工作将很难胜任。因此本书中所有的界面我们都将使用最基本的方式去实现,即编写XML代码。

3.2 常见控件的使用方法

3.2.1 TextView

  1. 然后使用android:layout_width指定了控件的宽度,使用android:layout_height指定了控件的高度。Android中所有的控件都具有这两个属性,可选值有三种:match_parent、fill_parent和wrap_content,其中**match_parent和fill_parent的意义相同,现在官方更加推荐使用match_parent。**match_parent表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。
  2. 我们使用android:gravity来指定文字的对齐方式,可选值有top、bottom、left、right、center等,可以用“|”来同时指定多个值,这里我们指定的“center”,效果等同于“center_vertical|center_horizontal”,表示文字在垂直和水平方向都居中对齐。

3.2.2 Button

  1. 如果你不喜欢使用匿名类的方式来注册监听器,也可以使用实现接口的方式来进行注册:
public class MainActivity extends Activity implements OnClickListener {
   
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.button:
            // 在此处添加逻辑
            break;
        default:
            break;
        }
    }
}

3.2.3 EditText

  1. 其实看到这里,我估计你已经总结出Android控件的使用规律了,基本上用法都很相似,给控件定义一个id,再指定下控件的宽度和高度,然后再适当加入些控件特有的属性就差不多了。
  2. 不过随着输入的内容不断增多,EditText会被不断拉长。这时由于EditText的高度指定的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非常难看。我们可以使用android:maxLines属性来解决问题。
  3. 这里通过android:maxLines指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,而EditText则不会再继续拉伸。

3.2.4 ImageView

  1. 可以看到,这里使用android:src属性给ImageView指定了一张图片,并且由于图片的宽和高都是未知的,所以将ImageView的宽和高都设定为wrap_content,这样保证了不管图片的尺寸是多少都可以完整地展示出来。

3.2.5 ProgressBar

  1. 这时你可能会问,旋转的进度条表明我们的程序正在加载数据,那数据总会有加载完的时候吧,如何才能让进度条在数据加载完成时消失呢?这里我们就需要用到一个新的知识点,Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有三种,visible、invisible和gone。visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。gone则表示控件不仅不可见,而且不再占用任何屏幕空间。我们还可以通过代码来设置控件的可见性,使用的是setVisibility()方法,可以传入View.VISIBLE、View.INVISIBLE和View.GONE三种值。

3.2.6 AlertDialog

  1. AlertDialog可以在当前的界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此一般AlertDialog都是用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。
public class MainActivity extends Activity implements OnClickListener {
   
    ...
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.button:
            AlertDialog.Builder dialog = 
                new AlertDialog.Builder(MainActivity.this);
            dialog.setTitle("This is Dialog");
            dialog.setMessage("Something important.");
            dialog.setCancelable(false);
            dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) { }
            });
            dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) { }
            });
            dialog.show();
            break;
        default:
            break;
        }
    }
}

3.2.7 ProgressDialog

  1. 不同的是,ProgressDialog会在对话框中显示一个进度条,一般是用于表示当前操作比较耗时,让用户耐心等待。
public class MainActivity extends Activity implements OnClickListener {
   
    ...
    @Override
    public void onClick(View v) {
        switch(v.getId()) {
        case R.id.button:
            ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
            progressDialog.setTitle("This is ProgressDialog");
            progressDialog.setMessage("Loading...");
            progressDialog.setCancelable(true);
            progressDialog.show();
            break;
        default:
            break;
        }
    }
}

如果在setCancelable()中传入了false,表示ProgressDialog是不能通过Back键取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。

3.3 详解四种基本布局

布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现。

3.3.1 LinearLayout

  1. 从名字就可以看出,android:gravity是用于指定文字在控件中的对齐方式,而android:layout_gravity是用于指定控件在布局中的对齐方式。
  2. 当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。
  3. 你会发现,这里竟然将EditText和Button的宽度都指定成了0,这样文本编辑框和按钮还能显示出来吗?不用担心,由于我们使用了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定,这里指定成0是一种比较规范的写法。
  4. 然后我们在EditText和Button里都将android:layout_weight属性的值指定为1,这表示EditText和Button将在水平方向平分宽度。
  5. 这里我们仅指定了EditText的android:layout_weight属性,并将Button的宽度改回wrap_content。这表示Button的宽度仍然按照wrap_content来计算,而EditText则会占满屏幕所有的剩余空间。

3.3.2 RelativeLayout

  1. android:layout_alignParentLeft、android:layout_alignParentTop、android:layout_alignParentRight、android:layout_alignParentBottom、android:layout_centerInParent这几个属性都是相对于父布局进行定位的,那控件可不可以相对于控件进行定位呢?当然是可以的:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Button 3" />
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/button3"
        android:layout_toLeftOf="@id/button3"
        android:text="Button 1" />
    <!-- ... -->
    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button3"
        android:layout_toRightOf="@id/button3"
        android:text="Button 5" />
</RelativeLayout>

注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。

3.3.3 FrameLayout

  1. 这种布局没有任何定位方式,所有的控件都会摆放在布局的左上角。

3.3.4 TableLayout

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TableRow>
        <TextView
            android:layout_height="wrap_content"
            android:text="Account:" />
        <EditText
            android:id="@+id/account"
            android:layout_height="wrap_content"
            android:hint="Input your account" />
    </TableRow>
    <TableRow>
        <TextView
            android:layout_height="wrap_content"
            android:text="Password:" />
        <EditText
            android:id="@+id/password"
            android:layout_height="wrap_content"
            android:inputType="textPassword" />
    </TableRow>
    <TableRow>
        <Button
            android:id="@+id/login"
            android:layout_height="wrap_content"
            android:layout_span="2"
            android:text="Login" />
    </TableRow>
</TableLayout>

在TableLayout中每加入一个TableRow就表示在表格中添加了一行,然后在TableRow中每加入一个控件,就表示在该行中加入了一列,TableRow中的控件是不能指定宽度的。

不过从图中可以看出,当前登录界面没有充分利用屏幕的宽度,右侧还空出了一块区域,这也难怪,因为在TableRow中我们无法指定空间的宽度。这时使用android:stretchColumns属性就可以很好地解决这个问题,它允许将TableLayout中的某一列进行拉伸,以达到自动适应屏幕宽度的作用。

这里将android:stretchColumns的值指定为1,表示如果表格不能完全占满屏幕宽度,就将第二列进行拉伸。没错!指定成1就是拉伸第二列,指定成0就是拉伸第一列。

3.4 系统控件不够用?创建自定义控件

可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中一种最基本的UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多的子View和子ViewGroup,是一个用于放置控件和布局容器。

3.4.1 引入布局

  1. 为了避免在每个活动中的布局中都编写一遍同样的标题栏代码,我们可以使用引入布局的方式,新建一个布局title.xml。两个Button和一个TextView。
<!-- 现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <include layout="@layout/title" />
</LinearLayout>
<!-- 最后别忘了在MainActivity中将系统自带的标题栏隐藏掉 -->

3.4.2 创建自定义控件

// 新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件
public class TitleLayout extends LinearLayout {
   
    public TtileLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 已经利用了前面写的title.xml
        LayoutInflater.from(context).inflate(R.layout.title, this); 
    }
}

首先我们重写了LinearLayout中的带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。

<!-- 现在自定义控件已经创建好了,然后我们需要在布局文件中添加这个自定义控件 -->
<LinearLayout xmlns:android=... >

    <com.example.uicustomviews.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ></com.example.uicustomviews.TitleLayout>
</LinearLayout>

添加自定义控件和添加普通控件的方式是一样的,只不过在添加自定义控件的时候我们需要指明控件的完整类名,包名在这里是不可以省略的。

重新运行程序,你会发现此时效果和使用引入布局方式的效果是一样的。

// 然后我们为标题栏中的按钮注册点击事件,修改TitleLayout中的代码
public class TitleLayout extends LinearLayout {
   
    public TitleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
        Button titleBack = (Button) findViewById(R.id.title_back);
        Button titleEdit = (Button) findViewById(R.id.title_edit);
        titleBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ((Activity) getContext()).finish();
            }
        });
        titleEdit.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), "You clicked Edit button",
                    Toast.LENGTH_SHORT).show();
            }
        });
    }
}

这样的话,每当我们在一个布局中引入TitleLayout,返回按钮和编辑按钮的点击事件就已经自动实现好了,也是省去了很多编写重复代码的工作。

3.5 最常用和最难用的控件——ListView

3.5.1 ListView的简单用法

public class MainActivity extends Activity {
   
    private String[] data = { "Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                MainActivity.this, android.R.layout.simple_list_item_1, data);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
}

不过,数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。

这里由于我们提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。注意我们使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。

最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。

3.5.2 定制ListView的界面

  1. Fruit类中只有两个字段,name表示水果的名字,imageId表示水果对应图片的资源id。(资源id去本质上全是int)
  2. 然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建fruit_item.xml
<LinearLayout ... 
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginLeft="10dip" />
</LinearLayout>
// 接下来创建自定义的适配器
// 要知道其实适配器的处理单元好像是Item,并非整个ListView
public class FruitAdapter extends ArrayAdapter<Fruit> {
   
    private int resourceId;
    // textViewResourceId指的是ListView子项布局的id
    public FruitAdapter(Context context, int textViewResourceId,
            List<Fruit> objects) {
        super(context, textViewResourceId, objects);
        resourceId = textViewResourceId;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position); //获取当前项的Fruit实例
        View view = LayoutInflater.from(getContext()).inflate(resourceId, null);
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据都传递进来。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在getView方法中,首先通过getItem()方法得到当前项的Fruit实例,然后使用LayoutInflater来为这个子项加载我们传入的布局,接着调用View的findViewById()方法分别获取到ImageView和TextView的实例,并分别调用它们的setImageResource()和setText()方法来设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。

public class MainActivity extends Activity {
   
    private List<Fruit> fruitList = new ArrayList<Fruit>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits(); //初始化水果数据
        FruitAdapter adapter = new FruitAdapter(MainActivity.this,
                R.layout.fruit_item, fruitList);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
    private void initFruits () {
        Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
        fruitList.add(apple);
        ...
        Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
        fruitList.add(mango);
    }
}
// 这样定制ListView界面的任务就完成了

虽然目前我们定制的界面还是很简单,但是相信你已经领悟到了诀窍,只要修改fruit_item.xml中的内容,就可以定制出各种复杂的界面了。

3.5.3 提升ListView的运行效率

  1. 目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法每次都将布局重新加载一遍,当ListView快速滚动的时候这就会成为性能的瓶颈。
  2. 仔细观察,getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便以后可以进行重用。
  3. 虽然现在已经不会再重复去加载布局,但是每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。我们可以借助一个V内部类iewHolder来对控件的实例进行缓存。
// 在适配器中应用这两个优化措施
public class FruitAdapter extends ArrayAdapter<Fruit> {
   
    ...
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
            view.setTag(viewHolder); //将ViewHolder存储在View中
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag(); //重新获取viewHolder
        }
        viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());
        return view;
    }
    class ViewHolder {
        ImageView fruitImage;
        TextView fruitImage;
    }
}
// 通过这两步优化之后,我们ListView的运行效率就已经非常不错了

3.5.4 ListView的点击事件

// ListView响应用户的点击事件,修改MainActivity
public class MainActivity extends Activity {
   
    private List<Fruit> fruitList = new ArrayList<Fruit>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();
        FruitAdapter adapter = new FruitAdapter(MainActivity.this,
                R.layout.fruit_item, fruitList);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                Fruit fruit = fruitList.get(position);
                Toast.makeText(MainActivity.this, fruit.getName(),
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
    ...
}

可以看到,我们使用了setOnItemClickListener()方法来为ListView注册了一个监听器,当用户点击了ListView中的任何一个子项时就会回调onItemClick()方法,在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toase将水果的名字显示出来。

3.6 单位和尺寸

3.6.1 px和pt的窘境

  1. pt是磅数的意思,1磅等于1/72英寸,一般pt都会作为字体的单位来使用。
  2. 可是现在到了手机上,这两个单位就显得有些力不从心了,因为手机的分辨率各不相同,一个200px宽的按钮在低分辨率的手机上可能将近占据满屏,而到了高分辨率的手机上可能只占据屏幕的一半。
  3. 可以明显看出,同样200px宽的按钮在不同分辨率的屏幕上显示的效果是完全不同的,pt的情况和px差不多,这导致这两个单位在手机领域上面很难有所发挥。

3.6.2 dp和sp来帮忙

  1. dp是密度无关像素的意思,也被称作dip,和px相比,它在不同密度的屏幕中的显示比例将保持一致。
  2. 这里有一个新名词需要引起我们的注意,什么叫密度?Android中的密度就是屏幕每英寸所包含的像素数,通常以dpi为单位。比如一个手机屏幕的宽是2英寸长是3英寸,如果它的分辨率是320*480像素,那这个屏幕的密度就是160dpi,如果它的分辨率是640*960,那这个屏幕的密度就是320dpi,因此密度值越高的屏幕显示的效果就越精细。
// 我们可以通过代码来得知当前屏幕的密度值是多少
public class MainActivity extends Activity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        float xdpi = getResources().getDisplayMetrics().xdpi;
        float ydpi = getResources().getDisplayMetrics().ydpi;
        Log.d("MainActivity", "xdpi is " + xdpi);
        Log.d("MainActivity", "ydpi is " + ydpi);
    }
}

根据Android的规定,在160dpi的屏幕上,1dp等于1px,而在320dpi的屏幕上,1dp就等于2px。因此,使用dp来指定控件的宽和高,就可以保证控件在不同密度的屏幕中的显示比例保持一致。

<!-- 使用dp替换px -->
<LinearLayout ... >
    <Button
        android:id="@+id/button"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="Button"
        />
</LinearLayout>

sp的原理和dp是一样的,它主要是用于指定文字的大小,这里就不再进行介绍了。

总结一下,在编写Android程序的时候,尽量将控件或布局的大小指定成match_parent或wrap_content,如果必须要指定一个固定值,则使用dp来作为单位,指定文字大小的时候使用sp作为单位。

3.7 编写界面的最佳实践

3.7.1 制作Nine-Patch图片

  1. 先学习一下如何制作Nine-Patch图片。它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸而哪些区域不可以。
<!-- 将这一张图片设置为一个LinearLayout的背景图片,修改activity_main.xml -->
<RelativeLayout ...
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/message_left" >
    </LinearLayout>
</RelativeLayout>

可以看到,由于message_left的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉伸了!这种效果非常差,用户肯定是不能容忍的,这时我们就可以使用Nine-Patch图片来进行改善。

在Android sdk目录下有一个tools文件夹,在这个文件夹中找到draw9patch.bat文件,我们就是使用它来制作Nine-Patch图片的。双击打开之后,在导航栏点击File->Open 9-patch将message_left.png加载进来。

我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分就表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分则表示内容会被放置的区域。

这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也是有了很大的改进。

3.7.2 编写精美的聊天页面


                
  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值