这是一个参考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
以及个人经验逐一踩掉,总的来说在交互逻辑和布局上积累了不少的经验,在总体的应用架构设计上也有了方案。🐸