原文:ViewPager Tutorial: Getting Started in Kotlin
作者:Diana Pislaru
译者:kmyhy
ViewPager 是一个强大的布局管理工具,允许你在 app 中使用滑动手势进行导航。通常用于创建幻灯片效果、启动引导,或者 tab view。通过左右滑动在两个 ViewPage 页面之间切换,从而节省屏幕空间,创建更加迷你的界面。
在本教程中,你将通过修改一个现成的 app 让 UI 变得更有趣,并学习 ViewPager 的使用。在这个过程中,你将学习:
注意:本教程假设你拥有 Kotlin 和 Android 开发经验。如果你不熟悉这门语言,请阅读这篇教程。如果你刚刚接触 Android,请阅读我们的入门和其它教程。
下载开始项目,并打开 Android Studio ,然后选择 Open an existing Android Studio project。
找到示例项目目录,然后点击 Open。
首先看一眼现有代码。在 assets 目录,有一个 JSON 文件,包含了一些数据,它们是最流行的 5 个电影类 Android App。
你可以在 MoviewHelper.kt 中找到读取 JSON 数据的助手方法。Picasso 库用于下载和显示图片。
本教程使用 fragments。如果你不熟悉它,请阅读这个教程。
Build & run。
这个 app 有几个页面,每个页面会显示一些电影信息。我敢打赌,你一定想左右滑动以便查看下一部电影!或者只有我一个人会这样想?现在,我们还只能通过底部的上一部、下一部按钮来进行“不那么优雅的”页面切换。
在 UI 中添加一个 ViewPager,将允许用户前后切换电影,通过在屏幕上左右骚动。你无需处理滑动动画、手势识别,因此实现起来比你想的还要简单。
我们可以将 ViewPager 实现分成 3 个步骤:
第一步,打开 MainActivity.kt 删除 onCreate() 中这句之后的所有内容:
val movies = MovieHelper.getMoviesFromJson("movies.json", this)
从类中删除 replaceFragment() 方法。
打开 activity_main.xml 将 RelativeLayout 替换成:
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_height="match_parent"
android:layout_width="match_parent" />
Here you created the ViewPager view, which is now the only child of the RelativeLayout. Here’s how the xml file should look:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:context="com.raywenderlich.favoritemovies.MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_height="match_parent"
android:layout_width="match_parent" />
RelativeLayout>
ViewPager 只在 Android Support 库中有效。Android Support Library 实际上是一系列库的集合,提供了对 widgets 和其它 标准 Android 特性的向下兼容。这些库提供了一些常见的 API,允许你在只支持低版本 API 级别的设备上使用高版本的 Android SDK 特性。你可以自己了解一下 Support Library 和 Support Library Packages。
回到 MainActivity.kt,导入 ViewPager:
import android.support.v4.view.ViewPager
现在你可以添加一个 ViewPager 属性了:
private lateinit var viewPager: ViewPager
注意:关键字 lateinit 的使用避免了在延迟初始化时 view 为空的问题。关于 lateinit 和其它 Kotlin 修饰符请阅读这里。
在 onCreate() 方法底部添加这句,以将 ViewPager 和你之前写的 xml 视图绑定:
viewPager = findViewById(R.id.viewPager)
第一步完成了。你现在有一个 ViewPager,但没有 Adapter 来告诉它怎么显示的话,它上面也做不了。如果你运行 app,你不会看到任何电影。
ViewPager 通常显示用 fragment 构成的“页面”,但如果你想显示静态内容的话,也可以用于显示简单视图比如 ImageView 上。在这个项目中,你将在每个页面中显示多个内容。这里,我们将使用 Fragment。
我们需要通过 PagerAdapter 将 Fragment 对象和 ViewPager 关联,PagerAdapter 是一个对象,它位于 ViewPager 和包含你想显示的内容的数据集(在这里指的就是电影数组)之间。PagerAdapter 将创建一个个的 Fragment,将电影数据填充到 Fragment 然后返回给 ViewPager。
PagerAdapter 是一个抽象类,因此你将使用它的子类(FragmentPagerAdapter 和 FragmentStatePagerAdapter)而不是这个类自身。
有两种用于管理每个 fragment 生命周期的标准 PagerAdapter 类型: FragmentPagerAdapter 和 FragmentStatePagerAdapter。它们都使用 fragment,但它们分别适用于不同场景:
FragmentPageAdapter 将 fragment 保存到内存,以便用户能够在它们之间进行导航。当一个 fragment 不可见时,PagerAdapter 会放开它,但不会销毁它,因此这个 fragment 对象仍然存活在 FragmentManager 中。只有 Activity 关闭时才释放它的内存。这使得页面间的动画更流畅和快速,但也会导致 app 的内存占用问题,如果 fragment 比较多的话。
FragmentStatePagerAdapter 如它的名称所暗示的,它会在用户不可见时会释放所有 fragment,只是将它们的状态保存在 FragmentManager 中。当用户回到一个 fragment,它会用所保存的状态恢复它。这种 PagerAdapter 对内存的需要更低,但可能两个页面之间的切换会更慢一点。
是时间做出选择了。你的电影只有 5 部,因此用 FragmentPagerAdapter 就足够了。如果你对这个教程感到无聊,想看看谁有的哈利.波特的电影时怎么办?你必须在 JSON 文件中再添加 8 部电影。这个数组更大。这样,你最好是用 FragmentStatePagerAdapter。
在项目导航面板中,右键点击 com.raywenderlich.favoritemovies,选择 New->Kotlin File/Class。命名为 MoviesPagerAdapter 然后选择类型为 Class。点击 OK。
编辑内容为:
package com.raywenderlich.favoritemovies
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
// 1
class MoviesPagerAdapter(fragmentManager: FragmentManager, private val movies: ArrayList) :
FragmentStatePagerAdapter(fragmentManager) {
// 2
override fun getItem(position: Int): Fragment {
return MovieFragment.newInstance(movies[position])
}
// 3
override fun getCount(): Int {
return movies.size
}
}
分段解释如下:
当 ViewPager 需要显示一个 fragment 时,它会和 PagerAdapter 进行一系列会话。首先,它用 getCount 方法询问 PagerAdapter 数组中有多少部电影。然后在某个新页面即将显示时调用 getItem(int position)。在这个方法中,PagerAdapter 会创建一个新的 fragment 用于显示数组中和这个位置对应电影的信息。
打开 MainActivity.kt 声明一个 MoviesPagerAdapter:
private lateinit var pagerAdapter: MoviesPagerAdapter
然后在 onCreate() 的已有代码下面添加:
pagerAdapter = MoviesPagerAdapter(supportFragmentManager, movies)
viewPager.adapter = pagerAdapter
初始化 MoviewsPagerAdapter 对象并将它和 ViewPager 关联。
注意:supportFragmentManager 等于 Java 中的 getSupportFragmentManager() 方法,viewPager.adapter = pagerAdapter 则等于 viewPager.setAdapter(pagerAdapter)。关于 Kotlin 的 getter/setter 访问器请看这里。
Build & run。app 表面上和之前一样,但你可以用轻扫手势而不是按钮来进行导航切换了。
注意:FragmentStatePagerAdapter 能省去你处理当前页面在运行时期间的配置改变的工作,比如设备的旋转。在这种情况下 activity 的状态会丢失,你必须通过 onCreate(savedInstanceState:Bundle?) 的方式将状态保存在 Bundle 对象中。幸好,你使用这种 PagerAdapter 能够为你自动完成这些工作。关于 savedInstanceState 对象和 activity 的生命周期,你可以参考这里。
你经常会看到这样一种特性,也就是能够在页面之间进行往复循环式的无限切换。在第一页右扫会向最后一页循环,当在最后一页上左扫则向第一页循环。例如有 3 页,这样切换:
当当前索引到达 getCount() 返回的数组对象数目时,FragmentStatePagerAdapter 会停止创建新的 fragment。因此你需要修改这个方法,让它返回一个非常大的数字,使用户在同一方向扫动时不可能达到这个数。这样在索引达到 getCount() 返回的值之前, PagerAdapter 会不停地创建新页面。
打开 MoviesPagerAdapter.kt 一个常量保存一个数值:
private const val MAX_VALUE = 200
现在将返回结果从 movie.size 替换成 getCount():
return movies.size * MAX_VALUE
将数组大小乘上 MAX_VALUE,轻扫限制将以电影部数的整数倍进行增长。这种方式你就不必担心当电影数组变大时,返回的数字小于电影数组的大小。
在 Adapter 的 getItem(position:Int) 方法中还有一个问题。因为 getCount() 现在返回的数字远比数组大小要大,当用户扫动次数超过最后一部电影时,ViewPager 会越界访问数组元素。
将 getItem(position:Int) 方法修改为:
return MovieFragment.newInstance(movies[position % movies.size])
这将确保 ViewPager 不会越界访问 movies 数组中的元素,因为对 postion 以 movies.size 为模进行取模后,值只可能大于 0 并小于 movies.size。
现在,只有用户翻过整个数组后页面才会无限滚动(左扫)。这是因为,当 app 启动后,ViewPager 显示第 0 部电影。要解决这个问题,请打开MainActivity.kt 在 onCreate() 中 将 PageAdapter 关联到 ViewPager 之后添加一句:
viewPager.currentItem = pagerAdapter.count / 2
这将告诉 ViewPager,显示数组中间的电影。现在无论是哪个方向,都有大量的扫动次数可用。要保证一开始显示的电影仍然是数组中的第一部电影,可以将 MAX_VALUE 设置为一个偶数(这里,保持 200 就好)。这样,除以 2 之后,pagerAdapter.count % movies.size 还是等于 0(也就是当 app 启动时拿到的仍然是第一部电影的索引)。
Build & run。你现在可以左扫、右扫任意多次了,当你到达最后一部电影后又会从头开始,当你返回到第一部电影后又从最后开始,或者当你翻到最后一部电影后又会从第一部开始。
TabLayout 是一个很好用的功能,允许你在多个页面间进行浏览和切换。TabLayout 为每个页面保留一个 tab,这个 tab 一般会显示页标题。用户可以点击 tab 切换到对应的页面或者用轻扫手势切换页面。
如果你将一个 TabLayout 添加到你的 ViewPager 中,你是无法看到 tab 的,因为自动布局使用 getCount() 方法作为 tab 的数目,但是现在 getCount() 方法返回的是一个超大的数字,要解决这个问题,你需要将数字缩小。
幸好,还有一个第三方库,叫做 RecyclerTabLayout 解决了这个问题。这个库在实现中使用了 RecycleView。你可以在这篇教程中学习这个神秘的 RecyclerView。要安装这个库,请打开 build.grad( app 的 Module 中),在 dependencies 中添加:
implementation 'com.nshmura:recyclertablayout:1.5.0'
Recyclertablayout 库使用了老版本的 Android Support Libraries 库,因此你必须添加这句才能让 Gradle 同步过程中不报错:
implementation 'com.android.support:recyclerview-v7:26.1.0'
现在点击 Sync Now,等 Android Studio 安装完这个库。
打开 activity_main.xml,在 ViewPager 之上加入下列代码:
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/recyclerTabLayout"
android:layout_height="@dimen/tabs_height"
android:layout_width="match_parent" />
现在在 ViewPager 中添加下列属性,让 ViewPager 在 RecyclerTabLayout 的下方对齐:
android:layout_below="@id/recyclerTabLayout"
整个布局文件变成这个样子:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:context="com.raywenderlich.favoritemovies.MainActivity">
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/recyclerTabLayout"
android:layout_height="@dimen/tabs_height"
android:layout_width="match_parent" />
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_below="@id/recyclerTabLayout"
android:layout_height="match_parent"
android:layout_width="match_parent" />
RelativeLayout>
打开 MainActivity.kt,导入 RecyclerTabLayout:
import com.nshmura.recyclertablayout.RecyclerTabLayout
在类头部声明一个 RecyclerTabLayout 变量:
private lateinit var recyclerTabLayout: RecyclerTabLayout
在 onCreate() 方法的设置 viewPager.currentItem 的代码之上添加:
recyclerTabLayout = findViewById(R.id.recyclerTabLayout)
recyclerTabLayout.setUpWithViewPager(viewPager)
第一句将 RecyclerTabLayout 对象和 xml 视图绑定,第二句将 RecyclerTabLayout 和 ViewPager 关联。
最后,你必须让 RecyclerTabLayout 知道在 Tab 上分别都显示些什么标题。打开 MoviesPagerAdapter.kt 添加这个方法:
override fun getPageTitle(position: Int): CharSequence {
return movies[position % movies.size].title
}
这个方法告诉 TabLayout 在对应位置的 Tab 上需要显示什么标题。它会针对用 getItem(postion:Int) 方法创建的 fragment 找到对应的电影来告诉 TabLayout 该显示什么。
运行 app。你会发现当你左右滑动 tab 时页面会做跳转。点击某个 tab,ViewPager 会自动滚动到相应的电影。
你可以下载完整示例项目。
干得不错!你修改了一个 app,通过 ViewPager 来改进它的 UI。你还添加了 TabLayout,实现了无限滑动。另外,你学习了 PagerAdapter 以及如何根据需要来选择 FragmentPagerAdapter 和 FragmentStatePagerAdapter。
如果你想进一步了解 ViewPager,可以参考这篇文档。你可以通过 PagerTransformer 自定义转换动画,你可以参考这篇教程。
加分项:你可以实现一个圆点指示器,就像其他照片流 APP 中一样。你可以参考这里的创建圆点指示器的方法。注意这个方法不能用于教程最后实现的 ViewPager,因为这种方法需要用 PagerAdapter 的 getCount() 方法来返回真实的页数。你可以实现这种指示器,而不要用无限滑动。同时可以用默认的 TabLayout 替代第三方库。你可以参考这个方案。
有任何问题和建议,请在论坛中留言。