这是一个参考Android Architecture Components中的架构设计方案,UI 上尽可能符合Material Design的小项目。
项目架构
由于这是一个比较完整的一个项目,因此在开发之前,就需要定下明确的架构方案。
在本次项目中,我们参考的是Android Architecture Components,是谷歌在 Google I/O 2017 发布一套帮助开发者解决 Android 架构设计的方案,也是这是谷歌官方指南中推荐的架构。
这个架构由负责生命周期的Lifecycle和数据库的Room等组件组成,我们的架构在这里使用了ObjectBox代替了Room,并且使用了Retrofit来处理网络数据,使用Dagger2来实现依赖注入。
整体架构图:

项目结构:
1 | . |
UI 与 交互
我在这个项目里面主要负责是UI 的设计和布局以及交互逻辑的处理。
设计准备
一款应用,UI 是用户感受最直观的东西,UI 的好坏决定了用户对这个应用的第一印象,是一个应用中仅次于功能的重要部分。而应用的交互逻辑直接决定了用户的使用体验,因此这一部分也同样重要。
页面
因此,在布局之前先做一个大概的设计初稿是十分有必要的,有了基本的布局和架构,才能更方便设计和延伸细节
最初版设计图:

颜色
为了统一 UI,主题色也是十分重要的,因此还需要找出最适合应用主题的颜色,最后我们定下了以褐色和黄色为主题(取自于王者荣耀 LOGO 颜色),整个应用以浅色为主。
颜色设计图(最后选择了最后一种):

风格
本项目基本上遵循了Google的Material Design设计规范,使用到不少Android Material Component中提供的控件,尽可能和 Android 和风格融合在一起。
界面
这个应用是一个内容型的应用,如何把内容展示出来是一个十分重要的问题。
这里一共把界面分成五大部分:
主菜单
- 用户进入应用的界面,也是用户选择查看内容的入口。必须要简明地展示出应用存在哪些内容,有哪些功能
搜索
- 综合应用的所有内容,如果说主菜单是一个大分类的入口,那么搜索就是一个统一的入口,可以一步到位找到需要查找的内容。
列表(英雄、装备、技能)
- 列表主要分成三大类,和官网的资料站提供的资料一致,而列表里面也可以对内容进行筛选。
收藏
- 用户收藏关注的内容,在收藏中用户可以添加自己的备注(因为我们的应用的资源全部都来自官方网站的 API,不可能实现增加或者删除英雄,只好做一个可以增删改的收藏)
英雄详情
- 装备和技能由于内容比较少,使用弹出窗口来展示内容。而英雄详情的数据就需要一个专门的界面来展示了。
主菜单
主菜单总体使用约束布局ConstraintLayout。
顶部的的Logo是一张SVG图片,然后使用drawable中的shape和gradient画出了一个渐变的背景
1 |
|
由于菜单项目比较少,而且考虑到自定义性比较高,因此直接根据约束进行布局,并没有使用ListView。
点击不同的菜单跳转到不同的页面。
约束布局可以很好地保证在不同分辨率的设备上,都可以保持大概的布局而不发生改变。事实上这里的菜单项也会根据屏幕的大小自适应地调整之间的间距。
而搜索框看上去是一个EditText,但实际上是一个带Icon的MaterialButton。点击后会跳转到搜索界面。
布局:

最终实际效果:

搜索
搜索这里主要使用到一个MaterialSearchView的控件,但是这个库的实现说暴露的接口比较少,无法高度定制,不能满足我们原来的设计需求。因此我们修改了他的部分源码,加上了我们所需要的功能。
首先是加入了默认的搜索选项和历史记录,这个是他原来没有提供的。
然后对于不同的搜索结果,根据他们的类型展示不同的图标。

这个提示列表本质上就是一个ListView。因此,修改他原来的adapter就可以实现我们自定义的功能。
然后中间是一个提示的文字,显示一些提示信息(如无搜索结果)
搜索结果使用RecyclerView实现
右边的标签表示了他们的不同的属性。

应用界面的数据都是来自一个统一的ViewModel,从这个ViewModel中我们可以获取所有的数据。但是这些数据是从三个不同的数据库中获取的,因此需要查遍三个数据库,把和关键词有关的项加入数据源中,展示出来。
他们的点击事件,则调用其他页面现成的函数,对于装备和技能就直接用对话框展示出来,对于英雄则跳转到详情界面

布局:

列表
列表是这个应用最核心的一个界面,因为很多内容都依赖于列表来展示。
布局复用
这个列表页面时重复使用在三个地方:英雄列表,装备列表和技能列表。
在这个Activity加载的时候,根据Intent中的参数展示不同的内容
1 | ListHandler(ListActivity activity, ListActivityBinding binding, int type) { |
虽然则三个页面使用的都是同一个布局,但是他们的数据和部分逻辑是单独的,因此我设计了一个接口
1 | public interface ListTypeHandler { |
然后把他们了不同的逻辑写在不同的Handler里面

最终由ListHandler负责共同部分逻辑的界面的处理,然后调用他们单独的Handler实现来展示不同的内容 。
对于列表,由于三种界面的数据是不一样的,因此如果想要使用同一个Adapter的话,必须具有一种统一的数据结构,这里同样也使用了接口
1 | public interface ListItem { |
由于列表的显示需要显示图片和名字,图片可以根据 ID 从 ViewModel中获取,而所有的Item都可以获取其id和名字name,因此使用了接口实现。

1 | void addData(List<? extends ListItem> item) { |
然后Adapter的函数就需要List<? extends ListItem>类型的参数,可以接受一切实现了ListItem接口的List数据。
在bind的时候就可以根据
1 | if (item instanceof HeroListItem) { |
来判断具体的类型再做进一步的细化操作。
可收缩标题栏
列表的顶端使用了Android Material Component中的CollapsingToolbarLayout。
这个控件具有两种形态:
在展开形态,标题默认显示在图片的左下角
在收缩形态,标题显示在标题栏上
而这两个形态之间,标题的颜色、大小和位置会随着用户的滑动而进行过渡。
其基本的使用方法如下:
1 | <android.support.design.widget.CoordinatorLayout |
外部是一个CoordinatorLayout是一个作为协调子 View 之间交互的容器。
使用这个控件,可以实现不同层面的视差滚动效果,只需要设置CollapsingToolbarLayout里面的元素的layout_collapseMode为parallax,就可以实现视差效果。具体来说就是图片的滚动和其他内容(如列表)的滚动速度不一致,我这里把视差滚动的因子调成0.6,在实际上可以看到图片的滚动比列表要慢上一些,有一种图片在比较远的位置,有深度的效果。
动态效果:

这个有一个需要主要的点就是我们需要把响应滚动事件的控件(如下面的 RecyclerView)
加上app:layout_behavior="@string/appbar_scrolling_view_behavior"这个属性,不然只能按着上面的标题栏进行滚动,交互效果就差上了很多。
细心的朋友可能会发现,当状态栏处于收缩状态的时候,浮动按钮消失了,而菜单出现了。这是通过监听AppBar的滚动事件实现的,对完全收缩、完成展开、中间部分三个阶段对于控件可见属性设置。
1 | binding.listAppbar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { |
至于menuItem的设置,也就是菜单栏的设置,可以在Activity里重写onCreateOptionsMenu函数创建
1 |
|
在res的menu中创建一个xml描述菜单项
1 |
|
适配状态栏
由于这个界面的顶部是图片,因此,如果状态栏还是一条色块就会显示得很难看。因此这里加入了状态栏的适配。
首先创建一个sytle
1 | <style name="TransBar" parent="AppTheme"> |
加入到AndroidMainfest.xml指定的activity里面
1 | <activity |
然后对于延伸到状态栏上面的控件,需要一层一层在布局加入android:fitsSystemWindows="true"这个属性。
上面的布局代码可以看到,从CoordinatorLayout一直到ImageView,都加上了这个属性。
下拉刷新
对于英雄列表,我们还实现了下拉刷新。因为数据都是来自于网络的,第一次访问会检查本地数据库是否存在数据,如果不存在则从使用 API 获取,如果本地存在则直接从本地获取,而本地的数据会在一定时候后过期。那么如果用户想要获取最新的数据就需要手动进行刷新。
拖住列表向下拉就可以手动刷新
这里主要使用了一个SmartRefreshLayout的控件,直接包在RecyclerView外部,就可以实现下拉刷新效果,使用起来极其简单。
效果图:

列表动画
对于RecyclerView的动画效果,这里使用了recyclerview-animators这个库
使用起来也非常简单:
1 | // 列表动画 |
上面的动画可以给每个具体的Item的进入和离开设置动画,下面的动画可以给没有加载的Item设置加载动画,比如向下滑动的时候就会显示。
需要注意的是,这些动画必须和RecyclerView.Adapter中的 notify等函数配合使用,通知那些数据被添加或者删除,动画才会显示出来。
比如刷新数据
1 | // 刷新数据 |
筛选
英雄和装备都实现了各自的筛选功能,通过接口由ListHandler统一调用。
筛选是通过BottomSheetDialogFragment实现的,也是Material Design中的一个控件,其本质就是一个DialogFragment,只不过是从界面的底部弹出,并且具有默认动画效果。
1 | this.sheet = new ItemSheetFragment((sheetBinding) -> { |
其用法和DialogFragment一样,通过show方法显示。
英雄的筛选分为 8 大类,而装备的筛选分为 10 大类

对于选中的部分,筛选界面会高亮显示

这里的分类标志是从官方资料站上面下载下来的一张图片,我们通过设置颜色滤镜,使用SRC_ATOP模式在原来的颜色上面叠加上主题颜色,通过setColorFilter使其显示高亮状态
1 | private void setImageFilter(int sort) { |
筛选的本身就是一个过滤的过程,将从ViewMode获取的所有数据的类型属性和当前类型做对比,如果符合就加入RecyclerView的数据源当中。
1 | binding.listCollapsingToolbar.setTitle(title + "装备"); |
布局:

弹出窗口
由于装备和技能的内容比较少,因此没有必要为他们专门装备一个界面,只需要一个DialogFragment就足够了。
装备:

召唤师技能:

这两个弹出窗口使用的都是同一个布局,而且都是DialogFragment实现的,而且都实现了一个静态的方法,在任何界面只需要传入数据就可以显示出这个弹出窗口。
他们的布局也比较简单,最外面一层MaterialCardView,用于实现卡片的显示效果,是Material Design的一个控件。通过设置不同控件(如图片、金钱)的可见性来呈现不同的显示效果。
布局:

调用方法:
1 | final CardFragment fragment = new CardFragment(activity); |
只需要生成一个Fragment实例,然后通过FragmentManager调用他的show方法就可以显示出来。
由于直接弹出框会显得特别生硬,因此我们可以为他加入一些动画。
首先在value-anim下使用xml编写进入和退出的动画
1 | <!-- in --> |
这里我使用的是缩放的动画,然后添加到里面
1 |
|
只需要在DialogFragment创建的时候指定窗口的动画即可
动画效果:

收藏
收藏的布局和列表差别不大,都是采用同样的结构,保持二级界面的 UI 的统一性。
侧滑列表项
需要特别关注的是这里的收藏的数据采用了LiveDate<>作为数据源,会在数据发生变化的时候通知订阅者,是一个简化版的RxJava.当收藏发生变化的时候,就会通知列表对数据进行更新。
这里使用到一个比较特殊的控件就是SwipeLayout,是一个可以左右滑动的控件:


通过左右滑动可以显示不同的界面,在这里这两个界面是两个按钮。向左滑动可以调出添加/修改备注的功能,向右滑动可以调出删除收藏的按钮。
使用起来也不难,首先是布局
1 | <com.daimajia.swipe.SwipeLayout |
SwipLayout会将其最右一个子布局作为顶层布局显示,而上面的所有子布局都会被当作底层布局。
由于这个布局是RecyclerView的Item的布局,因此需要在Adapter里面设置他们的属性
1 | binding.collectionSwipe.setShowMode(SwipeLayout.ShowMode.PullOut); |
只需要指定滑动的模式(拖动或者抽屉),然后指定左边拖动的布局以及右边拖动的布局,就可以实现一个可以左右侧滑的列表项了。
实际效果:

布局:

英雄详情
GIF 加载动画
由于英雄详情的首次进入需要加载的数据比较多(尤其是高清皮肤图),根据不同的网络状态,大概需要 2-3s 以上的事件加载(再次进入会检查本地缓存和数据库)
因此加入了一个加载的过渡动画

这个动画是一个Gif,由于原生的ImageView不支持直接设置Gif,因此这里使用了一个android-gif-drawable的控件,实际上的使用方法和ImageView是一模一样的。
1 | <pl.droidsonroids.gif.GifImageView |
布局

这里的布局看上去只是一张图片,加一些控件,但实际上还是一个CollapsingToolbarLayout,只不过我把它的高度调成了全屏,然后把标题的位置调成右下角,颜色调成黑色。
而这条斜线是通过drawable画出来的。Drawable本身并没有提供可画出斜线或者三角形的东西,因此这里需要用到一个特别的思路
1 | <!-- 正三角 --> |
这里首先通过shape画了一个带边框的白色矩形,然后使用rotate对其进行旋转,使其一条边显示倾斜显示并且下移和右移一定的百分比。
1 | <ImageView |
然后在布局的时候,对其 X 轴进行 2 倍的缩放。最后就会显示成斜线的一个显示效果。
然后为这个斜线和背景图片分别设置不同因子的视差效果,就会得到十分有趣的效果:

图片滚动
再看看上上张图,可以发现图片显示不完全,只能显示英雄的半张脸。由于图片都是横向的,而我们的可视窗口是竖向的,因此图片高度越大就越难显示全部内容。
因此需要一种方案展示全部图片。
我们这里采取的方案是使得图片可以左右滑动,让用户滑动到想要的位置(然后就可以通过分享功能分享布局截图给小伙伴了)
1 | <HorizontalScrollView |
实现起来也不难,只需要在图片的外层套一层HoriazontalScrollView
然后把其android:fillViewport设为true,图片的android:adjustViewBounds设为true,就可以实现手动左右滑动,甚至可以在代码里面控制图片的滚动。
这两个属性是十分重要的,如果没有这两个属性,图片就会默认以最长边进行适配,以显示全图,结果就是我们只看到图片在小小的上面,而不能放大。
加上了这两个属性,图片就可以适应滚动布局的高度而放大。
皮肤切换
对于同一个英雄,会有不同的皮肤数量,有些甚至有 5、6 个皮肤。因此我们需要一个RecyclerView来供用户选择显示的图片。
对于对于两个皮肤的英雄,会给出一个箭头指示可滑动的RecyclerView

最终效果:

布局:

下面还有TabLayout、ViewPager等一些布局,不过这是另外一位队友做的,这里就不详细说明了。
数据
数据优先从本地缓存、数据库中获取,如果没有找到再从官方的 API 联网获取。这一部分的工作是由另一位大佬完成的,这里就不详细说明了,这里主要说一下前端界面和后端数据的交互方法。
数据绑定
DataBinding 是Android Architecture Components中推荐使用的一部分。
DataBinding 是谷歌官方发布的一个框架,顾名思义即为数据绑定,是 MVVM 模式在 Android 上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel 层。DataBinding 能够省去我们一直以来的 findViewById() 步骤,大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常
通过数据绑定,我们只需要在布局的时候,在xml里面指定控件所绑定的数据和事件。就可以将控件的onClick和Handler里面的函数对应起来,将控件的数据,如src,text和model里面的数据绑定起来,只需要通过setModel就可以设置所有的数据。
当然,这个东西并不是所有的东西都可以绑定的,不过也不要紧。通过布局文件生成的对应的binding类,可以直接访问里面所有具有id的控件,并且将下划线命名法自动转换成Java里面的驼峰命名风格的变量。
下面就是代码中使用binding直接操作控件的方法。
1 | // 设置颜色 |
ViewModel
前端界面显示的所有数据都是在Handler层直接调用ViewModel获取的,而ViewModel则是通过Dagger2注入到各个Handler里面。
由于很多数据需要异步获取,因此ViewModel返回的数据都是LiveData<>,我们需要观察他们的变化对 UI 的内容进行填充
下面是使用ViewModel获取英雄列表的一个例子:
1 | viewModel.getHeroList().observe(activity, resource -> { |
根据数据的变化的不同情况对于界面做出不同的反应,显示给用户。
当状态LOADING的时候,可以显示进度条,告诉用户需要等待。
当因为网络问题不能获取数据的时候,就会观测到ERROR状态,就可以将无法获取数据的提示展示给用户。
当状态为SUCCESS的时候,就可以将数据内容显示到界面。
总结
这次期中项目完完整整地实现了一个具有实际用途的应用,从中学到了不少。
首先是对于应用架构的搭建更加地得心应手了,基本上了解了一个 Android 应用的基本构成和工作原理。
然后使用了很多 Android 应用开发中比较流行的界面 UI 库以及架构库,使用了谷歌官方推荐的MVVM的架构来开发应用,对于各个布局做了解耦处理(虽然耦合度还是比较高,部分代码也是比较乱)。
因为我在这个项目主要是负责界面的设计和交互逻辑的处理,在做这个项目时候,遇到了很多界面布局上的坑。
比如CollapsingToolbarLayout中标题收缩起来错位,fitsSystemWindows无效的情况。
还有DialogFragment布局最外层布局的宽度match_parent的问题。
还有NestedScrollView和RecyclerView在一起用造成的性能问题,使得RecyclerView一次性绑定过多Item,导致界面卡顿的问题。
还有MaterialButton无法在代码中设置背景色,必须要使用setBackgroundTintList和getColorStateList才能才能设置的问题。
还有一系列等等的坑,都通过Google以及个人经验逐一踩掉,总的来说在交互逻辑和布局上积累了不少的经验,在总体的应用架构设计上也有了方案。🐸