第20章 得心应手的“粘合剂”——适配器模式

第20章 得心应手的“粘合剂”——适配器模式

20.1 适配器模式介绍

适配器模式在我们的开发中使用率极高,从最早的ListView、GridView到现在最新的RecyclerView都需要使用Adapter,并且在开发中我们遇到的优化问题、出错概率较大的地方也基本都出自Adapter,这是一个让人又爱又恨的角色。

说到底,适配器是将两个不兼容的类融合在一起,它有点像粘合剂,将不同的东西通过一种转换使得它们能够协作起来。例如,经常碰到要在两个没有关系的类型之间进行交互,第一个解决方案是修改各自类的接口,但是如果没有源代码或者我们不愿意为了一个应用而修改各自的接口,此时怎么办?这种情况我们往往会使用一个Adapter,在这两种接口之间创建一个“混血儿”接口,这个Adapter会将这两个接口进行兼容,在不修改原有代码的情况下满足需求。

20.2 适配器模式的定义

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作

20.3 适配器模式的使用场景

  • 系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
  • 需要一个统一的输出接口,而输入端的类型不可预知

20.4 适配器模式的UML类图

UML类图如图20-1所示。
第20章 得心应手的“粘合剂”——适配器模式
适配器模式也分两种,即类适配器模式和对象适配器模式,首先学习类适配器模式,结构图如图20-1所示。

如图20-1所示,类适配器是通过实现Target接口以及继承Adaptee类来实现接口转换,例如,目标接口需要的是operation2,但是Adaptee对象只有一个operation3,因此就出现了不兼容的情况。此时通过Adapter实现一个operation2函数,将Adaptee的operation3转换为Target需要的operation2,以此实现兼容。

角色介绍

  • Target:目标角色,也就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因 此目标不可以是类。
  • Adaptee:现在需要适配的接口。
  • Adapter:适配器角色,也是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

20.5 适配器模式应用的简单示例

用电源接口做例子,笔记本电脑的电源一般都是用5V电压,但是我们生活中的电线电压一般都是220V。这个时候就出现了不匹配的状况,在软件开发中我们称为接口不兼容,此时就需要适配器来进行一个接口转换。在软件开发中有一句话正好体现了这点:任何问题都可以加一个中间层来解决。这个层我们可以理解为这里的Adapter层,通过这层来进行一个接口转换就达到了兼容的目的。

在上述电源接口这个示例中,5V电压就是Target接口,220V电压就是Adaptee类,而将电压从220V转换到5V就是Adapter。

20.5.1 类适配器模式

具体程序如下所示。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
Target角色给出了需要的目标接口,而Adaptee类则是需要被转换的对象。Adapter则是将Volt220转换成Target的接口。对应的Target的目标是要获取5V的输出电压,而Adaptee正常输出电压是220V,此时就需要电源适配器类将220V的电压转换为5V电压,解决接口不兼容的问题。
第20章 得心应手的“粘合剂”——适配器模式

20.5.2 对象适配器模式

与类的适配器模式一样,对象的适配器模式把被适配的类的API转换成为目标类的API与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用代理关系连接到Adaptee类,UML图如图20-2所示。
第20章 得心应手的“粘合剂”——适配器模式
从图20-2中可以看出,Adaptee类(Volt220)并没有getVolt5()方法,而客户端则期待这个方法。为使客户端能够使用Adaptee类,需要提供一个包装类Adapter。这个包装类包装了一个Adaptee的实例,从而此包装类能够把Adaptee的API与Target类的API衔接起来。Adapter与Adaptee是委派关系,这决定了适配器模式是对象的。示例代码如下。
第20章 得心应手的“粘合剂”——适配器模式
注意,这里为了节省代码,我们并没有遵循一些面向对象的基本原则。使用示例如下。
第20章 得心应手的“粘合剂”——适配器模式
这种实现方式直接将要被适配的对象传递到Adapter中,使用组合的形式实现接口兼容的效果。 这比类适配器方式更为灵活,它的另一个好处是被适配对象中的方法不会暴露出来,而类适配器由于继承了被适配对象,因此,被适配对象类的函数在Adapter类中也都含有,这使得Adapter类出 现一些奇怪的接口,用户使用成本较高。因此,对象适配器模式更加灵活、实用

在实际开发中Adapter通常应用于进行不兼容的类型转换的场景,还有一种就是输入有无数种情况,但是输出类型是统一的,我们可以通过Adapter返回一个统一的输出,而具体的输入留给用户处理,内部只需知道输出的是符合要求的类型即可。例如ListView的Adapter,用户的Item View各式各样,但最终都是属于View类型,ListView只需要知道getView返回的是一个View即可,具体是什么View类型并不需要ListView关心。而在使用Adapter模式的过程中建议尽量使用对象适配器的实现方式,多用合成或者聚合,少用继承。当然,具体问题具体分析,根据需要来选用实现方式,最适合的才是最好的。

20.6 Android源码中的适配器模式

在开发过程中,ListView的Adapter是我们最为常见的类型之一。我们需要使用Adapter加载每个Item View的布局,并且进行数据绑定等操作。代码大致如下。
第20章 得心应手的“粘合剂”——适配器模式
初学Android的时候我们只知道Google给出来的结果就是这么写的,久而久之我们发现,每次都写这些样板代码非常麻烦,就会产生这样的疑问:ListView为什么要使用Adapter模式呢?

我们知道ListView作为最重要的控件,它需要能够显示各式各样的视图(Item View),每个人需要的显示效果各不相同,显示的数据类型、数量等也千变万化,那么如何应对这种变化成为架构师需要考虑的最重要特性之一。

Android的做法是增加一个Adapter层来隔离变化,将ListView需要的关于Item View接口抽象到Adapter对象中,并且在ListView内部调用Adapter这些接口完成布局等操作。这样只要用户实现了 Adapter的接口,并且将该Adapter设置给ListView,ListView就可以按照用户设定的UI效果、数量、数据来显示每一项数据。ListView最重要的问题是要解决每一项Item视图的输出,Item View千变万化,但终究它都是View类型,Adapter统一将Item View输出为View,这样就很好地应对了Item View的可变性。这虽然有些脱离Adapter模式将不兼容的接口转换为可用接口的使用场景,但也是Adapter模式的一种变种实现。

那么ListView是如何通过Adapter模式来实现应对千变万化的UI效果呢?下面我们一起来一探究竟吧。

我们发现在ListView中并没有Adapter相关的成员变量,其实Adapter在ListView的父类AbsListView中,AbsListView是一个列表控件的抽象,我们看一看这个类。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
AbsListView定义了集合视图的逻辑框架,比如Adapter模式的应用、复用Item View的逻辑、布局子视图的逻辑等,子类只需要覆写特定的方法即可实现集合视图的功能。首先在AbsListView类型的View中添加窗口 (onAttachedToWindow函数)时会调用Adapter中的getCount函数获取到元素的个数,然后在onLayout函数中调用layoutChilden函数对所有子元素进行布局。AbsListView 中并没有实现layoutChilden这个函数,具体的实现在子类中,这里我们分析ListView中的实现,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
ListView覆写了AbsListView中的layoutChilden函数,在该函数中根据布局模式来布局Item View,例如,默认情况是从上到下开始布局,但是,也有从下到上开始布局的,例如QQ聊天窗口的气泡布局,最新的消息就会布局到窗口的最底部。我们看看这两种实现。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在每一种布局方式的函数中都会从makeAndAddView函数获取一个View,这个View就是ListView的每一项的视图,这里有一个pos参数,也就是对应这个View是ListView中的第几项。 我们看看makeAndAddView中的实现。
第20章 得心应手的“粘合剂”——适配器模式
在makeAndAddView函数中主要分为两个步骤,第一是根据position获取一个item View,然后将这个View布局到特定的位置。获取一个item View调用的是obatinView函数,该函数在AbsListView中。核心代码如下。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
obatinView函数定义了列表控件的Item View的复用逻辑,首先会从RecyclerBin中获取一个缓存的View,如果有缓存则将这个缓存的View传递到Adapter的getView第二个参数中,这也就是我们对Adapter的最常见的优化方式,即判断getView的convertView是否为空,如果为空则从xml中创建视图,否则使用缓存的View。这样避免了每次都从xml加载布局的消耗,能够显著提升ListView等列表控件的效率。通常的实现如下。
第20章 得心应手的“粘合剂”——适配器模式
通过这种缓存机制,即使有成千上万的数据项,ListView也能够流畅运行,因此,只有填满一屏所需的Item View存在内存中。ListView根据Adapter设置的数据项数量循环调用getView方法获取视图,第一次加载填满屏幕的数据项时getView的第二个参数convertView都为空, 此时每次都需要从xml中加载布局文件,填充数据之后返回给ListView。当整屏的数据项加载完毕之后用户向下滚动屏幕,此时item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个视图,此时item1被缓存起来,在下一项数据加载时传递给getView的第二个参数convertView,因此,convertView此时不是空值,它的值是item1。此时只需设定新的数据然后返回convertView,这样就避免了每次都从xml加载、初始化视图,减少了时间、性能上的消耗。原理如图20-3所示。

了解了它的工作原理后,就可以重复利用ListView的Item View,只要convertView不为空就直接使用,改变它绑定的数据就行了。当然,由于视图被缓存了,视图中的数据也会被缓存,因此,你需要在每次获取到了Item view时对每个数据项重新赋值,否则会出现数据错误的现象。
第20章 得心应手的“粘合剂”——适配器模式
我们最后总结一下这个过程。

ListView等集合控件通过Adapter来获取Item View的数量、布局、数据等,在这里最为重要的就是getView函数,这个函数返回一个View类型的对象,也就是Item View。由于它返回的是一个View抽象,而千变万化的UI视图都是View的子类,通过依赖抽象这个简单的原则和Adaper模式就将Item View的变化隔离了,保证了AbsListView类族的高度可定制化。在获取了View之后,将这些View通过特定的布局方式设置到对应的位置上,再加上Item View的复用机制,整个ListView 就运转起来了。

当然,这里的Adapter并不是经典的适配器模式,却是对象适配器模式的优秀示例,也很好地体现了面向对象的一些基本原则。这里的Target角色就是View,Adapter角色就是将Item View输出为View抽象的角色,Adaptee就是需要被处理的Item View。通过增加Adapter一层来将Item View的操作抽象起来,ListView等集合视图通过Adapter对象获得Item的个数、数据、Item View等,从而达到适配各种数据、各种Item视图的效果。因为Item View和数据类型千变万化,Android的架构师们将这些变化的部分交给用户来处理,通过getCount、getltem、getView等几个方法抽象出来,也就是将Item View的构造过程交给用户来处理,灵活地运用了适配器模式,达到了无限适配、拥抱变化的目的。

仔细一想,这个Adapter似乎并不是适配器模式,因为它缺少了适配器模式中最重要的对象转换语义,你可能会发现它似乎更像抽象工厂模式,getView、getltem等对应工厂中构建对象的函数,具体的抽象工厂类来构建具体的View、Item对象。然而不同的人有不同的看法,不管适配器还是抽象工厂,它设计思路、原则才是我们最需要关注的。

20.7 深度扩展

虽然ListView已经足够强大了,但是还存在一些问题,例如,每次都需要自己创建一个ViewHolder、手动判断是否有缓存View等。对于用户而言,这些都还可以改进,因此Google推出了一个更强大的控件RecyclerView。顾名思义,这个View代表的就是一个可循环使用的视图集合控件,它定义了ViewHolder类型标准,封装了View缓存判断逻辑,更强大的是它可以通过一个LayoutManager将一个RecyclerView显示为不同的样式,例如ListView、GridView形式、瀑布流形式。光是这些就足以促使我们好好学习这个控件,它将是未来几年在Android开发中取代ListView作为最重要的控件。

开发技术前线网站的Android客户端就是使用了RecyclerView作为显示文章列表的控件,如图20-4所示。
第20章 得心应手的“粘合剂”——适配器模式
这个界面很简单,每个文章列表就是几个普通的TextView组合而已。我们看看在主界面的布局xml中定义RecyclerView的代码如下。
第20章 得心应手的“粘合剂”——适配器模式
然后在Activity中进行初始化,除了类似ListView的Adapter之外,RecyclerView还有一个额外的设置就是设置布局方式,这也是RecyclerView能够在布局上比ListView更为灵活的原因。因为它将布局通过组合的形式交由LayoutManager实现,而不是像ListView、GridView这些类自己实现,因为这会导致一个类只能有一种布局方式,当用户需要实现其他布局效果时就需要替换整个实现类。而通过组合模式,将布局这个职责分离开来,使得一个RecyclerView与不同的LayoutManager搭配就能做成各种布局效果,这也是用组合代替继承的优秀案例。

我们看看图20-4的文章列表的实现代码,看看Activity中设置RecyclerView的代码。
第20章 得心应手的“粘合剂”——适配器模式
上述代码较为简单,就是一个普通的初始化过程,唯一与ListView不太相同的是第二步中设置了 LayoutManager,这个LayoutManager的职责就是负责RecylerView元素的布局。默认的实现有 LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,分别是线性布局、网格布局和瀑布流布局。这些布局不仅可以竖向布局,还支持横向布局,这几个布局已经能够基本满足我们的需求了,可见RecylerView的高度定制化能力。

下面看看文章Adapter的实现。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在RecyclerView中没有了getView函数,取而代之的是onBindViewHolder和onCreateViewHolder,这两个函数就相当于getView功能。首先会从onCreateViewHolder中获取ViewHolder对象,ViewHolder中又含有Item View的引用,获取到ViewHolder之后又会通过onBindViewHolder函数来绑定数据,这两个过程实际上是将getView函数分解开来,使得加载视图和绑定数据分离开来。在RecyclerView中的操作单位也不再是View,而是ViewHolder。Android中定义了一个Adapter基类,该类的一个泛型参数就是ViewHolder,用户需要继承ViewHolder实现自己的ViewHolder,对于Item view的操作都从这个ViewHolder对象进行。通过onBindViewHolder和onCreateViewHolder两个函数,Android屏蔽了对Item View缓存的手动判断,将这部分逻辑封装在RecyclerView中,这样就不需要像ListView中的Adapter的getView函数那样对convertView进 行判空等操作,简化了逻辑,隐藏了具体实现。

最后将数据设置给Adapter,再将Adapter设置给RecyclerView,RecyclerView再根据用户设置的LayoutManager将每个数据项布局到对应的位置,这样一来整个列表控件就显示在我们眼前了。

Android通过onBindViewHoider和onCreateViewHolder两个函数屏蔽了一些固定的逻辑,使得Adapter的使用更为简单,又通过LayoutManager实现具体的布局,使得RecyclerView具有更强大的定制能力,这就是RecyclerView比ListView更强大的原因所在。

如果我们看腻了如图20-4所示的单向列表效果,想要换成每行3篇文章的网格形式的布局, 此时只需要将LieanrLayoutManager替换为一个GridLayouManager即可。具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
效果图如图20-5所示。
第20章 得心应手的“粘合剂”——适配器模式
又如果你觉得高度一样的网格布局还是不好看,你想要高度不规则的瀑布流布局,那么此时需要做的同样也很简单。修改LayoutManager为StaggeredGridLayoutManager,然后在Adapter中为每个Item设置一个高度即可。具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
还需要做的是在Adapter中设置每个Item的高度,例如。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
此时的效果图如图20-6所示。
第20章 得心应手的“粘合剂”——适配器模式
如图20-6所示,通过替换LayoutManager和添加几行代码瀑布流效果就呈现在我们面前了! RecyclerView的功能真是非常强大,它的设计也是值得我们好好学习的示例。

不过,RecyclerView也有几个小问题需要注意,第一个是它没有设置处理Item点击事件的Listener接口,例如ListView可以通过setOnltemClickListener,但是,在RecyclerView中并没有这个接口。如果要给Item设置点击事件,那么需要通过Adapter实现,通常的做法是定义一个点击接口,然后设置给Adapter,最后在这个View的点击事件中触发这个类似于OnltemClickListener的回调函数。示例程序如下。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
第二个问题是,RecyclerView没有可以设置Header和footer的接口,当需要实现HeaderView时,需要根据viewType进行分类处理。例如当position为0时,你把viewType设置为0,其他位置的Item的viewType则为1,也就是说第一个item是Header,其他的是普通的Item。因此,需要在position为0时返回HeaderView的ViewHolder,而在其他位置时返回普通item的ViewHolder。并且需要注意的是,item count需要比数据集的数量多1,这个1就是这个HeaderView,另外,从数据集取数据时则需要把position减1。因为第一个View为Header,Header通常情况下并不使用数据集中的数据,也就是说第一个item不需要数据,需要数据的是第二个Item View,而此时它显示的数据应该是数据集中的第一个,因此取数据时需要把索引减1。示例代码如下。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
上述代码中有一个getViewType函数,在该函数中把第一个item的viewType设置为0,其他位置的设置为1。这样在onCreateViewHolder函数中就可以根据不同的viewType(onCreateViewHolder的第二个参数,不是positiont而是viewType)来创建不同的ViewHolder,不同的ViewHolder含有不同的View,因此达到了不同显示效果的目的。上文中的开发技术前线网站的第一项就是一个HeaderView,这个HeaderView是一个自动滚动的ViewPager,其他的都是普通的文本控件的组合视图。

RecyclerView很强大,但也有些不足,例如上述提到的两点。但是,对于HeaderView的设置来说,似乎又没有很好的解决方案。如果通过像ListView那样的形式添加header View也经常会出现问题,例如添加header view在setAdapter之后,导致header不显示的问题。而RecyclerView的这种做法显然问题更为明显,当然它的定制性也更高,至于Android工程师这样设计可能有另外的考虑。总的来说,还是开头的那句话,RecyclerView是个强大的、值得学习的控件!

RecyclerView设计与实现

在上一节中我们学习了RecyclerView的一些基本使用,本章我们来学习一下RecyclerView的设计与实现,以及它与ListView等组件有什么比较明显的区别。

对于RecyclerView和ListView来说,比较相同的一点是使用了Adapter和观察者模式,相关的代码如下。
第20章 得心应手的“粘合剂”——适配器模式
在用setAdapter时最终也会注册一个观察者,这个观察者具体实现类是RecyclerView的内部类RecyclerViewDataObserver,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
在数据集发生变化且调用了Adapter的notifyDataSetChanged之后就会调用RecyclerViewDataObserver的onChanged函数,在该函数中又会调用RecyclerView的requestLayout函数进行重新布局。这些过程与ListView的实现基本一致,最大的不同在于它们之间的布局实现上。在ListView中的布局是通过自身的layoutChilden函数来实现,而对于RecyclerView来说它的布局职责则是交给了LayoutManager对象。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在设置了布局管理器之后就会调用requestLayout函数进行布局,然后会调用onLayout函数,我们看看相关的实现。
第20章 得心应手的“粘合剂”——适配器模式
在onLayout函数中最终会调用dispatchLayout函数,而在dispatchLayout函数中又会调用LayoutManager的onLayoutChilden函数进行布局。在此,我们以LinearLayoutManager为例进行学习。下面看看LinearLayoutManager中的onLayoutChilden函数。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在onLayoutChilden函数中会调用fill函数,在fill函数中又会循环地调用layoutChunk函数进行布局,每次布局完之后就会计算当前屏幕剩余的可利用空间,并且做出判断是否还需要布局ItemView。因此,先看看layoutChunk的实现。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在layoutChunk中首先从layoutstate中获取到ItemView,然后获取ItemView的布局参数、尺寸信息,并且根据布局方式(横向或者纵向)计算出ItemView的上下左右坐标,最后调用layoutDecorated函数实现布局。layoutDecorated函数定义在LayoutManager中,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
从上述程序中可以看到,只是调用了ItemView的layout函数将ItemView布局到具体的位置。这样一来,就将布局的职责从RecyclerView分离到LayoutManager中,也使得RecyclerView更为灵活。

这里的layoutChunk函数很重要,我们看注释1处,首先通过LayoutState对象的next函数获取到ItemView,这里也是一个重要的地方。我们看看LayoutState函数的next实现。
第20章 得心应手的“粘合剂”——适配器模式
实际上就是调用RecyclerView.Recycler对象getViewForPosition函数获取到ItemView,我们继续深入RecyclerView.Recycler类的相关代码。
第20章 得心应手的“粘合剂”——适配器模式
我们知道在RecyclerView的Adapter中被缓存的单位已经不是ItemView了,而是一个ViewHolder,而原来的 ListView则是缓存的View。在RecyclerView.Recycler类中有mAttachedScrap、mChangedScrap、 mCachedViews几个ViewHolder列表对象,它们就是用于缓存ViewHolder的。在通过LayoutState的next函数获取ItemView时,实际上调用的是RecyclerView.Recycler的getViewForPosition函数,该函数首先会从这几个ViewHolder缓存中获取对应位置的ViewHolder,如果没有缓存则调用RecyclerView.Adapter中的 createViewHolder函数来创建一个新的ViewHolder,我们看看RecyclerView.Adapter的createViewHolder 函数。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
在createViewHolder函数中实际上调用了onCreateViewHolder函数创建ViewHolder对象。这也就是为什么在继承RecyclerView.Adapter时需要覆写onCreateViewHolder函数,并且在该函数中返回ViewHolder的原因。通过这个onCreateViewHolder函数会加载ItemView视图,并且把ItemView当作ViewHolder的构造参数传递给ViewHolder,此时ViewHolder就构建完毕了。

调用了Adapter的createViewHolder后,此时执行到Recycler的getViewForPosition函数的注释4处,也就是调用了Adapter中的bindViewHolder函数,相关代码如下。
第20章 得心应手的“粘合剂”——适配器模式
bindViewHolder函数与createViewHolder如出一辙,只是它调用的是onBindViewHolder函数而己。与onCreateViewHolder一样,onBindViewHolder也需要子类覆写,并且在这个函数中进行数据绑定。在getViewForPosition中实际上相当于一个模板方法,它封装了获取、绑定ViewHolder的过程,子类只需要覆写特定的函数即可完成这个过程。

执行完onBindViewHolder函数之后数据就被绑定到ItemView上了。

我们最后再来分析一下这个过程。

与ListView—样,RecyclerView还是通过Adapter和观察者模式进行数据绑定,使得RecyclerView的灵活性得到保证。RecyclerView的Adapter并不是ListView中的Adapter,它是一个封装了ViewHolder的创建与绑定等逻辑,降低了用户的使用成本。RecyclerView也将缓存单元从ItemView换成了ViewHoler,在一定程度上建立起了规范。RecyclerView与ListView最大的不同是RecyclerView将布局的工作交给了LayoutManager,在LayoutManager的onLayoutChilden中对ItemView进行布局、执行动画等操作,这样一来,使得RecyclerView可以动态地替换掉布局方式,例如,在运行时给RecyclerView重新设置一个LayoutManager就可以将原来是线性布局的视图改成网格布局,这大大地增加了灵活性。将布局职责独立出来也符合单一职责原则,而使用组合代替继承也会减少耦合、增强健壮性,也使得RecyclerView的布局具有更好的扩展性。

20.8 实战演示

对于很多开发人员来说,炫酷的UI效果是最吸引他们注意力的,很多人也因为这些炫酷的效果而去学习一些比较知名的UI库。而做出炫酷效果的前提是必须对自定义View有所理解,小民自然也不例外。特别对于刚处在开发初期的小民,对于自定义View这件事觉得又神秘又酷,于是小民决定深入研究自定义View以及相关的知识点。

小民的同事洋叔是一位资深的研发人员,擅长写UI特效,在开发领域知名度颇高。最近洋叔刚发布了一个效果不错的圆形菜单,这个菜单的每个Item环形排布,并且可以转动。小民决定仿照洋叔的效果实现一遍,但是,对于小民这个阶段来说只要实现环形布局就不错了,转动部分作为下个版本功能,就当作自定义View的练习了。

在了解了自定义View相关的知识点之后,小民就写好了这个圆形菜单布局视图,我们一步一步来讲解,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
小民的思路大致是这样的,首先让用户通过setMenuItemlconsAndTexts函数将菜单项的图标和文本传递进来,根据这些图标和文本构建菜单项,菜单项的布局视图由mMenuItemLayoutld存储起来,这个 mMenuItemLayoutld默认为circle_menu_item.xml,这个xml布局为一个ImageView显示在一个文本控件的上面。为了菜单项的可定制性,小民还添加了一个setMenuItemLayoutld函数让用户可以设置菜单项的布局,希望用户可以定制各种各样的菜单样式。在用户设置了菜单项的相关数据之后,小民会根据用户设置进来的图标和文本数量来构建、初始化相等数量的菜形菜单CircleMenuLayout中。然后添加了一个可以设置用户点击菜单项的处理接口的setOnltemClickListener函数,使得菜单的点击事件可以被用户自定义处理。

在将菜单项添加到CircleMenuLayout之后就是要对这些菜单项进行尺寸丈量和布局了,我们先来看丈量尺寸的代码,具体如下。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
代码比较简单,就是先测量CircleMenuLayout的尺寸,然后测量每个菜单项的尺寸。尺寸获取了之后就到了布局这一步,这也是整个形菜单的核心所在。具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
onLayout函数看起来稍显复杂,但它的含义就是将所有菜单项按照圆弧的形式布局。整个圆为360°,如果每个菜单项占用的角度为60°,第一个菜单项的角度为0°〜60°,那么第二个菜单项的位置,计算公式的图形化表示如图20-7所示。第20章 得心应手的“粘合剂”——适配器模式
图20-7右下角那个小就是我们的菜单项,那么它的left坐标就是mRadius/2+tmpcoas,top坐标则是 mRadius/2+tmpsina。这里的tmp就是我们代码中的distanceFromCenter变量。到了这一步之后小民的第一版形菜单算是完成了。

下面就来集成一下这个圆形菜单。

创建一个工程之后,首先在布局xml中添加形菜单控件,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
为了更好地显示效果,在布局xml文件中为圆形菜单的上一层以及圆形菜单,本书都添加了一个背景图。然后在MainActivity中设置菜单项数据以及点击事件等。代码如下所示。
第20章 得心应手的“粘合剂”——适配器模式
第20章 得心应手的“粘合剂”——适配器模式
效果如图20-8和图20-9所示。
第20章 得心应手的“粘合剂”——适配器模式
小民得意洋洋地蹦出了两个字:真酷!同时也为自己的学习能力感到骄傲,脸上写满了满足与自豪,感觉自己又朝高级工程师迈近了一步。

“这不是洋叔写的圆形菜单嘛,小民也下载了?”正准备下班的主管看到这个UI效果问清情况。小民只好把其中的缘由、实现方式一一说给主管听,小民还特地强调了CircIeMenuLayout的可定制性,通过setMenuItemLayoutld函数设置菜单项的布局id,这样菜单项的UI效果就可以被用户定制化了。主管扫视了小民的代码,似乎察觉出了什么。于是转身找来还在埋头研究代码的洋叔,并且把小民的实现简单介绍了一遍,洋叔老师在扫视了一遍代码之后就发现了其中的问题所在。

"小民,你刚才说用户通过setMenuItemLayoutld函数可以设定菜单项的UI效果。那么问题来了,在你的CircIeMenuLayout中默认实现的是circle_menu_item.xml的逻辑,比如加载菜单项布局之后会通过findViewByld找到布局中的各个子视图,并且进行数据绑定。例如设置图标和文字,但这是针对circle_menu_item.xml这个布局的具体实现。如果用户设置菜单项布局为other_menu_item.xml,并且每个菜单项修改为一个Button,那么,此时必须修改CircIeMenuLayout中初始化菜单项的代码。因为布局变了,菜单项里面的子View类型也变化了,菜单需要的数据也发生了变化。 例如菜单项不再需要图标,只需要文字。这样一来,用户每换一种菜单样式就需要修改一次CircleMenuLayout类一,并且设置菜单数据的接口也需要改变。这样就没有定制性可言了,而且明显违反了开闭原则。反复对CircleMenuLayout进行修改不免会引入各种各样的问题……”洋叔老师果然一针见血,小民这才发现了问题所在,于是请教洋叔应该如何处理比较合适。

"这种情况你应该使用Adapter,就像ListView中的Adapter一样,让用户来自定义菜单项的布局、解析、数据绑定等工作,你需要知道的仅仅是每个菜单项都是一个View。这样一来就将变化通过Adapter层隔离出去,你依赖的只是Adapter这个抽象。每个用户可以有不同的实现,你只需要实现圆形菜单的丈量、布局工作即可。这样就可以拥抱变化,可定制性就得到了保证。当然,你可以提供一个默认的Adapter,也就是使用你的circle_menu_item.xml布局实现的菜单,这样没有定制需求的用户就可以使用这个默认的实现了。”小民频频点头,屡屡称是。“这确实是我之前没有考虑好,也是经验确实不足,我再好好重构一下。”小民发现问题之后也承认了自己的不足,两位前辈看小民这么好学就陪着小民一起重构代码。在两位前辈的指点下,经过不到五分钟重构,小民的CircleMenuLayout成了下面这样。
第20章 得心应手的“粘合剂”——适配器模式
现在的CircleMenuLayout把解析xml、初始化菜单项的具体工作移除,添加了一个Adapter,在用户设置了 Adapter之后,在onAttachedToWindow函数中调用Adapter的getCount函数获取菜单项的数量,然后通过getView函数获取每个View,最后将这些菜单项的View添加到圆形菜单中,圆形菜单布局再将它们布局到特定的位置即可。

我们看现在使用CircleMenuLayout是怎样的形式。首先定义了一个实体类Menuitem来存储菜单项图标和文本的信息,具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
然后再实现一个Adapter,这个Adapter的类型就是ListAdapter。我们需要在getView中加载菜单项xml、绑定数据等,相关代码如下。
第20章 得心应手的“粘合剂”——适配器模式
这与我们在ListView中使用Adapter是一致的,实现getViewgetCount等函数,在getView中加载每一项的布局文件,并且绑定数据等。最终将菜单View返回,然后这个View就会被添加到CircleMenuLayout中。这一步的操作原来是放在CircleMenuLayout中的,现在被独立出来,并且通过Adapter进行了隔离。这样就将易变的部分通过Adapter抽象隔离开来,即使用户有成千上万种菜单项UI效果,那么通过Adapter就可以很容易地进行扩展、实现,而不需要每次都修改CircleMenuLayout中的代码。CircleMenuLayout布局类相当于提供了一个圆形布局抽象,至于每一个子View是啥样的它并不关心。通过Adapter隔离变化,拥抱变化,就是这么简单。

“原来ListView、RecyclerView通过一个Adapter就是这个原因,通过Adapter将易变的部分独立出去交给用户处理。又通过观察者模式将数据和UI解耦合,使得View与数据没有依赖,一份数据可以作用于多个UI,应对UI的易变性。原来如此!”小民最后总结道。

例如,当我们的产品发生变化,需要将圆形菜单修改为普通的ListView样式,那么要做的事很简单,就是将xml布局中的CircleMenuLayout修改为ListView,然后将Adapter设置给ListView即可。具体代码如下。
第20章 得心应手的“粘合剂”——适配器模式
这样就完成了 UI替换,成本很低,也基本不会引发其他错误。这也就是为什么我们在CircleMenuLayout中要使用ListAdapter的原因,就是为了与现有的ListView、GridView等组件进行兼容,当然我们也没有必要重新再定义一个Adapter类型。替换为ListView的效果如图12-10所示。
第20章 得心应手的“粘合剂”——适配器模式
小民在重构完CircleMenuLayout之后深感收获颇多,为了报答主管和洋叔的指点,嚷嚷着要请他们吃饭…

20.9 小结

Adapter模式的经典实现在于将原本不兼容的接口融合在一起,使之能够很好地进行合作。但是,在实际开发中,Adapter模式也有一些灵活的实现。例如ListView中的隔离变化,使得整个UI架构变得更灵活,能够拥抱变化。Adapter模式在开发中运用非常广泛,因此,掌握Adapter模式是非常必要的。

优点

  • 更好的复用性
    系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。
  • 更好的扩展性
    在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。

缺点

  • 过多地使用适配器,会让系统非常零乱,不易整体把握。例如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果出现太多这种情况,无异于一场灾难。因此,如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
上一篇:flutter中ListView的详细讲解


下一篇:ListView小常识