Android第一行代码学习思考笔记(第一个Android项目、探究活动和UI开发)

第一个Android项目、探究活动和UI开发

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

似乎再多的困难也阻挡不了Android快速前进的步伐。由于谷歌的开放政策,任何手机厂商和个人都能免费获取到Android操作系统的源码,并且可以自由地使用和定制。

1.1了解全貌——Android王国简介

1.1.1Android系统架构

Android大致可以分为四层架构:Linux内核层、系统运行层、应用框架层和应用层。

1.Linux内核层

Android系统是基于Linux内核的,Linux内核层层为Android设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等

2.系统运行库层

系统运行库层通过一些C/C++库来为Android系统提供了主要的特性支持。如SQLite库提供了数据库的支持,OpenGL|ES库提供了3D绘图的支持,Webkit库提供了浏览器内核的支持等。
这一层还有Android运行时库主要提供了一些核心库,能允许开发者使用Java语言来编写Android应用。Android运行时库中还包含了Dalvik虚拟机(5.0系统之后改为ART运行环境),使得每一个Android应用都能运行在独立的进程当中,并且拥有一个自己的Dalvik虚拟机实例。相较于Java虚拟机,Dalvik是专门为移动设备定制的,针对手机内存、CPU性能有限等情况做了优化处理。

3.应用架构层

应用架构层主要提供了构建应用程序时可能用到的各种API,Android自带的一些核心应用就是使用这些API完成的,开发者也可以通过使用这些API来构建自己的应用程序。

4.应用层

所有安装在手机上的应用程序都是属于这一层的比如系统自带的联系人、短信等程序,或者是你从Google Play上下载的小游戏,当然还包括你自己开发的程序。

]

1.1.2Android应用开发特色

1.四大组件

Android系统四大组件:

**活动(Activity):**所有Android应用程序的门面,凡是在应用程序中看得到的东西,都放在活动中。

**服务(Service):**低调,看不到但会一直在后台默默运行,即使退出了应用服务仍可以继续运行。

**广播接收器(Broadcast Receiver):**允许的应用接收来自各处的广播消息,比如电话、短信等,当然应用可以向外发出广播消息。

**内容提供器(Content Provider):**为应用程序之间共享数据提供了可能,比如想要读取系统电话簿中的联系人就需要通过内容提供器来实现。

2.丰富的系统控件

Android系统为开发者提供了丰富的系统控件,使我们可以轻松地编写出漂亮的界面。品位高也能定制自己的控件。

3.SQLite数据库

Android系统自带了这种轻量级、运算速度极快的嵌入式关系型数据库。它不仅支持标准的SQL语法,还可以通过Android封装好的API进行操作,让存储和读取数据变得非常方便。

4.强大的多媒体

Android系统提供了如音乐、视频、录音、拍照、闹铃等等多媒体服务,可以在程序中通过代码进行控制,让应用变得更丰富多彩。

5.地理位置定位

现在Android手机都内置GPS,发挥想象可以做出创意十足的应用。

1.2开发环境

1.2.1准备所需要的工具

JDK:是Java语言的软件开发工具包,包含Java的运行环境、工具集合、基础类库等内容。

**Android SDK:**是谷歌提供的Android开发工具包,在开发Android程序时,我们需要通过引入该工具包使用Android相关的API

**Android Studio:**谷歌推出,强大方便。

1.3创建你的第一个Android项目

1.3.1分析你的第一个Android程序

1…gradle和.idea

。gradle和.idea目录下放置的都是Android Studio自动生成的一些文件,无需手动编辑。

2.app

项目中的代码、资源等内容几乎都放置在app目录下,我们后面的开发工作也基本都在app目录下进行,待会儿会对这个目录单独展开进行讲解。

3.build

build目录也无需过多关心,主要包含一些编译时自动生成的文件。

4.gradle

gradle目录下包含了gradle wrapper的配置文件,使用gradle wrapper不需要提前将gradle下载好,Android Studio会自动根据本地的缓存情况决定是否需要联网下载gradle,默认没有启用gradle wrapper的方式,可以点击Android Studio导航进行配置更改。

5…gitignore

.gitignore用来将指定的目录或文件排除在版本控制之外的,版本控制将在第5章中开始正式学习。

6.build.gradle

build.gradle是项目全局的gradle构建脚本,通常在其中的内容不需要修改。第5章中开始正式的学习。

7.gradle.properties

gradle.properties是全局的gradle配置文件,配置的属性将会影响到项目中所有的gradle编译脚本。

8.gradlew和gradlew.bat

gradlew和gradlew.bat是用来在命令行界面中执行gradle命令的,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。

9.HelloWorld.iml

iml文件是所有IntelliJ IDEA项目都会自动生成的一个文件(Android Studio是基于IntelliJ IDEA开发的),用于标识这是一个IntelliJ IDEA项目,我们不需要修改这个文件中的任何内容。

10.local.properties

用于指定本机中的Android SDK路径,通常其中内容内容自动生成。本机的Android SDK位置发生了变化,就将这个文件中的路径改成新的位置即可。

11.settings.gradle

用于指定项目中所有引入的模块。由于HelloWorld项目中就只有一个app模块,因此该文件只引入app一个模块。通常情况下模块的引入都是自动完成的,需要我们手动去修改的场景比较少。

app目录下的内容是我们以后工作重点。详细分析如下:

1.build

和外层的build目录类似,主要也是包含一些在编译时自动生成的文件,不过比外面build的内容更多更杂。

2.libs

如果项目中使用到了第三方jar包,就需要把这些jar包都放在libs目录下,这个目录下的jar包会被自动添加到构建路径里。

3.androidTest

用来编写Android Test测试用例的,可以对项目进行一些自动化测试。

4.java

java目录是放置所有Java代码的地方,展开该目录将看到创建的HelloWorldActivity文件

5.res

项目中使用到的所有图片、布局、字符串等资源都放在这个目录下。其中很多子目录,图片放在drawable目录下,布局在layout目录下,字符串放在values目录下,所以不用担心会把res目录弄乱。

6.AndroidManifest,xml

是整个Android项目的配置文件,程序中定义的所有四大组件都要在AndroidManifest,xml中注册,还能给应用程序添加权限声明,后续详细说明。

7.test

用来编写Unit Test测试用例,是对项目进行自动化测试的另一种方式。

8.gitignore

用于将app模块内的指定的目录或文件排除在版本控制之外,作用和外层的.gitignore文件类似

9.app.iml

IntelliJ IDEA项目自动生成的文件,初学不用关心

10.build.gradle

app模块的gradle构建脚本,build.gradle中会指定很多项目构建相关的配置,稍后会详细分析gradle构建脚本中的具体内容

11.proguard-rules.pro

用于指定项目代码的混淆规则,当代码开发完成后打成安装包文件,如果不希望代码被别人破解通常会将代码进行混淆,从而让破解者难以阅读。

<activity
      android:name=".MainActivity">
      <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
  </activity>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />表示是项目的主活动。点击应用图标首先启动主活动。

活动继承自AppCompatActivity(应用兼容活动)是一种向下兼容的Activity,可以将Activity在各个系统版本中增加的特性和功能最低兼容到Android2.1系统。所有活动都需要继承Activity或Activity的子类才能拥有活动的特性(AppCompatActivity是Activity的子类),

Android程序设计讲究逻辑和视图分离,在布局文件中编写界面然后在活动中引入,在onCreate()第二行调用setContentView()就是这个方法给当前的活动引入了R,layout.xxxlayout布局。

1.3.2详解项目中资源

res中drawable开头的文件用来存放图片,mipmap开头的文件夹用于放应用图标,values开头的文件夹用于存放字符串、样式、颜色等配置,lauyout文件夹用于存放布局文件。drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等文件夹在制作程序时最好能给同一张图片提供几个不同分辨率版本。程序运行时会自动根据运行设备分辨率加载。

<resources>
    <string name="app_name">MaterialTest</string>
</resources>
  • 在代码中通过R.string.app_name获得该字符串引用
  • 在XML中通过@string/app_name获得该字符串引用

如果引用图片资源就替换成drawble,如果引用应用图标就替换成mipmap,如果引用布局文件就替换成layout。

1.3.3详解build.gradle文件

Android采用Gradle构建项目,Gradle基于Groovy领域特定语言(DSL)声明项目设置,摒弃了传统基于XML(如Ant和Maven)的各种烦琐配置。外层目录的build.gradle

为什么使用Log而不使用System.out

System.out.println()日志打印不可控制、打印时间无法确定、不能添加过滤器、日志没有级别区分等等

Android开发者和出色的Android开发者还是有很大的去别的,你还需要付出更多的努力才行。我希望在Android的世界你可以放下身段,以一只萌级小菜鸟的身份起飞,在后面的旅途中你会不断地成长。

第二章 先从看到的入手——探究活动

2.1活动是什么

活动(Acticity)是最容易吸引用户的地方,是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以用包含零个或多个活动,不包含任何活动的应用程序很少见,谁也不想让自己的应用无法被用户看到。

2.2活动的基本用法

勾选Generate Layout File(产生布局文件)表示会自动为FirstActivity创建一个对应的布局文件,勾选LauncherActivity(发射器活动)表示会自动将FirstActivity设置为当前项目的主活动,勾选Backwards Compatibility表示为项目启用向下兼容的模式,需要勾选。

项目中的任何活动都应该重写Activity的onCreate()方法。onCreate()默认实现是调用父类的onCreate()方法。

2.2.1创建和加载布局

Android程序的设计讲究逻辑和视图分离,最好每一个Activity对应一个布局,布局就是用来显示界面内容的,因此我们现在就来手动创建一个布局文件。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1"
        />
</LinearLayout>

添加了一个Button元素,并在Button元素的内部增加了几个属性。

android:id是给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作。@+id/button1去掉加号后@id/button1就是XML中引用资源的语法,只是把string替换成了id,如果是引用就不加+,定义就加+。

android:layout_width制定了当前元素的宽度,这里使用match_parent表示让当前元素和父元素一样宽。

android:layout_height指定当前元素高度,这里使用wrap_content表示当前元素的高度只要刚好能包含里面的内容就行。

android:text指定了元素中显示的文字内容。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.first_layout);

setContentView()中传入布局R.layout…用于给当前的活动加载布局,R.layout.first_layout就会得到first_layout.xml布局的id。可以通过工具栏Preview预览当前布局。

2.2.2在AndroidManifest文件中注册

所有的Activity都要在AndroidManifest.xml中进行注册才能生效,FirstActivity已经在AndroidManifest.xml注册过了。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication" >
        <activity android:name=".FirstActivity" 
						**android:label = "This is FirstActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>**
        </activity>
        <activity android:name=".MainActivity" >
        </activity>
    </application>

</manifest>

Activity的注册声明要放在标签内,这里是通过标签来对活动进行注册的。是Android Studio帮我们完成了注册。

标签中:android:name指定具体注册哪个Activity,这里填入.FirstActivity是com.example.activitytest.FirstActivity的缩写,最外层标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册Activity时包名就可以省略,直接使用.FirstActivity就足够。

不过,因为还没有为程序配置主Activity,我们的程序仍然不能运行,程序运行起来不知道首先启动哪个Activity。配置主Activity的方法就是在标签内部加入标签,并在这个标签里添加和 。我们还可以使用android:label指定活动中标题栏的内容,标题栏是显示在活动最顶部的,给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。需要注意,如果你的应用程序没有声明任何一个Activity作为主Activity,这个程序仍然可以正常安装,只是无法在启动器中看到或者打开这个程序。这种程序一般作为第三方服务供其他应用在内部进行调用的。

2.2.3在活动中使用Toast

Toast时Android系统提供的一种提醒方式,在程序中可以使用它将一些短小的信息通知给用户,会在一段时间后自动消失,并且不会占用任何屏幕空间。首先需要定义一个弹出Toast的触发点

protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.first_layout);
		**Button button1 = (Button) findViewById(R.id.button_1);
		button1.setOnClickListener(new View.OnClickListener() {
		    @Override
		    public void onClick(View v) {
		        Toast.makeText(FirstActivity.this,"You clicked Button 1",
		                Toast.LENGTH_SHORT).show();
		    }
		});**

在Activity中,可以通过findViewById()方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1,来得到按钮的实例,这个值是刚才在first_layout.xml中通过android:id属性指定的。findViewById()方法返回一个View对象,将它向下转型成Button对象,得到按钮的实例,调用setOnClickListener()方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()方法。因此,弹出Toast的功能当然是要在匿名类View.OnClickListener的onClick()方法中编写。

Toast的用法简单,通过静态方法makeText()创建出一个Toast对象,然后调用show()将Toast显示出来就可以了。这里需要注意的是,makeText()方法需要传入3个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动就是一个Context对象,因此这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG.

2.2.4在活动中使用Menu

手机屏幕空间非常有限,因此充分利用屏幕空间在手机界面设计中非常重要。Android提供了一种方式,可以让菜单都能得到展示的同时,还能不占用任何屏幕空间。

<?xml version="1.0" encoding="utf-8"?>
<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>

我们创建了两个菜单项,标签是用来创建具体的某一个菜单项,然后通过android:id给菜单项指定一个唯一的标识符,通过android:title给这个菜单项指定一个名称。

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main,menu);
        return true;
    }
}

getMeunInflater()方法能够得到MenuInflater对象,再调用MenuInflater对象的inflater()方法就可以给当前活动创建菜单了。inflate()方法接收两个参数,第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里传入R.menu.main。第二个参数用于指定菜单项将添加到哪一个Meun对象当中,这里使用onCreateOptionsMenu()方法中传入的menu参数。然后在方法中返回true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单无法显示。

当然,仅仅让菜单显示出来不够,定义菜单要可用才行,因此还要定义菜单响应事件。

@Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.add_item:
                Toast.makeText(this, "You clcker 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;
    }
}

在onOptionsItemSelected()方法中,通过调用item.getItemId()来判断我们点击的是哪一个菜单项,然后给每个菜单项加入自己的逻辑处理,这里我们活学活用,弹出一个刚刚学会的Toast。

可以看到,菜单里的菜单名项默认是不会显示出来的,只有点击一下菜单按钮才会弹出里面具体的内容,因此它不会占用任何活动的空间。

2.2.5销毁一个活动

通过上一节的学习,已掌握了手动创建活动的方法,并学会了在活动中创建Toast菜单。会疑惑,如何销毁一个活动?

只要按一下Back键就可以销毁当前活动了,不过如果希望在程序中通过代码来销毁活动而不是通过按键方式,Activity类提供了一个finish()方法,我们在活动中调用这个方法就可以销毁当前活动了。

public void onClick(View v) {
//                Toast.makeText(FirstActivity.this, "You clicked Button 1",
//                        Toast.LENGTH_SHORT).show();
                finish();
            }
});

这时点一下按钮,当前的活动就成功被销毁了,效果和按下Back键是一样的。

2.3使用Intent在活动之间穿梭

创建多少个活动方法都和上一节中的介绍一样。问题在于,在启动器中点击应用的图标只会进入到该应用的主活动,那么怎样才能由主活动跳转到其他活动呢?

2.3.1使用显式Intent

在ActivityTest项目中创建一个活动。Android Studio为我们自动生成SecondActivity.java和second_layout.xml这两个文件。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
		xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondActivity">
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2"
        />
</LinearLayout>

然后SecondActivity中代码已经自动生成了一部分,默认就好:

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);
    }
}

另外不要忘记,任何一个活动都需要在AndroidManifest.xml中注册,不过Android Studio已经帮我们自动完成了

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">
        <activity android:name=".SecondActivity"></activity>
        <activity android:name=".FirstActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MainActivity"></activity>
    </application>

</manifest>

剩下的问题是如何启动第二个活动,这时需要引入一个新的概念:Intent。

Intent是Android程序中各组件之间进行交互的一种重要方式,可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent可被用于启动活动、启动服务以及发送广播等场景,我们先将目光锁定在启动活动上面。

Intent大致可以分为两种:显示Intent和隐式Intent。

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext,Class<?>cls)。这个构造函数接受两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class指定想要启动的目标活动,这个构造函数就可以构建出Intent的“意图”。Activity类中提供了一个startActivity()方法接受一个Intent参数,专门用于启动活动。这里我们将构建好的Intent传入startActivity()方法就可以启动目标活动。

修改FirstActivity中按钮的点击事件,代码如下所示:

button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                Toast.makeText(FirstActivity.this, "You clicked Button 1",
//                        Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
                startActivity(intent);
            }
        });

我们首先构建出了一个Intent,传入FirstActivity.this作为上下文,传入Second-Activity.class作为目标活动,这样我们的“意图”就非常明显了,即在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity()方法来执行这个Intent。按Back键可以销毁当前活动回到上一个活动。使用这种方式来启动活动,Intent的“意图”非常明显,因此我们称之为显示Intent。

2.3.2使用隐式Intent

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

什么是合适的活动?就是可以响应我们这个隐式Intent的活动,但目前SecondActivity什么样的隐式Intent都响应不了。

通过在标签下配置的内容,可以指定当前活动能够响应的action和category,打开AndroidManifest.fest添加如下代码:

<activity android:name=".SecondActivity">
      <intent-filter>
          <action android:name="com.example.activitytest.ACTION_START" />
          <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
  </activity>

在标签中我们指明当前活动可以响应com.example.activitytest.ACTION_START这个action,标签则包含一些附加信息,更精确指明当前的活动能够响应的Intent中还可能带有category。只有和中的内容同时匹配上Intent中指定的action和category时,这个活动才能响应该Intent。修改FirstActivity中按钮的点击事件,代码如下所示:

button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                Toast.makeText(FirstActivity.this, "You clicked Button 1",
//                        Toast.LENGTH_SHORT).show();
//                Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
                Intent intent = new Intent("com.example.activitytest.ACTION_START");
                //com.example.activitytest.ACTION_START只是一个名字改成什么都可以
                startActivity(intent);
            }
        });

com.example.activitytest.ACTION_START只是一个名字改成什么都可以

对比看看,我们使用了Intent的另一个构造函数,将action的字符串传了进去,表明我们想要启动能够响应com.example.activitytest.ACTION_START这个action的活动。前面说要和同时匹配上才能响应,没看到哪里有指定category啊?这是因为android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法时会自动将这个category添加到Intent中。

成功,说明我们在标签下配置的action和category的内容生效。每个Intent只能指定一个action,却能指定多个category。目前我们的Intent只有一个默认的category,在FirstActivity中按钮的点击事件中再添加一个Category。

button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                Toast.makeText(FirstActivity.this, "You clicked Button 1",
//                        Toast.LENGTH_SHORT).show();
//                Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
                Intent intent = new Intent("com.example.activitytest.ACTION_START");
                //com.example.activitytest.ACTION_START只是一个名字改成什么都可以
                intent.addCategory("com.example.activitytest.MY_CATEGORY");
                startActivity(intent);
            }
        });
     }

可以调用Intent类的addCategory() 方法添加一个category,这里我们指定了一个自定义的category值为com.example.activitytest.MY_CATEGORY。

报错:错误信息提醒我们,没有任何一个活动可以相应我们的Intent,因为SecondActivity的标签中并没有声明可以响应这个category,所以出现了没有任何活动响应该Intent的情况。

Process: com.example.myapplication, PID: 9212
    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>

2.3.3更多隐式Intent的用法

使用隐式Intent不仅可以启动自己程序内的活动,还可以启动其他程序的活动,使得Android多个应用程序之间的功能共享成了可能。比如应用程序需要展示一个网页,调用系统的浏览器就行。

FirstActivity中按钮点击事件修改代码:

button1.setOnClickListener(new View.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是I是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。 然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。重新运行程序,在FirstActivity界面点击按钮就可以看到打开了系统浏览器。

Intent的setData()方法接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常都以字符串形式传入Uri.parse()方法中解析。

与此对应,我们还可以在标签中配置一个标签,用于更精确地指定当前活动能响应什么类型的数据。标签中主要可以配置以下内容。

android:scheme。用于指定数据的协议部分,如上例中的http部分。

android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。

android:port。用于指定数据的端口部分,一般紧随在主机名之后。

android:path。用于指定主机名和端口之后的部分,如一段网址后跟在域名之后的内容。

android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有标签中指定的内容和Intent中携带的Data完全一致当前活动才能够响应该Intent。不过一般在标签中都不会指定过多的内容,如上面浏览器示例中只指定android:scheme为http,就可以响应所有的http协议的Intent。

为了能更直观地理解,我们建立一个能响应打开网页Intent的Activiity。修改ThirdActivity注册信息:

<activity android:name=".ThirdActivity">
    <intent-filter tools:ignore="AppLinkUrlError">
        <action android:name = "android.intent.action.VIEW" />
        <category android:name =  "android.intent.category.DEFAULT" />
        <data android:scheme = "http" />
    </intent-filter>
</activity>

在ThirdActivity的中配置了ThirActivity能响应常量值为Intent.ACTION_VIEW的action,category指定了默认category值,在标签中我们通过android:scheme指定数据的协议必须是http协议,这样ThirdActivity就和浏览器一样能响应一个打开网页的Intent。

虽然我们声明ThirdActivity可以响应打开网页的Intent,但实际上这个活动没有加载并显示网页的功能,在真正的项目中尽量不要出现这种可能误导用户的行为。

除了http协议还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面代码展示了调用系统拨号界面。

button1.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
          Intent intent = new **Intent(Intent.ACTION_DIAL);**
          intent.setData**(Uri.parse("tel:10086"));**
          startActivity(intent);
      }
  });

首先指定Intent的action是Intent.ACTION_DIAL,这也是一个Android系统的内置动作。在data部分指定了协议是tel,号码是10086。

2.3.4向下一个活动传递数据

Intent不止可以启动一个活动,还提供了一系列putExtra()方法的重载可以把数据暂存在Intent中在启动活动时把数据从Intent中取出传递数据。

使用显式Intent传递一个字符串。

button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String data = "Hello SecondActivity";
        Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
//putExtra()方法重载,把要传递的数据暂存在Intent中。第一个参数是键,第二个参数是要传递的数据。
        intent.putExtra("extra_data",data);
        startActivity(intent);
    }
});

public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);
//getIntent()方法获取到启动SecondActivity的Intent,
        Intent intent = getIntent();
//调用启动SecondActivity的Intent的getStringExtra()方法,传入相应的键值获取到相应的数据。
//不同类型换中间的字母即可。getIntExtra()、getBooleanExtra().
        String data = ((Intent) intent).getStringExtra("extra_data");
        Log.d("SecondActivity",data);
    }
}

2.3.5返回数据给上一个活动

按一下Back返回上一个Activity,没有用于启动上一个Activity的Intent来传递数据。Activity中有一个startActivityForResult()方法用于启动活动,用startActivityForResult()启动的活动期望在销毁时返回一个结果给上一个Activity。startActivityForResult()接受两个参数,第一个参数是Intent,第二个是请求码(是个唯一值就行),用于在之后的回调中判断数据的来源。

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

这里使用startActivityForResult()方法来启动SecondActivity,请求码传入1,
接下来是在SecondActivity的点击事件中添加返回数据的逻辑

button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
				//先构建一个没有指定意图的Intent
        Intent intent = new Intent();
				//把要传递的数据存放在Intent中
        intent.putExtra("data_return","Hello FirstActivity");
     //Activity类的setResult()方法是专门用于向上一个Activity返回数据的。
		//setResult()第一个参数用于向上一个活动返回处理结果,一般是RESULT_OK或RESULT_CANCELD
		//setResult()第二个参数把带有数据的Intent传递回去
				setResult(RESULT_OK,intent);
				//调用finish()方法销毁活动。
        finish();
    }
});

先把要传递的数据放在Intent中,然后调用了Activity类的setResult()方法。第一个参数用于向上一个活动返回数据,一般是RESULT_OK或RESULT_CANCLED,第二个参数把带有数据的Intent传递回去。SecondActivity是由startActivityForResult()方法启动的,因此SecondActivity被销毁后会回调上一个活动的onActivityForResult()方法,因此在FirstActivity中写这个方法来得到返回的数据。

//onActivityResult()方法带有三个参数,
//第一个参数requestCode为启动活动时传入的请求码,
//第二个参数resultCode我们返回数据时传入的处理结果,
//第三个参数data为携带返回数据的Intent。
@Override
protected void onActivityResult(int requestCode, int resultCode,Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case 1:
            if (resultCode == RESULT_OK) {
                String returnedData = data.getStringExtra("data_return");
                Log.d("FirstActivity", returnedData);
            }
            break;
        default:
    }
}
//如果不点击按钮直接按下Back键回到FirstActivity,数据就无法返回,
//我们通过在SecondActivity中重写onBackPressed()方法来解决。
public void onBackPressed() {
    Intent intent = new Intent();
    intent.putExtra("data_return","Hello FirstActivity");
    setResult(RESULT_OK,intent);
    finish();
}

2.4活动的生命周期

深入理解活动的生命周期可以写出连贯流畅的程序。并在合理管理应用资源方面游刃有余。

2.4.1返回栈

Android使用任务(task)来管理Activity,一个任务就是一组存放在栈里的Activity的集合,返回栈(Back Stack)先进后出,系统总是会显示处于栈顶的活动给用户。

2.4.2活动状态

每个活动在生命周期中有4种状态。

1.运行状态

位于返回栈栈顶的Activity处于运行状态。如果回收会带来非常差的用户体验。

2.暂停状态

不处于栈顶,但仍然可见的Activity。比如对话框形式的Activity只占用屏幕中间部分区域。暂停状态中的Activity仍完全存活着,系统也不愿意回收这种Activity(因为它仍可见,回收可见的任何东西都会带来不好的用户体验),除非在内存极低情况下才会考虑回收暂停状态的Activity。

3.停止状态

不处于返回栈栈顶,且完全不可见的Activity处于停止状态。系统仍会为停止状态的Activity保存相应状态和成员变量,当其他地方需要内存时,处于停止状态的Activity可能会被回收。

4.销毁状态

从返回栈中移除后的活动处于销毁状态。系统最倾向于回收销毁状态的活动来保证手机内存充足。

2.4.3活动的生存期

Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节。

onCreate()。每个活动都需要重写onCreate()方法,会在活动第一次被创建时调用,在onCreate()中完成活动初始化操作如加载布局、绑定事件。

onStart()。在Activity由不可见变可见时调用。

onResume ()。在Activity准备好和用户交互时调用。此时Activity位于栈顶并处于运行状态。

onPause()。在系统准备启动或恢复另一个活动时调用。通常在onPause()中把消耗CPU的资源释放掉,保留关键数据,这个方法要快才能不影响栈顶Activity的使用。

onStop()。在活动完全不可见时调用,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,onStop()不会执行。

onDestory()。在Activity被销毁之前调用,之后的Activity变为销毁状态。

onRestart()。在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。

除了onRestart()都两两相对,从而又可以将活动分为3种生存期。

可以将Activity分为以下3种生存期。

  • 完整生存期。Activity在onCreate()和onDestory()之间经历的。一般Activity在onCreate()方法中完成各种初始化操作,onDestroy()方法中完成释放内存操作。
  • 可见生存期。onStart()和onStop()之间经历。可见生存期内Activity对用户总是可见的,即便有可能无法和用户交互,我们可以在onStart()中对资源加载,在onStop()中对资源释放。保证处于停止状态Activity不会过多占用内存。
  • 前台内存期。onResume()和onPause()之间经历。前台生存期内Activity总是处于运行状态,此时Activity可以和用户进行交互。

2.4.4体验活动生命周期

新建ActivityLifeCycleTest项目,建NormalActivity和DialogActivity,在AndroidManifest.xml的DialogActivity中设置android:theme="@style/Theme.AppCompat.Dialog",让DialogActivity使用对话框式主题。

编辑activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

		用于启动NormalActivity
    <Button
        android:id="@+id/start_normal_activity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start NormalActivity"
        />
用于启动DialogActivity    
<Button
        android:id="@+id/start_dialog_activity"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start DialogActivity"
        />
    
</LinearLayout>

在LinearLayout中加入两个按钮,一个用于启动NormalActivity,一个用于启动DialogActivity。

当MainActivity第一次被创建会依次执行onCreate()、onStart()和onResume()方法、点击第一个按钮启动NormalActivity,由于NormalActivity把MainActivity完全遮挡住,因此MainActivity的onPause()和onStop()都会得到执行,按下Back键返回MainActivity。由于之前MainActivity进入停止状态,所以执行onRestart()、onStart()和onResume()方法。而onCreate()方法不会执行,因为此时看到的MainActivity是之前添加到Back Stack中的,没有重新创建。点击第二个按钮启动DialogActivity,因为DialogActivity没有完全遮挡住MainActivity,所以MainActivity执行onPause()方法进入了暂停状态而不是停止状态,按下Back键返回MainActivity只有onResume()方法会执行,再按下Bakc键退出程序打印onPause、onStop、onDestroy最终销毁MainActivity。

2.4.5活动被回收了怎么办

在活动A基础上启动了活动B,A进入了停止状态,由于系统内存不足将A回收,按下Back返回活动A,这时A不会执行onRestart()方法,而是执行A的onCreate()方法,重新创建Activity A。A的临时数据和状态全没了。

Activity提供onSaveInstanceState()(保存实例状态)回调方法,保证在Activity被回收之前一定会调用。onSaveInstanceState()携带一个Bundle(包)参数,Bundle提供一系列方法保存数据,putString()保存字符串,putInt()保存整形数据。每个保存方法传入两个参数,第一个是键用于从Bundle中取值,第二个参数是真正要保存的内容

//MainActivity中添加代码将临时数据保存
//Persistable可读写 Persistent执着的
  @Override
  public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
      super.onSaveInstanceState(outState);
      String tempData = "Something you just typed";
      outState.putString("data_key",tempData);
  }

恢复数据在onCreate()方法中,onCreate()有一个Bundle类型的参数,一般情况下为null,如果活动被系统回收之前有通过onSaveInstanceState()方法保存数据,onCreate()的Bundle参数就会带有之前所保存的全部数据,取出即可。修改ManActivity中onCreate()方法

@Override
protected void onCreate(Bundle savedInstanceState){
	super.onCreate(savedInstanceState);
	Log.d(TAG,"onCreate");
	setContentView(R.layout.activity_main);
	if(savedInstanceState != null){
      String tempData = savedInstanceState.getString("data_key");
      Log.d(TAG,tempData);//打印之前保存在Bundle中的数据
  }
	...
}

用Intent传递数据时用的类似方法,Intent还可以结合Bundle一起用于传递数据,先把需要传递的数据存在Bundle对象中,再将Bundle对象存在Intent里,到了目标Activity先从Intent中取出Bundle,再从Bundle中取出数据。当手机屏幕发生旋转Activity也会重新创建,Activity中的数据也会丢失,也可以通过onSaveInstanceState()方法解决。

2.5Activity的启动模式

我们应该根据特定的需求为每个Activity指定恰当的启动模式。启动模式一共4种,分别是standard、singleTop、singleTask(单一任务)和singleInstance(单实例)。AndroidManifest.xml中给标签指定android:launchMode属性来选择启动模式。

2.5.1standard

Activity默认的启动模式,不进行显式指定的情况下,所有活动都会自动使用这种启动模式。之前写过所有活动都为standard模式。standard模式下系统每启动一个Activity都会在返回栈中入栈并处于栈顶、系统不会在乎standard模式启动的活动是否已在返回栈中存在,每次启动都会创建该Activity的新实例。

重写onCreate()
Log.d("FirstActivity",this,toString);
button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //standard
        **Intent intent = new Intent(FirstActivity.this, FirstActivity.class);
        startActivity(intent);**
    }
});

打印信息:com.example.activitytest.activitytest.FirstActivity@71b88c5
com.example.activitytest.activitytest.FirstActivity@37b39291
com.example.activitytest.activitytest.FirstActivity@2030989

返回栈中创建了3个FirstActivity实例,每点一次就会创建出一个新的FirstActivity实例。

2.5.2singleTop

在启动Activity时如果发现返回栈的栈顶已经是该Activity,则直接使用它,不会创建新Activity实例。

<activity
      android:name=".FirstActivity"
      **android:launchMode="singleTop"**
      android:label="This is FirstActivity">
      <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
  </activity>

但当FirstActivity并未处于栈顶位置时,再启动FirstActivity,会创建新的实例。

2.5.3singleTask

整个应用程序上下文只存在一个实例,每次启动该Activity时系统会首先在返回栈中检查是否存在该Activity实例,如果发现已存在则直接使用该Activity,并把这个Activity之上的所有Activity统统出栈。

FirstActivity中
<activity
    **android:name=".FirstActivity"**
    android:launchMode="singleTask"
    android:label="This is FirstActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
@Override
protected void onRestart() {
    super.onRestart();
    Log.d("FirstActivity","onRestart");
}

SecondActivity中
protected void onDestroy() {
    super.onDestroy();
    Log.d("SecondActivity","onDestory");
}

在SecondActivity中启动FirstActivity时,发现返回栈中已存在一个FirstActivity实例,并且在
SecondActivity下面,于是SecondActivity执行onRestart()和onDestroy()后出栈,栈中只剩下了FirstActivity。

2.5.4singleInstance

singleInstance模式启动的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈),这样做的意义是如果我们想实现其他程序和我们程序共享一个Activity的实例,因为每个应用都有自己的返回栈,同一个Activity在不同返回栈入栈时必然创建了新的实例。而使用singleInstance模式可以解决,会有一个单独的返回栈来管理这个活动,不管哪个应用程序访问这个活动,都共用同一个返回栈,就解决了共享Activity实例的问题。

<activity
    android:name=".SecondActivity"
    **android:launchMode="singleInstance"**>
    <intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
				<category android:name="com.example.activittest.MY_CATEGORY" />
    </intent-filter>
</activity>
在FirstActivity、SecondActivity和ThirdActivity的onCreate()方法中用getTaskId()分别打印返回栈id,

从FirstActivity进入SecondActivity,从SecondActivity进入ThirdActivity

打印信息:
FirstActivity:Task id is 171
SecondActivity:Task id is 172
ThirdActivity:Task id is 171

我们发现SecondActivity的Task id 与1和3的不同,按下Back键竟然从ThirdActivity直接返回到FirstActivity,按下Back再按返回到SecondActivity。因为ThirdActivity在栈顶时按下Back,ThirdActivity从返回栈中出栈,FirstActivity成了栈顶活动,因此ThirdActivity直接返回到FirstActivity,FirstActivity按下Back键当前返回栈为空,于是显示另一个返回栈栈顶SecondActivity。

2.6活动的最佳实践

2.6.1知晓当前是在哪一个活动

新建BaseActivity类,普通Java类即可因为不需要让BaseActivity在AndroidManifest.xml中注册,BaseActivity继承自AppCompatActivity,让First、Second、ThirdActivity都extends BaseActivity,项目中所有Activity的现有功能并不受影响,因为仍完全继承了Activity所有特性

重写onCreate()public class BaseActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        Log.d("BaseActivity",getClass().getSimpleName());/打印当前实例的类名
    }
}

现在进入活动界面,活动类名就会被打印出来,就可以知晓当前界面对应的活动。

2.6.2随时随地退出程序

用一个专门的集合类对所有的活动进行管理,在ThirdActivity中就不用连按3次Back键才能退出了。

//新建ActivityCollector类作为活动管理器
public class ActivityCollector {
		//通过一个List暂存活动
    public static List<Activity> activities = new ArrayList<>();
    //提供addActivity()方法用于向List中添加活动
    public static void addActivity(Activity activity){
        activities.add(activity);
    }
    //提供removeActivity()方法用于从List中移除活动
    public static void removeActivity(Activity activity){
        activities.remove(activity);
    }
    //提供finishAll()方法用于将List中存储的活动全部销毁
    public static void finishAll(){
        for(Activity activity : activities){
            if(!activity.isFinishing()){
                activity.finish();
            }
        }
        activities.clear();
    }
}

public class BaseActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        Log.d("BaseActivity",getClass().getSimpleName());//获取了当前实例的类名
**//在BaseActivity的onCreate()方法中调用ActivityCollector.addActivity
//表明将当前正在创建的活动添加到任务管理器里**
        **ActivityCollector.addActivity(this);**
    }

    **@Override
    protected void onDestroy() {
        super.onDestroy();
//同理表明将一个马上要销毁的活动从任务管理器里移除
        ActivityCollector.removeActivity(this);
    }**
}

public class ThirdActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("ThirdActivity","Task id is "+ getTaskId());
        setContentView(R.layout.third_layout);
        **Button button3 = (Button) findViewById(R.id.button_3);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ActivityCollector.finishAll();//从此不管想在什么地方退出程序,只需要调用ActivityCollector.finishAll();方法就可以了
            }
        });**
    }
}

还可以在销毁所有活动的代码后面加上杀掉当前进程保证程序完全退出
android.os.Process.killProcess(android.os.Process.myPid());killProcess()方法用于杀掉一个进程,接收一个进程id参数,myPid()方法来获得当前进程的进程id。killProcess()只能用于杀掉当前程序的进程,不能用这个方法去杀掉其他程序。

2.6.3启动活动的最佳写法

启动活动的方式是,首先Intent构建出当前的”意图“,然后调用startActivity()或startActivityForResult()方法将活动启动起来,如果有数据需要从这个活动传递到另一个活动,也可以借助Intent完成。

在SecnodActivity中添加actionStart()方法,在方法中完成Intent的构建,SecondActivity中需要的数据都是通过actionStart(Context context,String data1,String data2)方法的参数传过来的,把这些数据存储在Intent中,最后调用startActivity()方法启动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);
}

可以非常清晰知道启动SecondActivity需要传递哪些数据,简化了启动活动的代码

button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        **SecondActivity.actionStart(FirstActivity.this,"data1","data2"**}
});

3.软件也要拼脸蛋——UI开发的点点滴滴

3.1如何编写程序界面

可视化编辑工具制作出的界面通常不具有很好的屏幕适配性,不利于真正了解界面实现原理

3.2常用控件的使用方法

新建UIWidgetTest项目

3.2.1TextView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"//给了当前控件唯一标识符
        android:layout_width="match_parent"//官方更推荐match_parent和fill_parent意义相同
        android:layout_height="wrap_content"//刚好包住里面内容,由内容决定控件大小(TextView)
        android:gravity="center"//指定文字对齐方式,可以用"|"同时指定多个值
																//center效果等同于center_vertical|center_horizontal
				android:gravity="center"//指定文字对齐方式,可以用"|"同时指定多个值
        android:textSize="24sp"//字体大小、Android字体大小用sp作为单位
        android:textColor="#00ff00"//字体颜色
				android:text="This is TextView" />
</LinearLayout>

3.2.2Button

<Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button"
//系统会对Button中所有英文字母自动进行大写转换,不想要这样的效果可使用如下配置禁用这一默认特性
        **android:textAllCaps="false"**/>

用匿名类的方式注册监听器,每当点击按钮就执行监听器中的onClick()方法。
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    **Button button = (Button) findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //添加逻辑
        }
    });**
}
还可以使用接口方式来注册监听器。
public class MainActivity extends AppCompatActivity **implements View.OnClickListener**{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button 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.3EditText

允许用户在EditText里输入和编辑内容,并可在程序中对这些内容进行处理。比如发短信,聊QQ。Android控件用法都很相似,定义一个id,指定控件的宽度和高度,再适当加入一些控件特有属性就差不多。所以用XML编写界面一点都不难,完全可以不借助任何可视化工具。
比较人性化的软件在输入框显示提示性文字,一旦用户输入了任何内容,提示性文字就会消失。

<EditText
	  android:id="@+id/edit_text"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  **android:hint="Type something here"
用android:maxLines解决输入内容过多界面变得难看的问题。
指定最大行数为2,这样输入内容超过时会自动滚动,EditText不会再继续拉伸。**
		**android:maxLines="2"**
	  />

EditText和Button结合通过点击按钮来获取EditText中输入的内容。

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    **private EditText editText;**

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
//通过findViewById()方法得到EditText的实例
        **editText = (EditText) findViewById(R.id.edit_text);**
        button.setOnClickListener(this);
    }

    //接口方式注册监听器
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.button:
                //在此处添加逻辑
	//在按钮的点击事件里调用EditText的getText()方法获取到输入的内容,再调用toString方法转换成字符串
                **String inputText = editText.getText().toString();
								//**使用Toast将输入的内容显示出来。
                **Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();**
                break;
            default:
                break;
        }
    }
}

3.2.4ImageView

用于在界面上展示图片的一个控件。图片通常放在以"drawable“开头的目录下

activity_main.xml中添加
<ImageView
        android:id="@+id/image_view"
//长宽设定为wrap_content不管图片尺寸多大都能完整展示出来。
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
//android:src属性给ImageView制定了一张图片
        android:src="@drawable/IMG_20201210_202040"
        />
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;

    **private ImageView imageView;**

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        editText = (EditText) findViewById(R.id.edit_text);
        **imageView = (ImageView)findViewById(R.id.image_view);**
        button.setOnClickListener(this);
    }

    //接口方式注册监听器
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.button:
//通过调用ImageView的setImageResource()方法将显示的图片改为img_2
                **imageView.setImageResource(R.drawable.img_2);**
                break;
            default:
                break;
        }
    }
}

3.2.5ProgressBar

ProgressBar用于在界面上显示一个进度条,表示正在加载一些数据

<ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

用所有Android控件都具有Android控件的可见属性,通过android:visibility进行指定,可选值有3种:visible、invisible和gone。
visable表示控件是可见的,为默认值,不指定android:visbility时,控件都是可见的。
invisable表示控件不可见,但它仍然占据这原来的位置和大小,可以理解为变透明状态。
gone表示不可见而且不再占用任何屏幕空间。
通过代码来设置控件的可见性,使用的是setVisibility()方法,可以传入View.VISIBLE、View.INVISIBLE和View.GONE。
点一下按钮让进度条消失,再点一下让进度条出现。

public void onClick(View v) {
        switch (v.getId()){
            case R.id.button:
								通过getVisibility()方法判断progressBar是否可见,不可见就显示
                if(progressBar.getVisibility() == View.GONE){
                    progressBar.setVisibility(View.VISIBLE);
                }else {
                    progressBar.setVisibility(View.GONE);//可见就隐藏。
                }
                //水平进度条
//每点击一次按钮,就获取进度条当前进度,然后在现有的进度上加10作为更新后的进度
//                int progress = progressBar.getProgress();
//                progress = progress + 10;
//                progressBar.setProgress(progress);
                break;

            default:
                break;
        }
    }
}
<ProgressBar
    android:id="@+id/progress_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
//刚刚是圆形进度条,通过style熟悉将它指定为水平进度条
    style="?android:attr/progressBarStyleHorizontal"
    android:max="100"
    />

3.2.6AlertDialog(警告对话框)

可以在当前的界面弹出一个置顶于所有界面元素之上的对话框,屏蔽掉其他控件的交互能力。一般用于提示非常重要的内容或者警告信息。比如防止用户误删重要内容,删除前弹出一个确认对话框。

public void onClick(View v) {
    switch (v.getId()){
        case R.id.button:
//首先通过AlertDialog.Builder创建一个AlertDialog实例
            **AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
				    //设置标题
		        dialog.setTitle("This is Dialog");
            //设置内容
						dialog.setMessage("Something important");
            //不能通过Back键取消,数据加载完成后通过ProgressDialog的dismiss()方法关闭对话框
						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.7ProgressDialog

会弹出一个对话框,框中显示进度条,屏蔽掉其他控件的交互能力。一般用于表示当前操作比较耗时,让用户耐心地等待。

public void onClick(View v) {
        switch (v.getId()){
            case R.id.button:
                //ProgressDialog
                ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
                progressDialog.setTitle("This is ProgressDialog");
                progressDialog.setMessage("Loading...");
                progressDialog.setCancelable(true);
                progressDialog.show();
                break;
            default:
                break;
        }

3.3详解4种布局

布局是一种可用于放置很多控件的容器,可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。

3.3.1线性布局

LinearLayout将包含的控件线性方向依次排列。
android:orientation属性指定vertical是垂直排列。horizontal是水平排列。

		<Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1"/>
    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button2"/>
    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button3"/

android:gravity用于指定文字在控件中对齐方式,而android:layout_gravity用于指定控件在布局中的对齐方式。需要注意,当horizontal只有垂直方向上的对齐才会生效,水平方向上长度不固定,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。
vertical时,只有水平方向上的对齐方式才会生效。

android:layout_weight允许我们使用比例的方式来指定控件的大小

android:layout_width=“0dp”

android:layout_width=“0dp”

由于使用了android:layout_weight="1"此时控件的宽度不再由android:layout_width来决定,这里指定成0dp是比较规范的写法。dp是Android中用于指定控件大小、间距等属性的单位。
EditText和Button都将android:layout_weight=“1”,表示EditText和Button将在水平方向平分宽度。

系统根据控件layout_weight值占总和比例分配控件所占屏幕比例。

这里我们仅指定了EditText的android:layout_weight属性,并将Button的宽度改回wrap_content。Button仍然按照wrap_content来计算,而EditText则会占满屏幕所有剩余空间。不仅在各种屏幕的适配方面会非常好,而且看起来更加舒服。

3.3.2相对布局

RelativeLayout可以通过相对定位的方式让控件出现在布局的任何位置。其中的属性都有规律可循。

<EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:maxLines="2"
        />
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"**
        android:text="Send"
        />
    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"**
        android:text="Send"
        />
    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_centerInParent="true"**
        android:text="Send"
        />
    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"**
        android:text="Send"
        />
    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"**
        android:text="Send"
        />

上面的例子中每个控件都是相对于父布局进行定位,下面的例子控件相对于控件进行定位。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="horizontal">

    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:maxLines="2"
        />
    <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="button1"
        />
    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/button3"
        android:layout_toRightOf="@id/button3"
        android:text="button2"
        />
    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="button3"
        />
    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button3"
        android:layout_toLeftOf="@id/button3"
        android:text="button4"
        />
    <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="Sbutton5"
        />

</RelativeLayout>

需要为这个属性指定相对控件id的引用。

android:layout_alignLeft表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐android:layout_alignTop和android:layout_alignBottom同理。

3.3.3帧布局

FrameLayout,没有方便的定位方式,所有控件都会默认摆放在布局的左上角。

可以通过android_gravity="left"或"right"来指定控件在布局中左上角还是右上角。

3.3.4约束布局

ConstraintLayout可以按照比例约束控件位置和尺寸

    <TextView
        android:id="@+id/TextView1"
        ...
        android:text="TextView1" />

    <TextView
        android:id="@+id/TextView2"
        ...
        app:layout_constraintLeft_toRightOf="@+id/TextView1" />

    <TextView
        android:id="@+id/TextView3"
        ...
        app:layout_constraintTop_toBottomOf="@+id/TextView1" />

常用属性:

layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf

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

在这里插入图片描述

所用的所有控件都是直接或间接继承自View的,所用所有布局都直接或间接继承ViewGroup。View能在屏幕上绘制一块矩形区域,我们使用各种控件就是在View基础上又添加了各自特有的功能。ViewGroup是一种特殊的View,可以包含很多子View和ViewGroup,是一个用于放置控件和布局的容器。

3.4.1引入布局

创建一个自定义的标题栏,使用引入布局的方式就不需要在每一个活动的布局中写一遍同样的代码。
新建title.x

<?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="wrap_content"
//android:background用于为布局或控件指定一个背景,可以使用颜色或图片来进行填充
    android:background="@mipmap/title_bg">

    <Button
        android:id="@+id/title_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@mipmap/back_bg"
        android:text="Back"
        android:textColor="#fff" />
中间的TextView可以显示一段标题文本。
android:layout_margin指定控件在上下左右方向上偏移的距离,
也可以使用android:layout_marginLeft或android:layout_marginTop等属性来单独指定控件在某个方向上偏移的距离。
    <TextView
        android:id="@+id/title_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:gravity="center"
        android:text="Title Text"
        android:textColor="#fff"
        android:textSize="24sp" />
    <Button
        android:id="@+id/title_edit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@mipmap/edit_bg"
        android:text="Edit"
        android:textColor="#fff"/>
</LinearLayout>

编写完标题栏布局,在程序中使用标题栏修改activity_main.xml

<?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">
//只需要一行include语句将标题栏布局引入进来。
    **<include layout ="@layout/title"/>**
</LinearLayout>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //将自带标题栏隐藏
//getSupportActionBar()
        **ActionBar actionbar = getSupportActionBar();
        if (actionbar != null){
//**ActionBar的hide()方法将标题栏隐藏起来
            **actionbar.hide();
        }**
    }
}

3.4.2创建自定义控件

每个标题栏的返回按钮都是销毁当前活动。给每一个活动都重新注册返回按钮的点击事件会增加很多代码量,所以用自定义控件的方式来解决。


//新建TitleLayout继承自LinearLayout
public class TitleLayout extends LinearLayout {
//重写LinearLayout中带有两个参数的构造函数,在布局中引入TitleLayout控件会调用这个构造函数
    public TitleLayout(Context context, AttributeSet attrs){
        super(context, attrs);
//借助LayoutInflater对象对标题栏布局进行动态加载,LayoutInflater的from方法可以构建出一个LayoutInflater对象
//调用inflate()方法就可以动态加载一个布局文件,
//inflate()方法接收两个参数,第一个是要加载的布局文件的id,这里传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里指定为TitleLayout,传入this。
        LayoutInflater.from(context).inflate(R.layout.title,this);
        //构建出一个LayoutInflater对象,然后调用inflate()方法动态加载一个布局文件,inflate()方法接受两个参数,
        // 第一个参数是要加载的布局文件的id,第二个参数是给记载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。
    }
}

在布局文件中添加这个自定义控件。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
添加自定义控件需要指明控件完整类名,不可省略包名
    <com.example.uicustomviews.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

//新建TitleLayout继承自LinearLayout
public class TitleLayout extends LinearLayout {
//重写LinearLayout中带有两个参数的构造函数,在布局中引入TitleLayout控件会调用这个构造函数
    public TitleLayout(Context context, AttributeSet attrs){
        super(context, attrs);
//借助LayoutInflater对象对标题栏布局进行动态加载,LayoutInflater的from方法可以构建出一个LayoutInflater对象
//调用inflate()方法就可以动态加载一个布局文件,
//inflate()方法接收两个参数,第一个是要加载的布局文件的id,这里传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里指定为TitleLayout,传入this。
        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();//弹出一段文本。
            }
        });**
    }
}

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

ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕,有大量数据需要展示时,就需要借助ListView。

3.5.1ListView的简单用法

新建ListViewTest项目

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

//在布局中加入ListView控件
    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

public class MainActivity extends AppCompatActivity {

**//ListView是用于展示大量数据的,先将需要ListView处理的数据提供好
    private String[] data = { "Apple","Banana","Orange","watermelon" ,
            "Pear","Grape","Pineapple" , "Strawberry" , "cherry","Mango",
        "Apple","Banana", "Orange", "watermelon","Pear", "Grape","Pineapple" , "Strawberry" , "cherry", "Mango"};**

		@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

  **//数据无法直接传递给ListView,需要借助适配器来完成
  //Adapter构造函数需要三个参数,上下文,ListView子项layout的id,以及要适配的数据
        //list_item_1作为ListView子项布局id,是一个Android内置的布局文件,里面只有一个TextView
        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);//将构建好的适配器对象传递进去**
    }
}

3.5.2定制ListView的界面

public class Fruit {
		//name和imageId字段
    private String name;
    private int imageId;

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;//水果对应图片资源id
    }

    public String getName() {
        return name;
    }

    public int getImageId() {
        return imageId;
    }
}

自定义适配器继承自ArrayAdapter,泛型指定为Fruit类
public class FruitAdapter extends ArrayAdapter<Fruit> {
//ListView子项布局id
    private int resourceId;

**///重写构造函数
    public FruitAdapter (Context context, int textViewResourceId, List<Fruit> objects){
        //上下文,ListView子项布局的id和数据都传递进来
        super(context,textViewResourceId,objects);
        resourceId = textViewResourceId;
    }**

    @NonNull
    @Override
    //getView()在每个子项被滚动到屏幕内时会被调用
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);//获取当前项Fruit实例
        //inflate接收三个参数上下文,给加载好的布局再添加一个父布局,
		//false表示只让父布局中声明的layout属性生效,但不会为这个View添加父布局,
		//因为一旦View有了父布局,就不能添加到ListView中。
				//以后理解Viewg更深刻就好了
        View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
        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;
    }
}

public class MainActivity extends AppCompatActivity {

    **private List<Fruit> fruitList = new ArrayList<>();**

@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(){
        for(int i = 0;i<2;i++){//只添加一遍数据量不足以充满整个屏幕
            Fruit apple = new Fruit("Apple",R.drawable.apple_pic);//构造函数中传入水果名字和对应id就好
            fruitList.add(apple);
            Fruit banana = new Fruit( "Banana",R.drawable.banana_pic);
            fruitList.add ( banana) ;
            Fruit orange = new Fruit("Orange",R.drawable.orange_pic);
            fruitList.add(orange) ;
            Fruit watermelon = new Fruit( "watermelon",R.drawable.watermelon_pic);
            fruitList.add(watermelon) ;
            Fruit pear = new Fruit("Pear" ,R.drawable.pear_pic);
            fruitList.add(pear) ;
            Fruit grape = new Fruit("Grape",R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit pineapple = new Fruit("Pineapple",R.drawable.pineapple_pic);
            fruitList.add (pineapple) ;
            Fruit strawberry = new Fruit("strawberry",R.drawable.strawberry_pic);
            fruitList.add(strawberry) ;
            Fruit cherry = new Fruit("cherry",R.drawable.cherry_pic);
            fruitList.add( cherry) ;
            Fruit mango = new Fruit ( "Mango",R.drawable.mango_pic);
            fruitList.add(mango) ;

        }

只要修改fruit_item.xml中的内容,就可以定制出各种复杂界面。

3.5.3提升ListView运行效率

ListView有很多细节可以优化,FruitAdapter的getView()方法每次都将布局重新加载了一遍,当LastView滚动时,会成为性能的瓶颈。getView()方法中还有一个convertView参数用于将之前加载好的布局进行缓存,以便之后重用。

public class FruitAdapter extends ArrayAdapter {
    private int resourceId;

    public FruitAdapter (Context context, int textViewResourceId, List<Fruit> objects){
        //重写构造函数,上下文,ListView子项布局的id和数据都传递进来
        super(context,textViewResourceId,objects);
        resourceId = textViewResourceId;
    }

    @NonNull
    @Override
    //在每个子项被滚动到屏幕内时会被调用
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);//获取当前项Fruit实例
        //inflate接收三个参数false,上下文,,表示只让父布局中声明的layout属性生效。
        //View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
        **View view;
        if(convertView == null){
//如果convertView为null,就使用LayoutInflater去加载布局
            view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);  
        } else {
//convertView不为null直接对convertView重用
            view = convertView;             
        }**
        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;
    }
}

简单优化后,已经不会重复去加载布局,但每次在getView()方法中还是会调用View的findViewById()方法去获取控件的实例。可借助一个ViewHolder对这部分性能进行优化。

修改FruitAdapter中的代码
public class FruitAdapter extends ArrayAdapter {
    ...
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);//获取当前项Fruit实例
        View view;
        **ViewHolder viewHolder;**
        if(convertView == null){
**//当convertView为null时创建一个viewHolder对象
            view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
            viewHolder = new ViewHolder();
//将控件的实例都存放在ViewHolder里
            viewHolder.fruitImage = (ImageView)view.findViewById(R.id.fruit_image);         
            viewHolder.fruitName = (TextView)view.findViewById(R.id.fruit_name);     
//然后调用View的setTag()方法,将ViewHolder对象存储在View中
            view.setTag(viewHolder);**        
        } else {
            view = convertView;    
**//当convertView不为null时调用View的getTag()方法,把ViewHolder重新取出
            viewHolder = (ViewHolder)view.getTag();**                          
        }
				**viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());**
				return view;
    }

/*新增**ViewHolder内部类对控件的实例进行缓存。***/
    **class ViewHolder{
        ImageView fruitImage;
        TextView fruitName;
    }**
}

这样所有控件的实例缓存在ViewHolder里,就没必要每次都通过findViewById()方法来获取控件实例了。通过这两步优化,ListView的运行效率已经非常不错了。

3.5.4LastView的点击事件

ListView滚动只满足了视觉上的效果,如果不能点击的话ListView就没有什么实际的用途了。

修改MainActivity
		@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);
//我们使用setOnItemClickListener()方法为ListView注册了监听器
        **listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
//**用户点击ListView中的任何一个子项都会回调onItemClick()方法
            **public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                //**通过position参数判断点击的哪个子项**获取相应水果
								Fruit fruit = fruitList.get(position);**
								//通过Toast打印水果名字
                **Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });**
    }

3.6更强大的滚动控件——RecyclerView

如果不使用一些技巧来提升ListView的运行效率,那么ListView的性能会比较差,ListView的扩展性也需要更好,ListView只能实现数据纵向滚动的效果,不能实现横向滚动。

为此Android提供了更强大的滚动控件RecyclerView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。

3.6.1RecyclerView的基本用法

为RecyclerView准备一个适配器
//新建FruitAdapter类继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder,
//ViewHolder是我们在FruitAdapter中定义的一个内部类
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    **private List<Fruit> mFruitList;**

**//我们首先定义了一个内部类ViewHolder继承自RecyclerView.ViewHolder
    public static class ViewHolder extends RecyclerView.ViewHolder{/
        private ImageView fruitImage;
        private TextView fruitName;

//ViewHolder构造函数要传入一个View参数,通常是RecyclerView子项的最外层布局
        public ViewHolder(View view){
            super(view);
//我们就可以通过findViewById()方法来获取RecyclerView子项的最外层布局中ImageView和TextView的实例了
            fruitImage = (ImageView) view.findViewById(R.id.fruit_image); 
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }

        public ImageView getFruitImage() {
            return fruitImage;
        }

        public TextView getFruitName() {
            return fruitName;
        }
    }**

**//FruitAdapter也有一个构造函数,
//这个方法用于把要展示的数据源传进来,并赋值给一个全局变量mFruitList,后续操作都在数据源上进行
    public FruitAdapter(List<Fruit> fruitList)
    {
        mFruitList = fruitList;
    }

//FruitAdapter继承自RecyclerView.Adapter必重写**onCreate、onBindViewHolder()和getItemCount()。

**//onCreateViewHolder()方法用于创建ViewHolder实例
    @Override
    public ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) {
        //将fruit_item布局加载进来
        View view = LayoutInflater.from(parent.getContext()).
												inflate(R.layout.fruit_item,parent,false);
//新建ViewHodler实例,将加载出来的布局传入到ViewHolde构造函数中
				ViewHolder holder = new ViewHolder(view);
//最后将ViewHolder的实例返回
        return holder;
    }**

**//onBindViewHolder()方法用于对RecyclerView子项数据进行赋值,在每个子项被滚动到屏幕内时执行
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        //通过postion参数得到当前项的Fruit实例
        Fruit fruit = mFruitList.get(position);
				//将数据设置到ViewHolder的ImageView和TextView中。
        holder.fruitName.setText(fruit.getName());
        holder.fruitImage.setImageResource(fruit.getImageId());
				/*holder.getFruitName().setText(mFruitList.get(position).getName());
				//get返回的是个int或string不能再set*/
    }**

**//getItemCount()就是告诉RecyclerView有多少子项
    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}**

public class MainActivity extends AppCompatActivity {

    **private List<Fruit> fruitList = new ArrayList<>();**
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化水果数据
        **initFruits();

        //在onCreate()中~~先~~获取到RecyclerView实例
        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
        //LayoutManager用于指定RecyclerView的布局方式
		//LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
				//指定了RecyclerView的布局方式
        recyclerView.setLayoutManager(layoutManager);
        //将水果数据传入到FruitAdapter的构造函数中
        FruitAdapter adapter = new FruitAdapter(fruitList);
        //最后调用RecyclerView的setAdapter()方法来完成适配器设置
				//建立数据和RecyclerView之间的关联
        recyclerView.setAdapter(adapter);
    }**

    private  void initFruits(){
        for(int i = 0 ;i<2 ;i++){
            Fruit apple = new Fruit("Apple",R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit( "Banana",R.drawable.banana_pic);
            fruitList.add ( banana) ;
            Fruit orange = new Fruit("Orange",R.drawable.orange_pic);
            fruitList.add(orange) ;
            Fruit watermelon = new Fruit( "watermelon",R.drawable.watermelon_pic);
            fruitList.add(watermelon) ;
            Fruit pear = new Fruit("Pear" ,R.drawable.pear_pic);
            fruitList.add(pear) ;
            Fruit grape = new Fruit("Grape",R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit pineapple = new Fruit("Pineapple",R.drawable.pineapple_pic);
            fruitList.add (pineapple) ;
            Fruit strawberry = new Fruit("strawberry",R.drawable.strawberry_pic);
            fruitList.add(strawberry) ;
            Fruit cherry = new Fruit("cherry",R.drawable.cherry_pic);
            fruitList.add( cherry) ;
            Fruit mango = new Fruit ( "Mango",R.drawable.mango_pic);
            fruitList.add(mango) ;
        }
    }
}

使用RecyclerView实现和ListView一模一样的布局,但逻辑明显清晰了很多。

3.6.2实现横向滚动和瀑布流布局

ListView扩展性不好,只能实现纵向滚动,因此只能使用RecyclerView实现横向滚动,首先要把fruit_item布局里的元素改成垂直排列。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
**<!--vertical是垂直排列。horizontal是水平排列。-->
    android:orientation="vertical"
<!--将LinearLayout宽度由100dp改成match_parent
因为瀑布流宽度应该根据布局列数自动适配,而不是固定值->
    android:layout_width="match_parent"**
    android:layout_height="match_parent"
		**android:layout_margin="5dp"**>

**<!--我们将ImageView和TextView设置成了布局中水平居中,
并使用layout_marginTop属性让文字和图片之间保持一些距离-->**
    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        ****android:layout_gravity="center_horizontal"/>
    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        **android:layout_gravity="left"**
        android:layout_marginTop="10dp"/>
</LinearLayout>

public class MainActivity extends AppCompatActivity {

    private List<Fruit> fruitList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化水果数据
        initFruits();
        //获取到RecyclerView实例
        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

**//在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例
//StaggeredGridLayoutManager的构造函数接收两个参数,
//第一个参数指定布局列数,传入3会把布局分为3列。
//第二个参数用于指定布局的排列方式,传入StaggeredGridLayoutManager.VERTICAL会让布局纵向排列
				StaggeredGridLayoutManager layoutManager = new 
							StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
//把创建好的StaggeredGridLayoutManager实例设置到RecyclerView当中**
        recyclerView.setLayoutManager(layoutManager);
        //将水果数据传入到FruitAdapter的构造函数中
        FruitAdapter adapter = new FruitAdapter(fruitList);
        //完成适配器设置,关联数据和RecyclerView
        recyclerView.setAdapter(adapter);
    }

    private  void initFruits(){
        for(int i = 0 ;i<2 ;i++){
//每个水果名字都用**getRandomLengthName()方法来生成,保证各水果名字长短差距比较大。**
            Fruit apple = new Fruit(**getRandomLengthName("Apple")**,R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit(**getRandomLengthName("Banana")**,R.drawable.banana_pic);
            fruitList.add ( banana) ;
            Fruit orange = new Fruit(**getRandomLengthName("Orange")**,R.drawable.orange_pic);
            fruitList.add(orange) ;
            Fruit watermelon = new Fruit(**getRandomLengthName("watermelon")**,R.drawable.watermelon_pic);
            fruitList.add(watermelon) ;
            Fruit pear = new Fruit(**getRandomLengthName("Pear")**,R.drawable.pear_pic);
            fruitList.add(pear) ;
            Fruit grape = new Fruit(**getRandomLengthName("Grape")**,R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit pineapple = new Fruit(**getRandomLengthName("Pineapple")**,R.drawable.pineapple_pic);
            fruitList.add (pineapple) ;
            Fruit strawberry = new Fruit(**getRandomLengthName("strawberry")**,R.drawable.strawberry_pic);
            fruitList.add(strawberry) ;
            Fruit cherry = new Fruit(**getRandomLengthName("cherry")**,R.drawable.cherry_pic);
            fruitList.add( cherry) ;
            Fruit mango = new Fruit (**getRandomLengthName("Mango")**,R.drawable.mango_pic);
            fruitList.add(mango) ;
        }
    }

//瀑布流布局需要各个子项高度不一致才能看出明显效果,因此用**getRandomLengthName()方法**
    **private String getRandomLengthName(String name) {
        //在getRandomLengthName()中使用Random对象创造一个1到20之间的随机数
        Random random = new Random();
				//使用Random对象创造一个1到20之间的随机数
        int length = random.nextInt(20)+1;
        StringBuilder builder = new StringBuilder();
//将参数中的字符串随机重复遍。
        for(int i=0;i<length;i++){
            builder.append(name);
        }
        return builder.toString();
    }**
}

3.6.3RecyclerView的点击事件

和ListView一样如果RecyclerView不能响应点击事件就没什么实际用途。不过不同于ListView的是,RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法,需要我们给自己的子项具体的View去注册点击事件。相比于ListView要精确一些。setOnItemClickListener()方法注册的是子项的点击事件,但如果想点击子项中具体的某一按钮就没办法精确,实现起来很麻烦。为此RecyclerView摒弃了子项点击事件的监听器,所有点击事件就由具体View去注册

在RecyclerView注册点击事件,修改FruitAdapter中的代码。
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private List<Fruit> mFruitList;

    public static class ViewHolder extends RecyclerView.ViewHolder{
//在ViewHolder中添加了fruitView变量来保存子项最外层布局实例
				**View fruitView;**
        private ImageView fruitImage;
        private TextView fruitName;

//ViewHolder构造函数要传入一个View参数,通常是RecyclerView子项的最外层布局
        public ViewHolder(View view){
            super(view);
	          **fruitView = view;**
						fruitImage = (ImageView) view.findViewById(R.id.fruit_image); 
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }

        public ImageView getFruitImage() {
            return fruitImage;
        }

        public TextView getFruitName() {
            return fruitName;
        }
    }

//FruitAdapter也有一个构造函数,
//这个方法用于把要展示的数据源传进来,并赋值给一个全局变量mFruitList,后续操作都在数据源上进行
    public FruitAdapter(List<Fruit> fruitList)
    {
        mFruitList = fruitList;
    }
****
    @Override
**//在onCreateViewHolder()方法中注册点击事件**
    public ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) {
        //将加载出来的布局传入到构造函数中
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        **final ViewHolder holder = new ViewHolder(view);

//用holder给最外层布局fruitView注册监听器
        holder.fruitView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //先获取用户点击的position
                int position = holder.getAdapterPosition();
                //通过position拿到相应的Fruit实例
                Fruit fruit = mFruitList.get(position);
                //再使用Toast分别弹出两种不同的内容表示区别
                Toast.makeText(v.getContext(),"you clicked view" + fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });

//给holder的ImageView注册监听器
        holder.fruitImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int positon = holder.getAdapterPosition();
                Fruit fruit  = mFruitList.get(positon);
                Toast.makeText(v.getContext(),"you clicked image" + fruit.getName(),Toast.LENGTH_SHORT).show();         //不加.show()Toast肯定不显示啊
            }
        });**
        return holder;
    }
		...
}

inflate()方法的三个参数:

第一个是resource ID,指明了当前的Fragment对应的资源文件;
第二个参数是父容器控件;
第三个布尔值参数表明是否连接该布局和其父容器控件,在这里的情况设置为false,因为系统已经插入了这个布局到父控件,设置为true将会产生多余的一个View Group。

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
    private List<Msg> mMsgList;

    static class ViewHolder extends RecyclerView.ViewHolder{
        LinearLayout leftLayout;
        LinearLayout rightLayout;
        TextView leftMsg;
        TextView rightMsg;
        public ViewHolder(View view){
            super(view) ;
            leftLayout = (LinearLayout) view.findViewById(R.id.left_layout);
            rightLayout = (LinearLayout) view.findViewById(R.id.right_layout);
            leftMsg = (TextView) view.findViewById(R.id.left_msg);
            rightMsg = (TextView) view.findViewById(R.id.right_msg);
        }
    }
    public MsgAdapter(List<Msg> msgList){
        mMsgList = msgList;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).
										inflate(R.layout.msg_item,parent,false);
        return new ViewHolder(view);
    }

    @Override
//我们在onBindViewHolder()方法中增加了对消息类型的判断
//onBindViewHolder()方法用于对RecyclerView子项数据进行赋值,在每个子项被滚动到屏幕内时执行
    public void onBindViewHolder(MsgAdapter.ViewHolder holder, int position) {
        Msg msg = mMsgList.get(position);
        //对消息类型进行判断
        if(msg.getType() == Msg.TYPE_RECEIVED){     
            //如果这条消息是收到的,则显示左边的消息布局,将右边的消息布局隐藏
            holder.leftLayout.setVisibility(View.VISIBLE);
            holder.rightLayout.setVisibility(View.GONE);
            holder.leftMsg.setText(msg.getContent());
        } else if(msg.getType() == Msg.TYPE_SENT){
            //如果这条消息是发出的,则显示右边的消息布局,将左边的消息布局隐藏
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.leftLayout.setVisibility(View.GONE);
            holder.rightMsg.setText(msg.getContent());
        }
    }

    @Override
    public int getItemCount() {
        return mMsgList.size();
    }
}

3.7编写界面的最佳实践

创建一个UIBestPractice。

3.7.1制作9-Patch图片

9-Patch图片是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、或不可以。

3.7.2编写精美的聊天界面

编写RecyclerView子布局,新建msg_item.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">
    <LinearLayout
        android:id="@+id/left_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
<!--我们让收到的消息左对齐-->
        android:layout_gravity="left"
        android:background="@drawable/message_left">背景图
        <TextView
            android:id="@+id/left_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:textColor="#fff"/>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/right_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
<!--我们让发送的消息右对齐-->
        android:layout_gravity="right"
        android:background="@drawable/message_right">背景图
        <TextView
            android:id="@+id/right_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"/>
    </LinearLayout>
</LinearLayout>

让收到的消息和发出的消息在同一个布局里,只要在稍后的代码中根据消息的类型来决定隐藏和显示哪种消息就可以了。

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
    private List<Msg> mMsgList;

    static class ViewHolder extends RecyclerView.ViewHolder{
        LinearLayout leftLayout;
        LinearLayout rightLayout;
        TextView leftMsg;
        TextView rightMsg;
        public ViewHolder(View view){
            super(view) ;
            leftLayout = (LinearLayout) view.findViewById(R.id.left_layout);
            rightLayout = (LinearLayout) view.findViewById(R.id.right_layout);
            leftMsg = (TextView) view.findViewById(R.id.left_msg);
            rightMsg = (TextView) view.findViewById(R.id.right_msg);
        }
    }
    public MsgAdapter(List<Msg> msgList){
        mMsgList = msgList;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).
										inflate(R.layout.msg_item,parent,false);
        return new ViewHolder(view);
    }

    @Override
//我们在onBindViewHolder()方法中增加了对消息类型的判断
//onBindViewHolder()方法用于对RecyclerView子项数据进行赋值,在每个子项被滚动到屏幕内时执行
    public void onBindViewHolder(MsgAdapter.ViewHolder holder, int position) {
        Msg msg = mMsgList.get(position);
        //对消息类型进行判断
        if(msg.getType() == Msg.TYPE_RECEIVED){     
            //如果这条消息是收到的,则显示左边的消息布局,将右边的消息布局隐藏
            holder.leftLayout.setVisibility(View.VISIBLE);
            holder.rightLayout.setVisibility(View.GONE);
            holder.leftMsg.setText(msg.getContent());
        } else if(msg.getType() == Msg.TYPE_SENT){
            //如果这条消息是发出的,则显示右边的消息布局,将左边的消息布局隐藏
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.leftLayout.setVisibility(View.GONE);
            holder.rightMsg.setText(msg.getContent());
        }
    }

    @Override
    public int getItemCount() {
        return mMsgList.size();
    }
}

最后修改MainActivity中代码为RecyclerView初始化一些数据,并给发送按钮加入事件响应。

public class MainActivity extends AppCompatActivity {

    private List<Msg> msgList = new ArrayList<>();

    private EditText inputText;

    private Button send;

    private RecyclerView msgRecyclerView;

    private MsgAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initMsgs();
        inputText = (EditText) findViewById(R.id.input_text);
        send = (Button) findViewById(R.id.send);
        msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        msgRecyclerView.setLayoutManager(layoutManager);
        adapter = new MsgAdapter(msgList);
        msgRecyclerView.setAdapter(adapter);

        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//在发送按钮的点击事件里获取EditText中的内容
                String content = inputText.getText().toString();
                if (!"".equals(content)) {
//如果内容不为null则创建出一个新的Msg对象并添加到msgList列表中去
                    Msg msg = new Msg(content, Msg.TYPE_SENT);
                    msgList.add(msg);
//用Adapter的notifyItemInserted()方法用于通知列表有新的数据插入,
//这样新增的消息才能在Recycler中显示
                    adapter.notifyItemInserted(msgList.size()-1);
//调用ReyclerView的scrollToPosition()将显示的数据定位到最后一行,保证能看到最后发出的一条消息
                    msgRecyclerView.scrollToPosition(msgList.size()-1);
                    inputText.setText("");
                }
            }
        });
    }

//初始化几条数据用于在RecyclerView中显示
    private void initMsgs() {
        Msg msg1 = new Msg("Hello guy.", Msg.TYPE_RECEIVED);
        msgList.add(msg1);
        Msg msg2 = new Msg("Hello. Who is that?", Msg.TYPE_SENT);
        msgList.add(msg2);
        Msg msg3 = new Msg("This is Tom, I'm so happy to talking wiht you.", Msg.TYPE_RECEIVED);
        msgList.add(msg3);
    }
}

3.8小结与点评

本章从Android中的一些常见控件入手,依次介绍了基本布局的用法、自定义控件的方法、ListView的详细用法以及RecyclerView的使用。下一张涉及一些Android平板方面的知识点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值