Android学习记录——3.UI

1 如何编写程序界面

Android中有多种编写程序界面的方式可供选择。Android Studio和Eclipse中都提供了相应的可视化编辑器,允许使用拖放控件的方式来编写布局,并能在视图上直接修改控件的属性。不 过我并不推荐你使用这种方式来编写界面,因为可视化编辑工具并不利于你去真正了解界面背后的实现原理。通过这种方式制作出的界面通常不具有很好的屏幕适配性,而且当需要编写较为复杂的界面时,可视化编辑工具将很难胜任。因此本书中所有的界面都将通过最基本的方式去实现, 即编写XML代码。等你完全掌握了使用XML来编写界面的方法之后,不管是进行高复杂度的界面实现,还是分析和修改当前现有界面,对你来说都将是手到擒来。
讲了这么多理论的东西,也是时候学习一下到底如何编写程序界面了,下面我们就从Android中几种常见的控件开始吧。

2.常用控件的使用方法

Android提供了大量的UI控件,合理地使用这些控件就可以非常轻松地编写出相当不错的界 面,下面我们就挑选几种常用的控件,详细介绍一下它们的使用方法。

2.1 TextView

TextView可以说是Android中最简单的一个控件了,你在前面其实已经和它打过一些交道了。 它主要用于在界面上显示一段文本信息,比如你在第1篇博客看到的“Helloworld!”。下面我们就来看一看关于TextView的更多用法。
修改activity_main.xml中的代码,如图所示:
在这里插入图片描述
外面的LinearLayout先忽略不看,在TextView中我们使用android: id给当前控件定义了一 个唯一标识符,这个属性在上一篇博客中已经讲解过了。然后使用android:layout_width和 android:layout_height指定了控件的宽度和高度。Android中所有的控件都具有这两个属性, 可选值有 3 种:match parent、fill _parent 和 wrap_content。其中 match_parent 和 fill_parent的意义相同,现在官方更加推荐使用match_parent。match_parent表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。所以上面的代码就表示让TextView的宽度和父布局一样宽,也就是手机屏幕的宽度,让TextView的高度足够包含住里面的内容就行。当然除了使用上述值,你也可以对控件的宽和高指定一个固定的大小,但是这样做有时会在不同手机屏幕的适配方面岀现问题。
接下来我们通过android:text指定TextView中显示的文本内容,现在运行程序,效果如图所示:
在这里插入图片描述
虽然指定的文本内容正常显示了,不过我们好像没看出来TextView的宽度是和屏幕一样宽 的。其实这是由于TextView中的文字默认是居左上角对齐的,虽然TextView的宽度充满了整个 屏幕,可是由于文字内容不够长,所以从效果上完全看不出来。现在我们修改TextView的文字 对齐方式,如下所示:
在这里插入图片描述

这也说明了 TextView的宽度确实是和屏幕宽度一样的。
另外我们还可以对TextView中文字的大小和颜色进行修改,如下所示:
在这里插入图片描述
通过android: textsize属性可以指定文字的大小,通过android: textColor属性可以指定文字的颜色,在Android中字体大小使用sp作为单位。重新运行程序,效果如图所示:
在这里插入图片描述
当然TextView中还有很多其他的属性,这里就不再一一介绍了,用到的时候去查阅文档就可以了。

2.2 Button

Button是程序用于和用户进行交互的一个重要控件,相信你对这个控件已经非常熟悉了,因为我们在上一篇博客用了太多次Buttono。它可配置的属性和TextView是差不多的,我们可以在 activity_main.xml中这样加入 Button:
在这里插入图片描述
加入Button之后的界面如图所示:
在这里插入图片描述
细心的你可能会留意到,我们在布局文件里面设置的文字是“Button”,但最终的显示结果 却是“BUTTON”。这是由于系统会对Button中的所有英文字母自动进行大写转换,如果这不是 你想要的效果,可以使用如下配置来禁用这一默认特性:
在这里插入图片描述
接下来我们可以在MainActivity中为Button的点击事件注册一个监听器,如下所示:
在这里插入图片描述
这样每当点击按钮时,就会执行监听器中的onClick()方法,我们只需要在这个方法中加入待处理的逻辑就行了。如果你不喜欢使用匿名类的方式来注册监听器,也可以使用实现接口的方式来进行注册,代码如下所示:
在这里插入图片描述
这两种写法都可以实现对按钮点击事件的监听,至于使用哪一种就全凭你的喜好了。

2.3 EditText

EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内 容,并可以在程序中对这些内容进行处理。EditText的应用场景非常普遍,在进行发短信、发微博、聊QQ等操作时,你不得不使用EditText。那我们来看一看如何在界面上加入EditText吧, 修改activity main.xml中的代码,如下所示:
在这里插入图片描述
其实看到这里,我估计你已经总结出Android控件的使用规律了,用法基本上都很相似:给控件定义一个id,再指定控件的宽度和高度,然后再适当加入一些控件特有的属性就差不多了。
所以使用XML来编写界面其实一点都不难,完全可以不用借助任何可视化工具来实现。现 在重新运行一下程序,EditText就已经在界面上显示出来了,并且我们是可以在里面输入内容的, 如图所示:
在这里插入图片描述
细心的你平时应该会留意到,一些做得比较人性化的软件会在输入框里显示一些提示性的文 字,然后一旦用户输入了任何内容,这些提示性的文字就会消失。这种提示功能在Android里是 非常容易实现的,我们甚至不需要做任何的逻辑控制,因为系统已经帮我们都处理好了。修改 activity_main.xml, 如下所示:
在这里插入图片描述
这里使用android:hint属性指定了一段提示性的文本,然后重新运行程序,效果如图所示:
在这里插入图片描述
可以看到,EditText中显示了一段提示性文本,然后当我们输入任何内容时,这段文本就会 自动消失。
不过,随着输入的内容不断增多,EditText会被不断地拉长。这时由于EditText的高度指定 的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得 非常难看。我们可以使用android:maxLines属性来解决这个问题,修改activity_main.xml,如下所示:
在这里插入图片描述
这里通过android :maxLines指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,而EditText则不会再继续拉伸。
我们还可以结合使用EditText与Button来完成一些功能,比如通过点击按钮来获取EditText中输入的内容。修改UIActivity中的代码,如下所示:
在这里插入图片描述
首先通过findViewById()方法得到EditText的实例,然后在按钮的点击事件里调用EditText的getText()方法获取到输入的内容,再调用toString()方法转换成字符串,最后还是老方法, 使用Toast将输入的内容显示出来
重新运行程序,在EditText中输入一段内容,然后点击按钮,效果如图所示:
在这里插入图片描述

2.4 ImageView

ImageView是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多彩。学习这个控件需要提前准备好一些图片,图片通常都是放在以“drawable”开头的目录下的。 目前我们的项目中有一个空的drawable目录,不过由于这个目录没有指定具体的分辨率,所以一般不使用它来放置图片。这里我们在res目录下新建一个drawable-xhdpi目录,然后将事先准备好的两张图片img l.png和img_2.png复制到该目录当中。
接下来修改activity_main.xml,如下所示:
在这里插入图片描述
可以看到,这里使用android:src属性给ImageView指定了一张图片。由于图片的宽和高 都是未知的,所以将ImageView的宽和高都设定为wrap_content。这样就保证了不管图片的尺寸是多少,图片都可以完整地展示出来。重新运行程序,效果如图所示:
在这里插入图片描述
我们还可以在程序中通过代码动态地更改ImageView中的图片,然后修改MainActivity的代码,如下所示:
在这里插入图片描述
在按钮的点击事件里,通过调用ImageView的setImageResource()方法将显示的图片改成 img_2,现在重新运行程序,然后点击一下按钮,就可以看到ImageView中显示的图片改变了。

2.5 ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。它的用法也 非常简单,修改activity_main.xml中的代码,如下所示:
在这里插入图片描述
重新运行程序,会看到屏幕中有一个圆形进度条正在旋转,如图所示:
在这里插入图片描述
这时你可能会问,旋转的进度条表明我们的程序正在加载数据,那数据总会有加载完的时候吧?如何才能让进度条在数据加载完成时消失呢?这里我们就需要用到一个新的知识点: Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android:visibility 进行指定,可选值有3种:visible, invisible和gone。visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件不可见, 但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。gone则表示控件不仅不可见,而且不再占用任何屏幕空间。我们还可以通过代码来设置控件的可见性,使用的是 setvisibility()方法,可以传入 View.VISIBLE. View.INVISIBLE 和 View.GONE 这 3 种值。
接下来我们就来尝试实现,点击一下按钮让进度条消失,再点击一下按钮让进度条出现的这 种效果。修改MainActivity中的代码,如下所示:
在这里插入图片描述
在按钮的点击事件中,我们通过getVisibility()方法来判断ProgressBar是否可见,如果可见就将ProgressBar隐藏掉,如果不可见就将ProgressBar显示出来。重新运行程序,然后不断 地点击按钮,你就会看到进度条会在显示与隐藏之间来回切换。
另外,我们还可以给ProgressBar指定不同的样式,刚刚是圆形进度条,通过style属性可 以将它指定成水平进度条,修改activity_main.xml中的代码,如下所示:
在这里插入图片描述
指定成水平进度条后,我们还可以通过android:max属性给进度条设置一个最大值,然后在代码中动态地更改进度条的进度。修改MainActivity中的代码,如下所示:
在这里插入图片描述
每点击一次按钮,我们就获取进度条的当前进度,然后在现有的进度上加10作为更新后的进度。重新运行程序,点击数次按钮后,进度条会达到最大值,然后保持饱满状态。
ProgressBar还有几种其他的样式,你可以自己去尝试一下。

2.6 AlertDialog

AlertDialog可以在当前的界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的, 能够屏蔽掉其他控件的交互能力,因此AlertDialog一般都是用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。下面我们来学习一下它的用法,修改MainActivity中的代码,如下所示:
在这里插入图片描述
首先通过AlertDialog.Builder创建一个AlertDialog的实例,然后可以为这个对话框设置标题、 内容、可否取消等属性,接下来调用setPositiveButton()方法为对话框设置确定按钮的点击事件,调用setNegativeButton()方法设置取消按钮的点击事件,最后调用show()方法将对话框显示出来。重新运行程序,点击按钮后,效果如图所示:
在这里插入图片描述

2.7 ProgressDialog

progressDialog和AlertDialog有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其他控件的交互能力。不同的是,ProgressDialog会在对话框中显示一个进度条,一般用于表示当前操作比较耗时,让用户耐心地等待。它的用法和AlertDialog也比较相似,修改MainActivity中的代码,如下所示:
在这里插入图片描述
可以看到,这里也是先构建出一个ProgressDialog对象,然后同样可以设置标题、内容、可否取消等属性,最后也是通过调用show()方法将ProgressDialog显示出来。重新运行程序,点击按钮后,效果如图所示:
在这里插入图片描述
注意,如果在setCancelable()中传入了 false,表示ProgressDialog是不能通过Back键取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。

3.详解四种基本布局

一个丰富的界面总是要由很多个控件组成的,那我们如何才能让各个控件都有条不紊地摆放在界面上,而不是乱糟糟的呢?这就需要借助布局来实现了。布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现,如图所示:
在这里插入图片描述

3.1 线性布局

LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个 布局会将它所包含的控件在线性方向上依次排列。相信你之前也已经注意到了,我们在上一篇博客中学习控件用法时,所有的控件就都是放在LinearLayout布局里的,因此上一篇博客中的控件也确实是在垂直方向上线性排列的。
既然是线性排列,肯定就不仅只有一个方向,那为什么上一节中的控件都是在垂直方向排列 的呢?这是由于我们通过android:orientation属性指定了排列方向是vertical,如果指定的是 horizontal,控件就会在水平方向上排列了。
如果将android:orientation属性的值改成了horizontal,这就意味着要让LinearLayout中的控件在水平方向上依次排列。当然如果不指定android:orientation属性的值,默认的排列方向就是horizontal。
这里需要注意,如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,因为这样的话,单独一个控件就会将整个水平方向占满,其他的控件就 没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match _parent。
首先来看android:layout_gravity属性,它和我们上一篇博客中学到的android:gravity 属性看起来有些相似,这两个属性有什么区别呢?其实从名字就可以看出,android:gravity 用于指定文字在控件中的对齐方式,而android: layout_gravity用于指定控件在布局中的对齐方式。android:layout_gravity的可选值和android:gravity差不多,但是需要注意,当 LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方 式才会生效。
接下来我们学习下LinearLayout中的另一个重要属性 android:layout_weight。这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。
为什么将android:layout_weight属性的值同时指定为1就会平分屏幕宽度呢?其实原理也很简单,系统会先把LinearLayout下所有控件指定的layout _weight值相加,得到一个总值, 然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算岀的总值。因此如果想让EditText占据屏幕宽度的3/5, Button占据屏幕宽度的2/5,只需要将EditText的layout, weight 改成 3, Button 的 layout weight 改成 2 就可以了。
除此之外,由于我们使用了 android:layout_weight属性,此时控件的宽度就不应该再由android :layout_width来决定,这里指定成Odp是一种比较规范的写法。另外,dp是Android中用于指定控件大小、间距等属性的单位,后面我们还会经常用到它。

3.2 相对布局

RelativeLayout又称作相对布局,也是一种非常常用的布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意一些,它可以通过相对定位的方式让控件出现在布局的任何位置。也正因为如此,RelativeLayout中的属性非常多,不过这些属性都是有规律可循的,其实并不难理解和记忆。
相对布局通过这几个属性来设置控件相对于父布局的定位:android:layout_alignParentLeft、android:layoutalignParentTop、android:layoutalignParentRight、android:layout_ alignParentBottom、android :layout_centerlnParent 。这几个属性我们之前都没接触过, 可是它们的名字已经完全说明了它们的作用。
除此之外,控件也可以相对于控件进行定位。android: layout_above属性可以让一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用,其他的,属性也都是相似的,android: layout_below表示让一个控件位于另一个控件的下方,android: layout toLeftOf表示让一个控件位于另一个控件的左侧,android: layout_toRightOf表示让一个控件位于另一个控件的右侧。注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面, 不然会出现找不到id的情况。
RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout _alignLeft 表示让一个控件的左边缘和另一个控件的左边缘对齐,android: layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout_alignTop和 android:layout_alignBottom,道理都是一样的,我就不再多说。

3.3 帧布局

FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景也少了很多。这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角。
除了这种默认效果之外,我们还可以使用layout_gravity属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。
总体来讲,FrameLayout由于定位方式的欠缺,导致它的应用场景也比较少,不过,其可以用于碎片的布局。

3.4 百分比布局

前面介绍的3种布局都是从Android 1.0版本中就开始支持了,一直沿用到现在,可以说是 足了绝大多数场景的界面设计需求。不过细心的你会发现,只有LinearLayout支持使用 layout_weight属性来实现按比例指定控件大小的功能,其他两种布局都不支持。比如说,如果想用RelativeLayout来实现让两个按钮平分布局宽度的效果,则是比较困难的。
为此,Android引入了一种全新的布局方式来解决此问题一一百分比布局。在这种布局中,我们可以不再使用wrap_content, match_parent等方式来指定控件的大小,而是允许直接指定控件在布局中所占的百分比,这样的话就可以轻松实现平分布局甚至是任意比例分割布局的效果了。
由于LinearLayout本身已经支持按比例指定控件的大小了,因此百分比布局只为Frame-Layout 和 RelativeLayout 进行了功能扩展,提供了 PercentFrameLayout 和PercentRelativeLayout 这两个全新的布局,下面我们就来具体学习一下。
不同于前3种布局,百分比布局属于新增布局,那么怎么才能做到让新增布局在所有Android 版本上都能使用呢?为此,Android团队将百分比布局定义在了 support库当中,我们只需要在项目的build.gradle中添加百分比布局库的依赖,就能保证百分比布局在Android所有系统版本上的兼容性了。
打开app/build.gradle文件,在dependencies闭包中添加如下内容:

implementation 'com.android.support:percent:27.2.1'

版本号需要与编译版本对应。
假设要使用PercentFrameLayout,由于百分比布局并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。然后还必须定义一个app的命名空间,这样才能使用百分比布局的自定义属性。
在PercentFrameLayout中,使用app: layout_widthPercent属性将各按钮的宽度指定为布局的50%,使用app:layout_heightPercent属性将各按钮的高度指定为布局的50%。这里之所以能使用app前缀的属性就是因为刚才定义了 app的命名空间,当然我们 一直能使用android前缀的属性也是同样的道理。
不过PercentFrameLayout还是会继承FrameLayout的特性,即所有的控件默认都是摆放在布局的左上角。为了让控件不会重叠,可以借助 layout_gravity属性来调整位置。
PercentFrameLayout用法就介绍到这里,另外一个PercentRelativeLayout的用法也是非常相似的,它继承了 RelativeLayout中的所有属性,并且可以使用app: layout_widthPercent 和app:layout_heightPercent来按百分比指定控件的宽高。
这样我们就把最常用的几种布局都讲解完了,其实Android中还有AbsoluteLayout,TableLayout等布局,不过由于使用得实在是太少了,就不在进行讲解了。

4.自定义控件

在前面的博客我们已经学习了 Android中的一些常用控件以及基本布局的用法,不过当时我们并没有关注这些控件和布局的继承结构,现在是时候来看一下了。
在这里插入图片描述
可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基 础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View 和子ViewGroup,是一个用于放置控件和布局的容器。
这个时候我们就可以思考一下,当系统自带的控件并不能满足我们的需求时,可不可以利用 上面的继承结构来创建自定义控件呢?答案是肯定的。下面我们就来学习一下。

4.1 引入布局

经过前面的学习,相信创建一个标题栏布局对你来说已经不是什么困难的事情了,只需要加入两个Button和一个TextView,然后在布局中摆放好就可以了。可是这样做却存在着一个 问题,一般我们的程序中可能有很多个活动都需要这样的标题栏,如果在每个活动的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复。这个时候我们就可以使用引入布局的方式来解决这个问题,新建一个布局title.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="wrap_content"
    android:background="@drawable/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="@drawable/back_bg"
        android:text="Back"
        android:textColor="#fff"
        />

    <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="@drawable/edit_bg"
        android:text="Edit"
        android:textColor="#fff" />

</LinearLayout>

可以看到,我们在LinearLayout中分别加入了两个Button和一个TextView,左边的Button可用于返回,右边的Button可用于编辑,中间的TextView则可以显示一段标题文本。上面代码中的大多数属性都是你已经见过的,下面我来说明一下几个之前没有讲过的属性。android: background用于为布局或控件指定一个背景,可以使用颜色或图片来进行填充,这里我提前准备好了 3张图片 title_bg.png. back_bg.png和edit_bg.png,分别用于作为标题栏、返回按钮和
编辑按钮的背景。另外,在两个Button中我们都使用了 android: layout_margin这个属性,它可以指定控件在上下左右方向上偏移的距离,当然也可以使用android :layout_marginLeft或 android:layout marginTop等属性来单独指定控件在某个方向上偏移的距离。
现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了,修改activity_main.xml中的代码,如下所示:

<include layout="@layout/title"/>

没错!我们只需要通过一行include语句将标题栏布局引入进来就可以了。
最后别忘了在MainActivity中将系统自带的标题栏隐藏掉,代码如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null){
        actionBar.hide();
    }
}

这里我们调用了 getSupportActionBar()方法来获得ActionBar的实例,然后再调用ActionBar的hide()方法将标题栏隐藏起来。关于ActionBar的更多用法我们将会在后面讲解,现在你只需要知道可以通过这种写法来隐藏标题栏就足够了。现在运行一下程序,效果如图所示:
在这里插入图片描述
使用这种方式,不管有多少布局需要添加标题栏,只需一行include语句就可以了。

4.2 创建自定义控件

引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个活动中为这些控件单独编写一次事件注册的代码。比如说标题栏中的返回按钮,其实不管是在哪一个活动中,这个按钮的功能都是相同的,即销毁当前活动。而如果在每一个活动中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多重复代码,这种 情况最好是使用自定义控件的方式来解决。
新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件,代码如下所示:

public class TitleLayout extends LinearLayout {
    public TitleLayout(Context context, AttributeSet attrs){
    super(context, attrs);
    LayoutInflater.from(context).inflate(R.layout.title, this);
    }
}

首先我们重写了 LinearLayout中带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,这就要借助 Layoutlnflater来实现了。通过 Layoutlnflater 的 f rom()方法可以构建出一个 Layoutlnflater 对 象,然后调用inflate()方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局 再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入thiso
现在自定义控件已经创建好了,然后我们需要在布局文件中添加这个自定义控件,修改 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.mxt.firstandroidapplication.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


</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 view) {
            ((Activity)getContext()).finish();
        }
    });
    titleEdit.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(getContext(),"You Click Edit Button",Toast.LENGTH_SHORT).show();
        }
    });
    }
}

首先还是通过findViewByld()方法得到按钮的实例,然后分别调用setOnClickListener()方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹岀 一段文本。重新运行程序,点击一下编辑按钮,效果如图所示:
在这里插入图片描述
这样的话,每当我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。

5.最常用和最难用的控件——ListView

ListView绝对可以称得上是Android中最常用的控件之一,几乎所有的应用程序都会用到它。 由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕。相信你其实每天都在使用这个控件,比如查看QQ聊天记录,翻阅微博最新消息,等等。
不过比起前面介绍的几种控件,ListView的用法也相对复杂了很多,因此我们现在就来对ListView进行非常详细的讲解。

5.1 ListView的简单用法

修改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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </ListView>


</LinearLayout>

在布局中加入ListView控件还算非常简单,先为ListView指定一个id,然后将宽度和高度 都设置为match parent,这样ListView也就占满了整个布局的空间。
接下来修改MainActivity中的代码,如下所示:

private String[] data = { "Apple", "Banana", "Orange", "Watermelon", "Pear",
        "Grape", "Pineapple", "Strawberry", "Cherry", "Mango", "Apple",
        "Banana","Orange", "Watermelon", "Pear","Grape", "Pineapple",
        "Strawberry","Cherry","Mangou" };

@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是用于展示大量数据的,那我们就应该先将数据提供好。这些数据可以是从网上下载的,也可以是从数据库中读取的,应该视具体的应用程序场景而定。这里我们就简单使用了一个data数组来测试,里面包含了很多水果的名称。
不过,数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android 中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。ArrayAdapter有多个构造函数的重载,你应该根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将 ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id、以及要适配的数据。注意,我们使用了 android.R.layout.simple_list item l作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个 TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。
最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。
现在运行一下程序,效果如图所示。可以通过滚动的方式来查看屏幕外的数据:
在这里插入图片描述

5.2 定制ListView的界面

只能显示一段文本的ListView实在是太单调了,我们现在就来对ListView的界面进行定制,让它可以显示更加丰富的内容。
首先需要准备好一组图片,分别对应上面提供的每一种水果,待会我们要让这些水果名称的旁边都有一个图样。
接着定义一个实体类,作为ListView适配器的适配类型。新建类Fruit,代码如下所示:

package com.mxt.firstandroidapplication;

public class Fruit {
    private String name;
    private int imageld;
    public Fruit(String name, int imageld) {
    this.name = name;
    this.imageld = imageld;
    }
    public String getName() {
    return name;
    }
    public int getlmageld() {
    return imageld;
    }
}

Fruit类中只有两个字段,name表示水果的名字,imageld表示水果对应图片的资源id。 然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建fruit_Item.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="wrap_content">
    <ImageView
        android:id="@+id/fruitimage"
        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_vertical"
        android:layout_marginLeft="10dp" />
</LinearLayout>

在这个布局中,我们定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称,并让TextView在垂直方向上居中显示。
接下来需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为 Fruit类。新建类FruitAdapter,代码如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceld;

    public FruitAdapter(Context context, int textViewResourceld,
                        List<Fruit> objects) {
        super(context, textViewResourceld, objects);
        resourceld = textViewResourceld;
    }
        @Override
        public View getView(int position, View convertView, ViewGroup parent){
            Fruit fruit = getItem(position); // 获取当前项的 Fruit 实例
            View view = LayoutInflater.from(getContext()).inflate(resourceld, parent, false);
            ImageView fruitimage = (ImageView) view.findViewById(R.id.fruitimage);
            TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
            fruitimage.setImageResource(fruit.getlmageld());
            fruitName.setText(fruit.getName());
            return view;
        }
}

FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据都传递进来。另外又重写了 getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调 用。在getView()方法中,首先通过getltem()方法得到当前项的Fruit实例,然后使用 Layoutlnflater来为这个子项加载我们传入的布局。
这里Layoutlnflater的inflate。方法接收3个参数,前两个参数我们已经知道是什么 意思了,第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不为 这个View添加父布局,因为一旦View有了父布局之后,它就不能再添加到ListView中了。如 果你现在还不能理解这段话的含义也没关系,只需要知道这是ListView中的标准写法就可以了, 当你以后对View理解得更加深刻的时候,再来读这段话就没有问题了。
我们继续往下看,接下来调用View的findViewById()方法分别获取到ImageView和 TextView的实例,并分别调用它们的setImageResource()和setText ()方法来设置显示的图 片和文字,最后将布局返回,这样我们自定义的适配器就完成了。
下面修改MainActivity中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

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);
            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);
        }
}
}

可以看到,这里添加了一个initFruits()方法,用于初始化所有的水果数据。在Fruit类的构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中。另外我们使用了一个for循环将所有的水果数据添加了两遍,这是因为如果只添加一遍的话,数据量还不足以充满整个屏幕。接着在onCreate()方法中创建了 FruitAdapter对象,并将FruitAdapter 作为适配器传递给ListView,这样定制ListView界面的任务就完成了。
现在重新运行程序,效果如图所示:
在这里插入图片描述

5.3 提升ListView的运行效率

之所以说ListView这个控件很难用,就是因为它有很多细节可以优化,其中运行效率就是很 重要的一点。目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法中,每次都将布局重新加载了一遍,当ListView快速滚动的时候,这就会成为性能的瓶颈。
仔细观察会发现,getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。修改FruitAdapter中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceld;

    public FruitAdapter(Context context, int textViewResourceld,
                        List<Fruit> objects) {
        super(context, textViewResourceld, objects);
        resourceld = textViewResourceld;
    }
        @Override
        public View getView(int position, View convertView, ViewGroup parent){
            Fruit fruit = getItem(position); // 获取当前项的 Fruit 实例
            View view;
            if (convertView == null){
                view = LayoutInflater.from(getContext()).inflate(resourceld, parent, false);
            }
            else {
                view = convertView;
            }
            ImageView fruitimage = (ImageView) view.findViewById(R.id.fruitimage);
            TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
            fruitimage.setImageResource(fruit.getlmageld());
            fruitName.setText(fruit.getName());
            return view;
        }
}

可以看到,现在我们在getView()方法中进行了判断,如果convertView为null,则使用 Layoutlnflater去加载布局,如果不为null则直接对convertView进行重用。这样就大大提高了 ListView的运行效率,在快速滚动的时候也可以表现出更好的性能。
不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。 我们可以借助一个ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceld;

    public FruitAdapter(Context context, int textViewResourceld,
                        List<Fruit> objects) {
        super(context, textViewResourceld, objects);
        resourceld = textViewResourceld;
    }

        @Override
        public View getView(int position, View convertView, ViewGroup parent){
            Fruit fruit = getItem(position); // 获取当前项的 Fruit 实例
            View view;
            ViewHolder viewHolder;
            if (convertView == null){
                view = LayoutInflater.from(getContext()).inflate(resourceld, parent, false);
                viewHolder = new ViewHolder();
                viewHolder.fruitImage = (ImageView)view.findViewById(R.id.fruitimage);
                viewHolder.fruitName = (TextView)view.findViewById(R.id.fruit_name);
                view.setTag(viewHolder);
            }
            else {
                view = convertView;
                viewHolder = (ViewHolder)view.getTag();
            }
            viewHolder.fruitImage.setImageResource(fruit.getlmageld());
            viewHolder.fruitName.setText(fruit.getName());
            return view;
        }

        class ViewHolder{
            ImageView fruitImage;
            TextView fruitName;
        }
}

我们新增了一个内部类ViewHolder,用于对控件的实例进行缓存。当convertView为null 的时候,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder里,然后调用View的setTag()方法,将ViewHolder对象存储在View中。当convertView不为null的时候, 则调用View的getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了 ViewHolder里,就没有必要每次都通过findViewByld()方法来获取控件实例了。
通过这两步优化之后,我们ListView的运行效率就已经非常不错了。

5.4 ListView的点击事件

话说回来,ListView的滚动毕竟只是满足了我们视觉上的效果,可是如果ListView中的子项 不能点击的话,这个控件就没有什么实际的用途了。因此,本小节我们就来学习一下ListView如 何才能响应用户的点击事件。
修改MainActivity中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

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);
        listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Fruit fruit = fruitList.get(i);
                Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
    }

    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);
        }
}
}

可以看到,我们使用setOnItemClickListener()方法为ListView注册了一个监听器,当 用户点击了 ListView中的任何一个子项时,就会回调onItemClick()方法。在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将 水果的名字显示出来。
重新运行程序,并点击一下橘子,效果如图所示:
在这里插入图片描述

6.更强大的滚动控件——RecyclerView

ListView由于其强大的功能,在过去的Android开发当中可以说是贡献卓越,直到今天仍然还有不计其数的程序在继续使用着ListView。不过ListView并不是完全没有缺点的,比如说如果我们不使用一些技巧来提升它的运行效率,那么ListView的性能就会非常差。还有,ListView的扩展性也不够好,它只能实现数据纵向滚动的效果,如果我们想实现横向滚动的ListView是做不到的。
为此,Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的 ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。 目前Android官方更加推荐使用RecyclerView,未来也会有更多的程序逐渐从ListView转向 RecyclerView,那么本节我们就来详细讲解一下RecyclerView的用法。

6.1 RecycleView的基本用法

和百分比布局类似,RecyclerView也属于新增的控件,为了让RecyclerView在所有Android 版本上都能使用,Android团队采取了同样的方式,将RecyclerView定义在了 support库当中。因 此,想要使用RecyclerView这个控件,首先需要在项目的build.gradle中添加相应的依赖库才行。
打开app/build.gradle文件,在dependencies闭包中添加如下内容:

implementation 'com.android.support:recyclerview-v7:27.1.1'

添加完之后记得要点击一下Sync Now来进行同步。然后修改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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>


</LinearLayout>

在布局中加入RecyclerView控件也是非常简单的,先为RecyclerView指定一个id,然后将 宽度和高度都设置为match_parent,这样RecyclerView也就占满了整个布局的空间。需要注意的是,由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写岀来。
这里我们想要使用RecyclerView来实现和ListView相同的效果,因此就需要准备一份同样的水果图片。简单起见,我们就直接把上一节中图片复制过来就可以了,另外顺便将Fruit类和fruit_item.xml也复制过来,省得将同样的代码再写一遍。
接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自 RecyclerView.Adapter,并将泛型指定为 FruitAdapter.ViewHolder() 其中,ViewHolder 是我们在FruitAdapter中定义的一个内部类,代码如下所示:

package com.mxt.firstandroidapplication;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

/**
 * Created by Administrator on 2018-09-18.
 */

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{
    private List<Fruit> mFruitList;
    static class ViewHolder extends RecyclerView.ViewHolder{
//        View fruitView;
        ImageView fruitImage;
        TextView fruitName;
        public ViewHolder(View view){
            super(view);
//            fruitView = view;
            fruitImage = (ImageView)view.findViewById(R.id.fruitimage);
            fruitName = (TextView)view.findViewById(R.id.fruit_name);
        }
    }
    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
//        holder.fruitView.setOnClickListener(new View.OnClickListener(){
//            @Override
//            public void onClick(View v){
//                int position = holder.getAdapterPosition();
//                Fruit fruit = mFruitList.get(position);
//                Toast.makeText(v.getContext(),"你点击了文本" + fruit.getName(),Toast.LENGTH_SHORT).show();
//            }
//        });
//        holder.fruitImage.setOnClickListener(new View.OnClickListener(){
//            @Override
//            public void onClick(View v){
//                int position = holder.getAdapterPosition();
//                Fruit fruit = mFruitList.get(position);
//                Toast.makeText(v.getContext(),"你点击了图像" + fruit.getName(),Toast.LENGTH_SHORT).show();
//            }
//        });
        return holder;
    }
    @Override
    public void onBindViewHolder(ViewHolder holder,int position){
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getlmageld());
        holder.fruitName.setText(fruit.getName());
    }
    @Override
    public int getItemCount(){
        return mFruitList.size();
    }
}

虽然这段代码看上去好像有点长,但其实它比ListView的适配器要更容易理解。这里我们首先定义了一个内部类 ViewHolder, ViewHolder 要继承自 RecyclerView.ViewHolder。 然后 ViewHolder的构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewByld()方法来获取到布局中的ImageView和TextView的实例了。
接着往下看,FruitAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来, 并赋值给一个全局变量mFruitList,我们后续的操作都将在这个数据源的基础上进行。
继续往下看,由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写 onCreateViewHolder()、onBindViewHolder()和getltemCount()这 3 个方法。onCreateViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到构造函数当中,最后将ViewHolder的实例返回。onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。getltemCount ()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
适配器准备好了之后,我们就可以开始使用RecyclerView了,修改MainActivity中的代码, 如下所示:

package com.mxt.firstandroidapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

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)findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        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);
        }
}
}

可以看到,这里使用了一个同样的initFruits()方法,用于初始化所有的水果数据。接着在onCreate()方法中我们先获取到RecyclerView的实例,然后创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中。LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。 接下来我们创建了 FruitAdapter的实例,并将水果数据传入到FruitAdapter的构造函数中, 最后调用RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。
现在可以运行一下程序了,效果如图所示:
在这里插入图片描述
可以看到,我们使用RecyclerView实现了和ListView几乎一模一样的效果,虽说在代码量方面并没有明显地减少,但是逻辑变得更加清晰了。当然这只是RecyclerView的基本用法而已, 接下来我们就看一看RecyclerView还能实现哪些ListView实现不了的效果。

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

我们已经知道,ListView的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚动的话,ListView就做不到了。那么RecyclerView就能做得到吗?当然可以,不仅能做得到,还 非常简单,那么接下来我们就尝试实现一下横向滚动的效果。
首先要对fruit_item布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于纵向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_item里的元素改成垂直排列才比较合理。修改fruit_item.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="100dp"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/fruitimage"
        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="center_horizontal"
        android:layout_marginTop="10dp" />
</LinearLayout>

可以看到,我们将LinearLayout改成垂直方向排列,并把宽度设为100dp。这里将宽度指定为固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView的子项就会有长有短,非常不美观;而如果用match_parent的话,就会导致宽度过长,一个子项占满 整个屏幕。
然后我们将ImageView和TextView都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一些距离。
接下来修改MainActivity中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

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)findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        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);
        }
}
}

MainActivity 中只加入了一行代码,调用 LinearLayoutManager 的 setOrientation ()方法来设置布局的排列方向,默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerView就可以横向滚动了。
重新运行一下程序,效果如图所示:
在这里插入图片描述
为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就能实现了呢? 这主要得益于RecyclerView出色的设计。ListView的布局排列是由自身去管理的,而RecyclerView 则将这个工作交给了 LayoutManager,LayoutManager中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制岀各种不同排列方式的布局了。
除了 LinearLayoutManager 之外,RecyclerView 还给我们提供了 GridLayoutManager 和 StaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。这里我们来实现一下效果更加炫酷的瀑布流布局。
首先还是来修改一下fruit item.xml中的代码,如下所示:

<?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="wrap_content"
    android:layout_margin="5dp">
    <ImageView
        android:id="@+id/fruitimage"
        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>

这里做了几处小的调整,首先将LinearLayout的宽度由100dp改成了 match _parent,因为 瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。另外我们使用了 layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。还有就是将TextView的对齐属性改成了居左对齐,因为待会我们会将文字的长度变长,如果还是居中显示就会感觉怪怪的。
接着修改MainActivity中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

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)findViewById(R.id.recycler_view);
        StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }
    private void initFruits(){
        for(int i = 0;i < 2; i++){
            Fruit apple = new Fruit(getRandomLengthName("苹果"),R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit(getRandomLengthName("香蕉"),R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit orange = new Fruit(getRandomLengthName("橘子"),R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit watermelon = new Fruit(getRandomLengthName("西瓜"),R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pear = new Fruit(getRandomLengthName("梨子"),R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit grape = new Fruit(getRandomLengthName("葡萄"),R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit pineapple = new Fruit(getRandomLengthName("菠萝"),R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit(getRandomLengthName("草莓"),R.drawable.strawberry_pic);
            fruitList.add(strawberry);
            Fruit cherry = new Fruit(getRandomLengthName("樱桃"),R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit mango = new Fruit(getRandomLengthName("芒果"),R.drawable.mango_pic);
            fruitList.add(mango);
        }
    }
    private String getRandomLengthName(String name){
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for(int i = 0; i < length; i++){
            builder.append(name);
        }
        return builder.toString();
    }
}

首先,在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例。 StaggeredGridLayoutManager的构造函数接收两个参数,第一个参数用于指定布局的列数, 传入3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager. VERTICAL表示会让布局纵向排列,最后再把创建好的实例设置到RecyclerView 当中就可以了,就是这么简单!
没错,仅仅修改了一行代码,我们就已经成功实现瀑布流布局的效果了。不过由于瀑布流布 局需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。这里我们把眼 光聚焦在getRandomLengthName()这个方法上,这个方法使用了 Random对象来创造一个1到 20之间的随机数,然后将参数中传入的字符串重复随机遍。在initFruitsO方法中,每个水果 的名字都改成调用getRandomLengthName ()这个方法来生成,这样就能保证各水果名字的长短差距都比较大,子项的高度也就各不相同了。
现在重新运行一下程序,效果如图所示:
在这里插入图片描述
当然由于水果名字的长度每次都是随机生成的,你运行时的效果肯定和图中还是不一样的。

6.3 RecycleView的点击事件

和ListView 一样,RecyclerView也必须要能影响点击事件才可以,不然的话就没什么实际用 途了。不过不同于ListView的是,RecyclerView并没有提供类似于setOnItemClickListener() 这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件,相比于ListView 来说,实现起来要复杂一些。
那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView。偏偏在点击事件上却没有处理得非常好呢?其实不是这样的,ListView在点击事件上的处理并不人性化, setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的 某一个按钮呢?虽然ListView也是能做到的,但是实现起来就相对比较麻烦了。为此, RecyclerView干脆直接摒弃了子项点击事件的监听器,所有的点击事件都由具体的View去注册, 就再没有这个困扰了。
下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改FruitAdapter中的代码,如下所示:

package com.mxt.firstandroidapplication;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.List;

/**
 * Created by Administrator on 2018-09-18.
 */

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{
    private List<Fruit> mFruitList;
    static class ViewHolder extends RecyclerView.ViewHolder{
        View fruitView;
        ImageView fruitImage;
        TextView fruitName;
        public ViewHolder(View view){
            super(view);
            fruitView = view;
            fruitImage = (ImageView)view.findViewById(R.id.fruitimage);
            fruitName = (TextView)view.findViewById(R.id.fruit_name);
        }
    }
    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }
    @Override
    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.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(v.getContext(),"你点击了文本" + fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        holder.fruitImage.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(v.getContext(),"你点击了图像" + fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        return holder;
    }
    @Override
    public void onBindViewHolder(ViewHolder holder,int position){
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getlmageld());
        holder.fruitName.setText(fruit.getName());
    }
    @Override
    public int getItemCount(){
        return mFruitList.size();
    }
}

我们先是修改了 ViewHolder,在ViewHolder中添加了 fruitView变量来保存子项最外层 布局的实例,然后在onCreateViewHolder()方法中注册点击事件就可以了。这里分别为最外层 布局和ImageView都注册了点击事件,RecyclerView的强大之处也在这里,它可以轻松实现子项 中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的position,然后通过 position拿到相应的Fruit实例,再使用Toast分别弹出两种不同的内容以示区别。
现在重新运行代码,并点击香蕉的图片部分,效果如图所示。可以看到,这时触发了 ImageView的点击事件:
在这里插入图片描述
然后再点击菠萝的文字部分,由于TextView并没有注册点击事件,因此点击文字这个事件会被子项的最外层布局捕获到,效果如图所示:
在这里插入图片描述

7.编写界面的最佳实践

既然已经学习了那么多ui开发的知识,也是时候实战一下了。这次我们要综合运用前面所学的大量内容来编写出一个较为复杂且相当美观的聊天界面

7.1 制作Nine-Patch图片

在实战正式开始之前,我们还需要先学习一下如何制作Nine-Patch图片。你可能之前还没有听说过这个名词,它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。
制作Nine-Patch图片的过程可以参考CSDN的其他文章,这里不再详述(主要是制作过程太长了233)

7.2 编写精美的聊天界面

图片都提供好了之后就可以开始编码了。由于待会我们会用到RecyclerView,因此首先需要在app/build.gradle当中添加依赖库,如下所示:

compile 'com.android.support:recyclerview-v7:24.2.1'

接下来开始编写主界面,修改activity main.xml中的代码,如下所示:

<?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"
    android:background="#d8e0e8">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/msg_revycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
     <EditText
         android:id="@+id/input_text"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:hint="输入一些信息"
         android:maxLines="2"/>
      <Button
          android:id="@+id/send"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="发送"/>
    </LinearLayout>
</LinearLayout>

我们在主界面中放置了一个RecyclerView用于显示聊天的消息内容,又放置了一个EditText 用于输入消息,还放置了一个Button用于发送消息。这里用到的所有属性都是我们之前学过的, 相信你理解起来应该不费力。
然后定义消息的实体类,新建Msg,代码如下所示:

package com.example.administrator.uibestpractice;

/**
 * Created by Administrator on 2018-09-18.
 */

public class Msg {
    public static final int TYPE_RECEIVED = 0;
    public static final int TYPE_SENT = 1;
    private String content;
    private int type;
    public Msg(String content,int type){
        this.content = content;
        this.type = type;
    }
    public String getContent(){
        return content;
    }
    public int getType(){
        return type;
    }
}

Msg类中只有两个字段,content表示消息的内容,type表示消息的类型。其中消息类型有两个值可选,TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发岀的消息。
接着来编写RecyclerView子项的布局,新建msg_item.xml,代码如下所示:

<?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="wrap_content"
    android:padding="10dp">
    <LinearLayout
        android:id="@+id/left_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android: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>

这里我们让收到的消息居左对齐,发出的消息居右对齐,并且分别使用message_left.9.png和 message_right.9.png作为背景图。你可能会有些疑虑,怎么能让收到的消息和发出的消息都放在同一个布局里呢?不用担心,还记得我们前面学过的可见属性吗?只要稍后在代码中根据消息的类型来决定隐藏和显示哪种消息就可以了。
接下来需要创建RecyclerView的适配器类,新建类MsgAdapter,代码如下所示:

package com.example.administrator.uibestpractice;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.List;
/**
 * Created by Administrator on 2018-09-18.
 */
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
    public void onBindViewHolder(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();
    }
}

以上代码你应该非常熟悉了,和我们学习RecyclerView那一节的代码基本是一样的,只不过 在onBindViewHolder()方法中增加了对消息类型的判断。如果这条消息是收到的,则显示左边 的消息布局,如果这条消息是发出的,则显示右边的消息布局。
最后修改MainActivity中的代码,来为RecyclerView初始化一些数据,并给发送按钮加入事件响应,代码如下所示:

package com.example.administrator.uibestpractice;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import java.util.ArrayList;
import java.util.List;

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_revycler_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){
                String content = inputText.getText().toString();
                if(!"".equals(content)){
                    Msg msg = new Msg(content,Msg.TYPE_SENT);
                    msgList.add(msg);
                    adapter.notifyItemInserted(msgList.size() - 1);
                    msgRecyclerView.scrollToPosition(msgList.size() - 1);
                    inputText.setText("");
                }
            }
        });
    }
    private void initMsgs(){
        Msg msg1 = new Msg("在吗?",Msg.TYPE_RECEIVED);
        msgList.add(msg1);
        Msg msg2 = new Msg("在的",Msg.TYPE_SENT);
        msgList.add(msg2);
        Msg msg3 = new Msg("很高兴见到你",Msg.TYPE_RECEIVED);
        msgList.add(msg3);
    }
}

在initMsgs()方法中我们先初始化了几条数据用于在RecyclerView中显示。然后在发送按钮的点击事件里获取了 EditText中的内容,如果内容不为null则创建出一个新的Msg对象,并 把它添加到msgList列表中去。之后又调用了适配器的notifyltemlnserted()方法,用于通知 列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示。接着调用 RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得 到最后发出的一条消息。最后调用EditText的setText ()方法将输入的内容清空。
这样所有的工作就都完成了,终于可以检验一下我们的成果了,运行程序之后你将会看到非常美观的聊天界面,并且可以输入和发送消息,如图所示:
在这里插入图片描述
相信这个例子的实战过程不仅加深了你对本篇博客中所学UI知识的理解,还让你有了如何灵活运用这些知识来设计出优秀界面的思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值