安卓组合控件(底部标签栏、顶部导航栏、增强型列表、升级版翻页)

本章介绍App开发常用的一些组合控件用法,主要包括:如何实现底部标签栏、如何运用顶部导航栏、如何利用循环视图实现3种增强型列表、如何使用二代翻页视图实现更炫的翻页效果。

底部标签栏

本节介绍底部标签栏的两种实现方式:首先说明如何通过Android Studio菜单自动创建基于BottomNavigationView的导航活动,然后描述如何利用状态图形与风格样式实现自定义的标签按钮,最后阐述怎样结合RadioGroup和ViewPager制作自定义的底部标签栏。

利用BottomNavigationView实现底部标签栏

不管是微信还是QQ,淘宝抑或京东,它们的首屏都在底部展开一排标签,每个标签对应着一个频道,从而方便用户迅速切换到对应频道。这种底部标签原本是苹果手机的标配,原生安卓最开始并不提供屏幕底部的快捷方式,倒是众多国产App纷纷山寨苹果的风格,八仙过海各显神通整出了底部标签栏。后来谷歌一看这种风格还颇受用户欢迎,于是顺势在Android Studio种集成了该风格的快捷标签。
如今在Android Studio上创建官方默认的首屏标签页面已经很方便了,具体步骤如下:

  1. 首先右击需要添加标签栏的模块,在弹出的右键菜单中依次选择New->Activity->Buttom Navigation Views Activity,弹出如下图所示的活动创建对话框。
    在这里插入图片描述

  2. 在创建对话框的Activity Name一栏填写新活动的名称,再单击对话框右下角的Finish按钮,Android Studio就会自动创建该活动的Java代码及其XML文件。

  3. 编译运行App,进入刚创建的活动页面,其界面效果如下图所示。可见测试页面的底部默认提供了3个导航标签,分别是Home、Dashboard和Notifications。
    在这里插入图片描述

注意到初始页面的Home标签从文字到图片均为高亮显示,说明当前处于Home频道。接着点击Dashboard标签,此时界面即可切换到Dashboard频道,再点击Notifications则可切换到Notifications频道。
不过为了定制页面的详细内容,开发者仍需修改相关代码,譬如将标签文字从英文改成中文,将频道上方的描述说明从英文改成中文,给频道页面添加图像等其他控件,等等,故而还得梳理标签框架的实现方式。
但是由于Android API的更新,在你新建好底部导航栏后编译可能会出现下列报错:

Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.internal.jdk7.JDK7PlatformImplementations found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.internal.jdk7.JDK7PlatformImplementations$ReflectSdkVersion found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.internal.jdk8.JDK8PlatformImplementations found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.internal.jdk8.JDK8PlatformImplementations$ReflectSdkVersion found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.io.path.ExperimentalPathApi found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.io.path.PathRelativizer found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.io.path.PathsKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.io.path.PathsKt__PathReadWriteKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.io.path.PathsKt__PathUtilsKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.jdk7.AutoCloseableKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk7-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21)
Duplicate class kotlin.jvm.jdk8.JvmRepeatableKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.random.jdk8.PlatformThreadLocalRandom found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.streams.jdk8.StreamsKt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.streams.jdk8.StreamsKt$asSequence$$inlined$Sequence$1 found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.streams.jdk8.StreamsKt$asSequence$$inlined$Sequence$2 found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.streams.jdk8.StreamsKt$asSequence$$inlined$Sequence$3 found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.streams.jdk8.StreamsKt$asSequence$$inlined$Sequence$4 found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.text.jdk8.RegexExtensionsJDK8Kt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.time.jdk8.DurationConversionsJDK8Kt found in modules kotlin-stdlib-1.8.20 (org.jetbrains.kotlin:kotlin-stdlib:1.8.20) and kotlin-stdlib-jdk8-1.6.21 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)

Go to the documentation to learn how to Fix dependency resolution errors.

解决方法就是在项目的build.gradle.kts文件,dependencies下面添加这句代码指定使用哪个类就好了:

dependencies {
	implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.20"))
}

然后打开导航栏页面,马上又会报了另外一个错如下:

java.lang.IllegalStateException: Activity com.example.xxxxxx.TabNavigationActivity@d19f70f does not have an ActionBar set via setSupportActionBar()

这个报错大概意思就是说页面需要支持ActionBar,所以简单点操作就是修改主题支持ActionBar就可以了。现在打开亮色/暗色主题查看声明如下,可以看到声明为NoActionBar:

<style name="Base.Theme.Chapter09" parent="Theme.Material3.DayNight.NoActionBar">
    <!-- Customize your light theme here. -->
    <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Base.Theme.Chapter09" parent="Theme.Material3.DayNight.NoActionBar">
    <!-- Customize your dark theme here. -->
    <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>

然后删掉NoActionBar变为如下即可:

<style name="Base.Theme.Chapter09" parent="Theme.Material3.DayNight">
    <!-- Customize your light theme here. -->
    <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Base.Theme.Chapter09" parent="Theme.Material3.DayNight">
    <!-- Customize your dark theme here. -->
    <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>

接下来打开模块的build.gradle.kts,在dependencies节点内部发现多了下面两行依赖库,表示引用了标签导航的navigation库:

implementation("androidx.navigation:navigation-fragment:2.6.0")
implementation("androidx.navigation:navigation-ui:2.6.0")

再来看标签页面的XML文件,它的关键内容如下:

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/nav_view"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="0dp"
    android:layout_marginEnd="0dp"
    android:background="?android:attr/windowBackground"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:menu="@menu/bottom_nav_menu" />

<fragment
    android:id="@+id/nav_host_fragment_activity_tab_navigation"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toTopOf="@id/nav_view"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:navGraph="@navigation/mobile_navigation" />

从上面的布局内容可知,标签页面主要包含两个组成部分:一个位于底部的BottomNavigationView(底部导航视图),另一个是位于其上占据剩余屏幕的碎片fragment。底部导航视图又由一排标签菜单组成,具体菜单在@menu/bottom_nav_menu中定义;而碎片为各频道的主题部分,具体内容在@navigation/mobile_navigation中定义。哟,原来奥妙就在这两个文件中,赶紧打开menu目录之下的bottom_nav_menu.xml看看:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />
    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />
    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />
</menu>

上面的菜单定义文件以menu为根节点,内部容纳3个item节点,分别对应屏幕底部的3个标签。每个item节点都拥有id、icon、title 3个属性,其中id指定该菜单项的编号,icon指定该菜单项的图标,title指定该菜单的文本。顺藤摸瓜查看values目录之下的strings.xml,果然找到了下面的3个标签文本定义:

<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>

搞清楚了底部标签的资源情况,接着打开navigation目录之下的mobile_navigation.xml,究竟里面是怎么定义各个频道的呢?

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_home">
    <fragment
        android:id="@+id/navigation_home"
        android:name="com.example.chapter09.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />
    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.example.chapter09.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />
    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.example.chapter09.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>

上述的导航定义文件以navigation为根节点,内部依旧分布着3个fragment节点,显然正好对应3个频道。每个fragment节点拥有id、name、label、layout 4个属性,各属性的用途说明如下:

  • id:指定当前碎片的编号。
  • name:指定当前碎片的完整类型名路径。
  • label:指定当前碎片的标题文本。
  • layout:指定当前碎片的布局文件。

这些默认的碎片代码到底有何不同,打开其中一个HomeFragment.java研究研究,它的关键代码如下:

public View onCreateView(@NonNull LayoutInflater inflater,
                         ViewGroup container, Bundle savedInstanceState) {
    HomeViewModel homeViewModel =
            new ViewModelProvider(this).get(HomeViewModel.class);

    binding = FragmentHomeBinding.inflate(inflater, container, false);
    View root = binding.getRoot();

    final TextView textView = binding.textHome;
    homeViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
    return root;
}

看来频道用到的碎片代码仍然在onCreateView方法中根据传参来的XML布局文件生成页面元素,这样频道界面的修改操作就交给碎片编码了。总算理清了这种底部导航的实现方式,接下来准备修理修理默认的标签及其频道。先打开values目录下的strings.xml,把3各标签的文字从英文改成中文,修改内容如下:

<string name="title_home">首页</string>
<string name="title_dashboard">仪表盘</string>
<string name="title_notifications">消息</string>

再打开3个频道的碎片代码,给文本视图天上中文描述,首页频道HomeFragment.java修改之后的代码示例如下:

public View onCreateView(@NonNull LayoutInflater inflater,
                         ViewGroup container, Bundle savedInstanceState) {
    HomeViewModel homeViewModel =
            new ViewModelProvider(this).get(HomeViewModel.class);

    binding = FragmentHomeBinding.inflate(inflater, container, false);
    View root = binding.getRoot();

    final TextView textView = binding.textHome;
//  homeViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
    textView.setText("这是首页");
    return root;
}

修改完毕重新编译App,改过的各频道即显示对应的中文效果如下:
在这里插入图片描述

自定义标签按钮

按钮控件种类繁多,有文本按钮Button、图像按钮ImageButton、单选按钮RadioButton、复选框CheckBox、开关按钮Switch等,支持展现的形式有文本、图像、文本+图标,如此丰富的展现形式,已经能满足大部分需求。但总有少数场合比较特殊,一般的按钮样式满足不了,比如如下图所示的微信底部标签栏,一排有4个标签按钮,每个按钮的图标和文字都会随着选中而高亮显示。
在这里插入图片描述
这样的标签是各大主流App的标配,无论是淘宝、京东,还是微信、手机QQ,首屏底部是清一色的标签栏,而且在选中标签栏按钮时对应的文字、图标、背景一起高亮显示。虽然上一小节使用Android Studio自动生成了底部标签栏,但是整个标签栏都封装进了BottomNavigationView,看不到标签按钮的具体实现,令人不知其所以然,像这种标签,Android似乎都没有对应的专门控件,如果要自定义控件,就得设计一个布局容器,里面放入一个文本控件和图像控件,然后注册选中事件的监听器,一旦监听到选中事件,就高亮显示它的文字、图标与布局背景。
自定义控件固然是一个不错的思路,不过无须如此干戈。我们可以像操作开关按钮一样,通过状态图形自动展示显示背景,可以给background属性设置状态图形;要想高亮显示图标,可以给drawableTop属性设置状态图形;要想高亮显示文本,可以给textColor属性设置状态图形。既然背景、图标文字都能通过状态图形控制是否高亮显示,接下来的事情就好办了,具体实现步骤如下:

  1. 定义一个状态图形的XML描述文件,当状态为选中时展示高亮图标,其余情况展示普通图标,于是状态图形的XML内容示例如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:drawable="@drawable/tab_bg_selected" />
    <item android:drawable="@drawable/tab_bg_normal" />
</selector>

上面定义的tab_bg_selector.xml用于控制标签背景的状态显示,控制文本状态的tab_text_selector.xml和控制图标的tab_text_selector.xml可如法炮制。
2. 在活动页面的XML文件中添加CheckBox节点,并给该节点的background、drawableTop、textColor 3个属性分别设置对应的状态图形,修改后的XML文件内容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp">

    <!-- 复选框的背景、文字颜色和顶部图标都采用了状态图形,看起来像个崭新的标签控件 -->
    <CheckBox
        android:id="@+id/ck_tab"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:padding="5dp"
        android:gravity="center"
        android:button="@null"
        android:background="@drawable/tab_bg_selector"
        android:text="点我"
        android:textSize="12sp"
        android:textColor="@drawable/tab_text_selector"
        android:drawableTop="@drawable/tab_first_selector" />

    <TextView
        android:id="@+id/tv_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这里查看标签选择结果"
        android:textColor="@color/black"
        android:textSize="17sp" />
</LinearLayout>
  1. 活动页面的Java代码给复选框ck_tab设置勾选监听器,用力啊监听复选框的选中事件和取消选中事件,活动代码如下:
public class TabButtonActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_button);
        final TextView tv_select = findViewById(R.id.tv_select);
        CheckBox ck_tab = findViewById(R.id.ck_tab);
        // 给复选框设置勾选监听器
        ck_tab.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (buttonView.getId() == R.id.ck_tab) {
                String desc = String.format("标签按钮被%s了", isChecked?"选中":"取消选中");
                tv_select.setText(desc);
            }
        });
    }
}

运行App,一开始的标签按钮界面如下图:
在这里插入图片描述
点击标签控件,复选框变为选中状态,它的文字、图标、背景同时高亮显示如下图:
在这里插入图片描述
是不是很神奇?接下来不妨把该控件的共同属性挑出来,因为底部标签栏通常有4、5个标签按钮,如果每个按钮节点添加重复的属性,就太啰嗦了,所以把它们之间通用的属性挑出来,然后再values/styles.xml中定义名为TabButton的新风格,具体的风格内容如下:

<style name="TabButton">
    <item name="android:layout_width">0dp</item>
    <item name="android:layout_height">match_parent</item>
    <item name="android:layout_weight">1</item>
    <item name="android:padding">5dp</item>
    <item name="android:gravity">center</item>
    <item name="android:background">@drawable/tab_bg_selector</item>
    <item name="android:textSize">12sp</item>
    <item name="android:textColor">@drawable/tab_text_selector</item>
    <item name="android:button">@null</item>
</style>

然后,XML文件只要给CheckBox节点添加一行style="@style/TabButton",即可将其变为标签按钮。直接在styles.xml中定义风格,无需另外编写自定义控件的代码,这是自定义控件的另一种途径。
回到前述活动页面的XML文件,补充一下的布局内容,表示添加一行3个标签控件,也就是3个CheckBox节点都声明了style="@style/TabButton",同时每个CheckBox另外指定自己的标签文字和标签图标。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:orientation="horizontal">
    <CheckBox
        style="@style/TabButton"
        android:checked="true"
        android:drawableTop="@drawable/tab_first_selector"
        android:text="首页" />
    <CheckBox
        style="@style/TabButton"
        android:drawableTop="@drawable/tab_second_selector"
        android:text="分类" />
    <CheckBox
        style="@style/TabButton"
        android:drawableTop="@drawable/tab_third_selector"
        android:text="购物车" />
</LinearLayout>

重新运行App,发现标签界面多了一排标签按钮,分别是“首页” “分类” “购物车”,如下图:
在这里插入图片描述

多次点击3个按钮,它们的外观都遵循一种样式状态,可见统一的风格定义果然奏效了。

结合RadioGrop和ViewPager自定义底部标签栏

尽管使用Android Studio很容易生成自带底部标签的活动页面,可是该标签基于BottomNavigationView,标签的样式风格不方便另行调整,况且它也不支持通过左右滑动切换标签。因此,开发者若想实现拥有更多花样的标签栏,就得自己定义专门的底部标签栏了。
话说翻页视图ViewPager搭配翻页标签栏PagerTabStrip,本来已经实现了带标签的翻页功能,不过这个标签位于翻页视上方而非下方,而且只有标签没有标签图标,比起BottomNavigationView更不友好,所以用不了PagerTabStrip。鉴于标签栏每次只能选中一项标签,这种排他性与单选按钮类似,理论上采用一排单选按钮也能实现标签栏的单选功能。只是单选按钮的外观不满足要求,中用却不中看。这点小瑕疵倒也无妨,把它的样式改成上一小节介绍的标签按钮就行,也就是给RadioButton标签添加演示属性style="@style/TabButton"。然后用单选组RadioGroup容纳这几个单选按钮,再把单选按钮放在页面底部,把翻页视图放在单选组上当,于是整个页面的XML文件变成下面这样:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vp_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
    <RadioGroup
        android:id="@+id/rg_tabbar"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">
        <RadioButton
            android:id="@+id/rb_home"
            style="@style/TabButton"
            android:checked="true"
            android:text="首页"
            android:drawableTop="@drawable/tab_first_selector" />
        <RadioButton
            android:id="@+id/rb_class"
            style="@style/TabButton"
            android:text="分类"
            android:drawableTop="@drawable/tab_second_selector" />
        <RadioButton
            android:id="@+id/rb_cart"
            style="@style/TabButton"
            android:text="购物车"
            android:drawableTop="@drawable/tab_third_selector" />
    </RadioGroup>
</LinearLayout>

该页面对应的Java代码主要实现以下两个切换逻辑:

  1. 左右滑动翻页视图的时候,每个页面滚动结束,就自动选择对应位置的单选按钮。
  2. 点击某个单选按钮的时候,先判断当前选择的时第几个按钮,再将翻页视图翻到第几个页面。

具体到编码实现,则要给翻页视图添加页面变更监听器,并补充翻页完成的处理操作;还要给单选组注册选择监听器,并补充选中之后的处理操作。纤细的活动代码如下:

public class TabPagerActivity extends AppCompatActivity {
    private ViewPager vp_content; // 声明一个翻页视图对象
    private RadioGroup rg_tabbar; // 声明一个单选组对象

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_pager);
        vp_content = findViewById(R.id.vp_content); // 从布局文件获取翻页视图
        // 构建一个翻页适配器
        TabPagerAdapter adapter = new TabPagerAdapter(getSupportFragmentManager());
        vp_content.setAdapter(adapter); // 设置翻页视图的适配器
        // 给翻页视图添加页面变更监听器
        vp_content.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                // 选中指定位置的单选按钮
                rg_tabbar.check(rg_tabbar.getChildAt(position).getId());
            }
        });
        rg_tabbar = findViewById(R.id.rg_tabbar); // 从布局文件获取单选组
        // 设置单选组的选中监听器
        rg_tabbar.setOnCheckedChangeListener((group, checkedId) -> {
            for (int pos=0; pos<rg_tabbar.getChildCount(); pos++) {
                // 获得指定位置的单选按钮
                RadioButton tab = (RadioButton) rg_tabbar.getChildAt(pos);
                if (tab.getId() == checkedId) { // 正是当前选中的按钮
                    vp_content.setCurrentItem(pos); // 设置翻页视图显示第几页
                }
            }
        });
    }
}

由于翻页视图需要搭配翻页适配器,因此以上代码给出了一个适配器TabPagerAdapter,该适配器的代码很简单,仅仅返回3个碎片而已,下面是翻页适配器的代码例子:

public class TabPagerAdapter extends FragmentPagerAdapter {

    // 碎片页适配器的构造方法,传入碎片管理器
    public TabPagerAdapter(FragmentManager fm) {
        super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
    }

    // 获取指定位置的碎片Fragment
    @Override
    public Fragment getItem(int position) {
        if (position == 0) {
            return new TabFirstFragment();  // 返回第一个碎片
        } else if (position == 1) {
            return new TabSecondFragment();  // 返回第二个碎片
        } else if (position == 2) {
            return new TabThirdFragment();  // 返回第三个碎片
        } else {
            return null;
        }
    }

    // 获取碎片Fragment的个数
    @Override
    public int getCount() {
        return 3;
    }
}

当前的3个碎片TabFirstFragment、TabSecondFragment、TabThirdFragment并没有什么功能,只在其内部都放了一个文本试图而已。
至此从活动页面到适配器再到碎片全部重写了以便,运行App,打开该活动的初始界面,如下图所示。
在这里插入图片描述
当我们点击对应界面或者左右滑动时就会切换到对应碎片,如下图所示:
在这里插入图片描述

顶部导航栏

本节介绍顶部导航栏的组成控件:首先描述工具栏Toolbar的基本用法,然后叙述溢出菜单OverflowMenu的格式及其用法,最后讲解标签布局TabLayout的相关属性和方法用途。

工具栏ToolBar

主流App除了底部有一排标签栏外,通常顶部还有一排导航栏。在Android 5.0之前,这个顶部导航栏用的是ActionBar控件,但ActionBar存在不灵活、难以扩展等毛病,所以Android 5.0之后推出了Toolbar工具栏,意在取代ActionBar。
不过为了兼容之前的系统版本,ActionBar仍然保留。当然,由于Toolbar与ActionBar都占着顶部导航栏的位置,二者肯定不能共存,因此想要引入Toolbar就得先关闭ActionBar。具体的操作步骤如下:

  1. 在styles.xml中定义一个不包含ActionBar的风格样式,代码如下:
<style name="AppCompatTheme" parent="Theme.AppCompat.Light.NoActionBar" />
  1. 修改AndroidManifest.xml,给activity节点添加android:theme属性,并将属性值设为步骤1定义的风格,如android:theme=@style/AppCompatTheme
  2. 将活动页面的XML文件根节点改成LinearLayout,且为vertical(垂直方向);然后增加一个Toolbar节点,因为Toolbar本质是一个ViewGrop,所以允许在内部添加其他控件。下面是Toolbar节点的XML例子:
<androidx.appcompat.widget.Toolbar
    android:id="@+id/tl_head"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
  1. 打开活动页面的Java代码,在onCreate方法中获取布局文件中的Toolbar对象,并调用setSupportActionBar方法设置当前的Toolbar对象。对应代码如下:
// 从布局文件中获取名叫tl_head的工具栏
Toolbar tl_head = findViewById(R.id.tl_head);
setSupportActionBar(tl_head); // 使用tl_head替换系统自带的ActionBar

Toolbar之所以比ActionBar灵活,原因之一是Toolbar提供了多个属性,方便定制各种控件风格。它的常用属性及其设置方法见下表:

XML中的属性Toolbar类的设置方法说明
logosetLogo设置工具栏图标
titlesetTitle设置标题文字
titleTextColorsetTitleTextColor设置标题的文字颜色
subtitlesetSubtitle设置副标题文字。副标题在标题下方
subtitleTextColorsetSubtitleTextColor设置副标题的文字颜色
navigationIconsetNavigationIcon设置左侧的箭头导航图标
setNavigationOnClickListener设置导航图标的点击监听器

结合上表提到的设置方法,下面是给Toolbar设置风格的代码例子:

// 从布局文件中获取名叫tl_head的工具栏
Toolbar tl_head = findViewById(R.id.tl_head); 
tl_head.setTitle("工具栏页面"); // 设置工具栏的标题文本
setSupportActionBar(tl_head); // 使用tl_head替换系统自带的ActionBar
tl_head.setTitleTextColor(Color.RED); // 设置工具栏的标题文字颜色
tl_head.setLogo(R.drawable.ic_app); // 设置工具栏的标志图片
tl_head.setSubtitle("Toolbar"); // 设置工具栏的副标题文本
tl_head.setSubtitleTextColor(Color.YELLOW); // 设置工具栏的副标题文字颜色
tl_head.setBackgroundResource(R.color.blue_light); // 设置工具栏的背景
tl_head.setNavigationIcon(R.drawable.ic_back); // 设置工具栏左边的导航图标
// 给tl_head设置导航图标的点击监听器
// setNavigationOnClickListener必须放到setSupportActionBar之后,不然不起作用
tl_head.setNavigationOnClickListener(view -> {
    finish(); // 结束当前页面
});

运行App,观察到工具栏效果如下图所示,可见工具栏包括导航箭头图标、工具栏图标、标题、副标题。
在这里插入图片描述

溢出菜单OverflowMenu

导航栏右边往往有3个小点图标,点击后会在界面右上角弹出菜单。这个菜单名为溢出菜单OverflowMenu,意思是导航栏不够放、溢出来了。溢出菜单的格式同前面小节“利用BottomNavigationView实现底部标签栏”介绍的导航菜单,它们都在res/menu下面的XML文件中定义,不过溢出菜单多了个app:showAsAction属性,该属性用来控制菜单在导航栏上的展示位置,位置类型的取值见下表:

展示位置属性说明
always总是在导航栏上显示菜单图标
ifRoom如果导航栏右侧有空间,该项就直接显示在导航栏上,不再放入溢出菜单
never从不在导航栏上直接显示,总是放在溢出菜单列表里面
withText如果能在导航栏上显示,除了显示图标,还要显示该菜单项的文字说明

注意,因为showAsAction是菜单的自定义属性,所以要现在菜单XML的menu根节点增加命名空间声明xmlns:app="http://schemas.android.com/apk/res-auto",这样showAsAction指定的位置类型才会生效。下面是一个包含3个菜单的溢出菜单XML例子:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" >
    <item
        android:id="@+id/menu_refresh"
        android:icon="@drawable/ic_refresh"
        app:showAsAction="ifRoom"
        android:title="刷新"/>
    <item
        android:id="@+id/menu_about"
        android:icon="@drawable/ic_about"
        app:showAsAction="never"
        android:title="关于"/>    
    <item
        android:id="@+id/menu_quit"
        android:icon="@drawable/ic_quit"
        app:showAsAction="never"
        android:title="退出"/>
</menu>

有了上面的菜单文件menu_overflow.xml,还得在活动代码中增加对菜单的处理逻辑。下面是在活动页面中操作溢出菜单的代码片段:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // 从menu_overflow.xml中构建菜单界面布局
    getMenuInflater().inflate(R.menu.menu_overflow, menu);
    return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId(); // 获取菜单项的编号
    if (id == android.R.id.home) { // 点击了工具栏左边的返回箭头
        finish(); // 结束当前页面
    } else if (id == R.id.menu_refresh) { // 点击了刷新图标
        tv_desc.setText("当前刷新时间: " + DateUtil.getNowTime());
    } else if (id == R.id.menu_about) { // 点击了关于菜单项
        Toast.makeText(this, "这个是工具栏的演示demo", Toast.LENGTH_LONG).show();
    } else if (id == R.id.menu_quit) { // 点击了退出菜单项
        finish(); // 结束当前页面
    }
    return super.onOptionsItemSelected(item);
}

运行App,打开添加了溢出菜单的导航栏页面,可见初始界面如下图所示,此时导航栏右侧有刷新按钮和3点图标:
在这里插入图片描述

点击3点图标,弹出剩余的菜单项列表,如下图所示。点击某个菜单项可触发对应的菜单事件。
在这里插入图片描述

标签布局TabLayout

Toolbar作为ActionBar的升级版,它不仅允许设置内部控件的样式,还允许添加其他外部控件。例如京东App的商品页面,既有如下图左侧所示的商品页,又有下图右侧所示的详情页。可见这个导航栏拥有一排文字标签,类似于翻页视图附属的翻页标题栏PagerTabStrip,商品页和详情页之间通过点击标签进行切换。
在这里插入图片描述
通过导航栏集成文字切换标签,有效提高了页面空间的利用效率,该功能用到了design库中的标签布局TabLayout。使用该控件前要先修改build.gradle.kts,在dependencies节点下加入以下配置表示导入design库:

implementation("com.google.android.material:material:1.12.0")

TabLayout的展现形式类似于PagerTabStrip,同样是文字标签下划线,不同的是TabLayout允许定制更丰富的样式,它增加的新样式属性主要有下列6种:

  • tabBackground:指定标签的背景。
  • tabIndicatorColor:指定下划线颜色。
  • tabIndicatorHeight:指定下划线的高度。
  • tabTextColor:指定标签文字的颜色。
  • tabTextAppearance:指定标签文字的风格。样式风格来自styles.xml中的定义。
  • tabSelectedTextColor:指定选中文字的颜色。

下面是在XML文件中通过Toolbar集成TabLayout的内容片段:

<androidx.appcompat.widget.Toolbar
    android:id="@+id/tl_head"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    app:navigationIcon="@drawable/ic_back">
    <!-- 注意TabLayout节点需要使用完整路径 -->
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_title"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        app:tabIndicatorColor="@color/red"
        app:tabIndicatorHeight="2dp"
        app:tabSelectedTextColor="@color/red"
        app:tabTextColor="@color/grey"
        app:tabTextAppearance="@style/TabText" />
</androidx.appcompat.widget.Toolbar>

在Java代码中,TabLayout通过以下4个方法操作文字标签。

  • newTab:创建新标签。
  • addTab:添加一个标签。
  • getTabAt:获取指定位置的标签。
  • setOnTabSelectedListener:设置标签的选中监听器。该监听器需要实现OnTabSelectedListener接口的3个方法。
    (1)onTabSelected:标签被选中时触发。
    (2)onTabUnselected:标签被取消选中时触发。
    (3)onTabReselected:标签被重新选中时触发。

把TabLayout与ViewPager结合起来就是一个固定的套路,二者各自通过选中监听器或者翻页监听器控制页面切换,使用时直接套框架就行。下面时两者联合使用的代码例子:

public class TabLayoutActivity extends AppCompatActivity implements OnTabSelectedListener {
    private final static String TAG = "TabLayoutActivity";
    private ViewPager vp_content; // 声明一个翻页视图对象
    private TabLayout tab_title; // 声明一个标签布局对象
    private String[] mTitleArray = {"商品", "详情"}; // 标题文字数组

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_layout);
        Toolbar tl_head = findViewById(R.id.tl_head); // 从布局文件中获取名叫tl_head的工具栏
        tl_head.setTitle(""); // 设置工具栏的标题文本
        setSupportActionBar(tl_head); // 使用tl_head替换系统自带的ActionBar
        tab_title = findViewById(R.id.tab_title); // 从布局文件中获取名叫tab_title的标签布局
        vp_content = findViewById(R.id.vp_content); // 从布局文件中获取名叫vp_content的翻页视图
        initTabLayout(); // 初始化标签布局
        initTabViewPager(); // 初始化标签翻页
    }

    // 初始化标签布局
    private void initTabLayout() {
        // 给标签布局添加一个文字标签
        tab_title.addTab(tab_title.newTab().setText(mTitleArray[0]));
        // 给标签布局添加一个文字标签
        tab_title.addTab(tab_title.newTab().setText(mTitleArray[1]));
        tab_title.addOnTabSelectedListener(this); // 给标签布局添加标签选中监听器
        // 监听器ViewPagerOnTabSelectedListener允许直接关联某个翻页视图
        //tab_title.addOnTabSelectedListener(new ViewPagerOnTabSelectedListener(vp_content));
    }

    // 初始化标签翻页
    private void initTabViewPager() {
        // 构建一个商品信息的翻页适配器
        GoodsPagerAdapter adapter = new GoodsPagerAdapter(
                getSupportFragmentManager(), mTitleArray);
        vp_content.setAdapter(adapter); // 设置翻页视图的适配器
        // 给vp_content添加页面变更监听器
        vp_content.addOnPageChangeListener(new SimpleOnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                tab_title.getTabAt(position).select(); // 选中指定位置的标签
            }
        });
    }

    // 在标签被重复选中时触发
    public void onTabReselected(Tab tab) {}

    // 在标签选中时触发
    public void onTabSelected(Tab tab) {
        vp_content.setCurrentItem(tab.getPosition()); // 设置翻页视图显示第几页
    }

    // 在标签取消选中时触发
    public void onTabUnselected(Tab tab) {}
}

运行App测试,准备观察标签布局的界面效果。先点击“商品”标签,此时页面显示商品的图片概览,如下图左侧所示;再点击“详情”标签,切换到商品的详情页面,如下图右侧所示。
在这里插入图片描述

增强型列表

本节介绍如何利用循环试图Recyclerview实现3种增强型列表,包括线性列表布局、普通网格布局、瀑布流网格布局等,以及如何动态更新循环视图内部的列表项数据。

循环视图Recyclerview

尽管ListView和GridView分别实现了多行单列和多行多列的列表,使用也很简单,可是它们缺少变化,风格也比较呆板。为此Android推出了更灵活多变的循环视图Recyclerview,它的功能非常强大,不但足以囊括列表视图和网格视图,还能实现高度错开的瀑布网格效果。总之,只要学会RecyclerView,就相当于同时掌握了ListView、GridView,再加上瀑布流一共3种列表界面。
由于RecyclerView来自recyclerview库,因此在使用RecyclerView前要修改build.gradle.kts,在dependencies节点中加入以下配置表示导入recyclerview库:

implementation("androidx.recyclerview:recyclerview:1.3.2")

下面是RecyclerView的常用方法说明:

  • setAdapter:设置列表项的循环适配器。适配器采用RecyclerView.Adapter。
  • setLayoutManager:设置列表项的布局管理器。管理器一共3种,包括线性布局管理器LinearLayoutManager、网格布局管理器GridLayoutManager、瀑布流网格布局管理器StaggeredGridLayoutManager。
  • addItemDecoration:添加列表项的变更动画。默认动画为DefaultItemAnimator。
  • scrollToPosition:滚动到指定位置。

循环视图有专门的循环适配器RecyclerView.Adapter,在setAdapter方法之前,得先实现一个从RecyclerView.Adapter派生而来的适配器,用来定义列表项的界面布局及其控件操作。下面是实现循环适配器时有待重写的方法说明。

  • getItemCount:获得列表项的数目。
  • onCreateViewHolder:创建整个布局的视图特有持有者,可在该方法中指定列表项的布局文件。第二个参数为视图类型viewType,根据视图类型加载不同的布局,从而实现带头部的列表布局。
  • getItemViewType:返回每一项的视图类型。这里的类型与onCreateViewHolder方法的viewType参数保持一致。
  • getItemId:获得每个列表项的编号。

以上两个方法不是必须的,可以重写也可以不重写。
举个公众号消息列表的例子,若要通过循环适配器实现的话,需要让自定义的适配器完成下列步骤:

  1. 在构造方法中传入消息列表。
  2. 重写getItemCount方法,返回列表项的个数。
  3. 定义一个由RecyclerView.ViewHolder派生而来的内部类,用作列表项的视图持有者。
  4. 重写onCreateViewHolder方法,根据指定的布局文件生成视图对象,并返回该视图对象对应的视图持有者。
  5. 重写onBindViewHolder方法,从参数中的视图持有着获取各个控件实例,再操纵这些控件(设置文字、设置图片、设置点击监听器等)。

根据上述步骤编写而成的循环适配器代码示例如下:

public class RecyclerLinearAdapter extends RecyclerView.Adapter<ViewHolder> {
    private Context mContext; // 声明一个上下文对象
    private List<NewsInfo> mPublicList; // 公众号列表

    public RecyclerLinearAdapter(Context context, List<NewsInfo> publicList) {
        mContext = context;
        mPublicList = publicList;
    }

    // 获取列表项的个数
    public int getItemCount() {
        return mPublicList.size();
    }

    // 创建列表项的视图持有者
    public ViewHolder onCreateViewHolder(ViewGroup vg, int viewType) {
        // 根据布局文件item_linear.xml生成视图对象
        View v = LayoutInflater.from(mContext).inflate(R.layout.item_linear, vg, false);
        return new ItemHolder(v);
    }

    // 绑定列表项的视图持有者
    public void onBindViewHolder(ViewHolder vh, final int position) {
        ItemHolder holder = (ItemHolder) vh;
        holder.iv_pic.setImageResource(mPublicList.get(position).pic_id);
        holder.tv_title.setText(mPublicList.get(position).title);
        holder.tv_desc.setText(mPublicList.get(position).desc);
    }
    
    // 定义列表项的视图持有者
    public class ItemHolder extends ViewHolder {
        public ImageView iv_pic; // 声明列表项图标的图像视图
        public TextView tv_title; // 声明列表项标题的文本视图
        public TextView tv_desc; // 声明列表项描述的文本视图

        public ItemHolder(View v) {
            super(v);
            iv_pic = v.findViewById(R.id.iv_pic);
            tv_title = v.findViewById(R.id.tv_title);
            tv_desc = v.findViewById(R.id.tv_desc);
        }
    }
}

回到活动页面,由循环视图对象调用setAdapter方法设置适配器,具体的调用代码如下:

// 初始化线性布局的循环视图
private void initRecyclerLinear() {
    // 从布局文件中获取名叫rv_linear的循环视图
    RecyclerView rv_linear = findViewById(R.id.rv_linear);
    // 创建一个垂直方向的线性布局管理器
    LinearLayoutManager manager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
    rv_linear.setLayoutManager(manager); // 设置循环视图的布局管理器
    // 构建一个公众号列表的线性适配器
    RecyclerLinearAdapter adapter = new RecyclerLinearAdapter(this, NewsInfo.getDefaultList());
    rv_linear.setAdapter(adapter);  // 设置循环视图的线性适配器
}

运行App,观察到公众号消息界面如下图所示,可见该效果仿照微信公众号的消息列表,看起来像是用ListView实现的。
在这里插入图片描述
注意:循环视图并未提供点击监听器和长按监听器,若想让列表项能够响应点击事件,则需再适配器的onBindViewHolder方法中给列表的根布局注册点击监听器,代码示例如下:

// 列表项的点击事件需要自己实现。ll_item为列表项的根布局
holder.ll_item.setOnClickListener(v -> {
	// 这里补充点击事件的处理代码
});

布局管理器LayoutManager

循环视图之所以能够变身为3种列表,是因为它允许指定不同的列表布局,这正是布局管理器LayoutManager的拿手好戏。LayoutManager不但提供了3类布局管理,分别实现类似列表视图、网格视图、瀑布流网格的效果,而且可由循环视图对象随时调用setLayoutManager方法设置新布局。一旦调用了setLayoutManager方法,界面就会根据新布局刷新列表项。此特性特别适用于手机在竖屏与横屏之间的显示切换(如竖屏时展示列表,横屏时展示网格),也适用于在不同屏幕尺寸(手机与平板)之间的显示切换(如在手机上展示列表,在平板上展示网格)。接下来分别介绍循环视图的3类布局管理器。

线性布局管理器LinearLayoutManager

LinearLayoutManager可看作是线性布局LinearLayout,它在垂直方向布局时,展示效果类似于列表视图ListView;在水平方向布局时,展示效果类似于水平方向的列表视图。
下面是LinearLayoutManager的常用方法。

  • 构造方法:第二个参数指定了布局方向,RecyclerView.HORIZONTAL表示水平,RecyclerView.VERTICAL表示垂直;第三个参数指定了是否从相反方向开始布局。
  • setOrientation:设置布局方向,RecyclerView.HORIZONTAL表示水平方向,RecyclerView.VERTICAL表示垂直方向。
  • setReverseLayout:设置是否从相反方向开始布局,默认为false。如果设置为true,那么垂直方向将从下往上开始布局,水平方向将从右往左开始布局。
    前面在介绍循环视图时,采用了最简单的线性布局管理器,虽然调用addItemDecoration方法能够添加列表项的分割线,但是RecyclerView并未提供默认的分割线,需要先由开发者自定义分割线的样式,再调用addItemDecoration方法设置分割线样式。下面是个允许设置线宽的分割线实现代码:
public class SpacesDecoration extends RecyclerView.ItemDecoration {
    private int space; // 空白间隔

    public SpacesDecoration(int space) {
        this.space = space;
    }

    @Override
    public void getItemOffsets(Rect outRect, View v, RecyclerView parent, RecyclerView.State state) {
        outRect.left = space; // 左边空白间隔
        outRect.right = space; // 右边空白间隔
        outRect.bottom = space; // 上方空白间隔
        outRect.top = space; // 下方空白间隔
    }
}

网格布局管理器GridLayoutManager

GridLayoutManager可看作是网格布局GridLayout,从展示效果来看,GridLayoutManager类似于网格视图GridView。不管是GridLayout还是GridView,抑或GridLayoutManager,都呈现多行多列的网格布局。
下面是GridLayoutManager的常用方法。

  • 构造方法:第二个参数指定了网格列数。
  • setSpanCount:设置了网格的列数。
  • setSpanSizeLookup:设置网格项的占位规则。默认一个网格项占一列,若想某个网格项占多列,就可在此设置占位规则。

下面是在活动页面种给循环视图设置网格布局管理器的代码例子:

// 初始化网格布局的循环视图
private void initRecyclerGrid() {
    // 从布局文件中获取名叫rv_grid的循环视图
    RecyclerView rv_grid = findViewById(R.id.rv_grid);
    // 创建一个网格布局管理器(每行5列)
    GridLayoutManager manager = new GridLayoutManager(this, 5);
    rv_grid.setLayoutManager(manager); // 设置循环视图的布局管理器
    // 构建一个市场列表的网格适配器
    RecyclerGridAdapter adapter = new RecyclerGridAdapter(this, NewsInfo.getDefaultGrid());
    adapter.setOnItemClickListener(adapter); // 设置网格列表的点击监听器
    adapter.setOnItemLongClickListener(adapter); // 设置网格列表的长按监听器
    rv_grid.setAdapter(adapter); // 设置循环视图的网格适配器
    rv_grid.setItemAnimator(new DefaultItemAnimator());  // 设置循环视图的动画效果
    rv_grid.addItemDecoration(new SpacesDecoration(1));  // 设置循环视图的空白装饰
}

运行App,观察到网格布局管理器的循环视图界面如下图所示,看起来跟GridView的展示效果没什么区别。
在这里插入图片描述
但GridLayoutManager绝非GridView可比,因为它还提供了setSpanSizeLookup方法,该方法允许一个网格占据多列,展示更加灵活。下面是使用占位规则的网格管理器代码例子:

// 初始化合并网格布局的循环视图
private void initRecyclerCombine() {
    // 从布局文件中获取名叫rv_combine的循环视图
    RecyclerView rv_combine = findViewById(R.id.rv_combine);
    // 创建一个四列的网格布局管理器
    GridLayoutManager manager = new GridLayoutManager(this, 4);
    // 设置网格布局管理器的占位规则。以下规则为:第一项和第二项占两列,其他项占一列;
    // 如果网格的列数为四,那么第一项和第二项平分第一行,第二行开始每行有四项。
    manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            if (position == 0 || position == 1) { // 为第一项或者第二项
                return 2; // 占据两列
            } else { // 为其它项
                return 1; // 占据一列
            }
        }
    });
    rv_combine.setLayoutManager(manager); // 设置循环视图的布局管理器
    // 构建一个猜你喜欢的网格适配器
    RecyclerCombineAdapter adapter = new RecyclerCombineAdapter(
            this, NewsInfo.getDefaultCombine());
    adapter.setOnItemClickListener(adapter); // 设置网格列表的点击监听器
    adapter.setOnItemLongClickListener(adapter); // 设置网格列表的长按监听器
    rv_combine.setAdapter(adapter); // 设置循环视图的网格适配器
    rv_combine.setItemAnimator(new DefaultItemAnimator());  // 设置循环视图的动画效果
    rv_combine.addItemDecoration(new SpacesDecoration(1));  // 设置循环视图的空白装饰
}

运行App,观察到占位规则的界面效果如下图所示。可见第一行只有两个网格,而第二行右4个网格,这意味着第一行的每个网格都占据两列位置。
在这里插入图片描述

瀑布流网格布局管理器StaggeredGridLayoutManager

电商App在展示众多商品信息时,往往通过高矮不一的各自展示。因为不同商品的外观尺寸不一样,比如冰箱在纵向比较长,空调在横向比较长,所以若用一样规格的网格展示,必然导致有的商品图片会被压缩得很小。像这种根据不同得商品形状展示不同高度的图片,就是瀑布流网格的应用场合。自从有了瀑布流网格布局管理器StaggeredGridLayoutManager,瀑布流效果的开发过程便大大简化了,只要在循环适配器中动态设置每个网格的高度,系统就会在界面上自动排列瀑布流网格。
下面是StaggeredGridLayoutManager的常用方法。

  • 构造函数:第一个参数指定了瀑布流网格每行的列数;第二个参数指定了瀑布流布局的方向,取值说明同LinearLayoutManager。
  • setSpanCount:设置瀑布流网格每行的列数。
  • setOrientation:设置瀑布流布局的方向。取值说明同LinearLayoutManager。
  • setReverseLayout:设置是否从相反方向开始布局,默认为false。如果设置为true,那么垂直方向将从下往上开始布局,水平方向将从右往左开始布局。

下面是在活动页面中操作瀑布流网格布局管理器的代码例子:

// 初始化瀑布流布局的循环视图
private void initRecyclerStaggered() {
    // 从布局文件中获取名叫rv_staggered的循环视图
    RecyclerView rv_staggered = findViewById(R.id.rv_staggered);
    // 创建一个垂直方向的瀑布流布局管理器(每行3列)
    StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(
            3, RecyclerView.VERTICAL);
    rv_staggered.setLayoutManager(manager); // 设置循环视图的布局管理器
    // 构建一个服装列表的瀑布流适配器
    RecyclerStagAdapter adapter = new RecyclerStagAdapter(this, NewsInfo.getDefaultStag());
    adapter.setOnItemClickListener(adapter); // 设置瀑布流列表的点击监听器
    adapter.setOnItemLongClickListener(adapter); // 设置瀑布流列表的长按监听器
    rv_staggered.setAdapter(adapter);  // 设置循环视图的瀑布流适配器
    rv_staggered.setItemAnimator(new DefaultItemAnimator());  // 设置循环视图的动画效果
    rv_staggered.addItemDecoration(new SpacesDecoration(3));  // 设置循环视图的空白装饰
}

运行App,观察到瀑布流网格布局的效果如下图,每个网格的高度依照具体图片的高度变化而变化,使得整个页面更加生动活泼。
在这里插入图片描述

动态更新循环视图

循环视图不但支持多种布局,而且更新内部数据也很方便。原先列表视图或者网格视图若想更新列表项,只能调用setAdapter方法重新设置适配器,或者由适配器对象调用notifyDataSetChanged刷新整个列表界面,可是这两种更新方式都得重新加载全部列表项,非常低效。相比之下,循环视图允许动态更新局部记录,既能对一条列表项单独添加/修改/删除,也能更新全部列表项。这种动态更新功能用到了循环适配器对象的下列方法:

  • notifyItemInserted:通知适配器在指定位置插入了新项。
  • notifyItemRemoved:通知适配器在指定位置删除了原有项。
  • notifyItemChanged:通知适配器在指定位置发生了数据变化。此时循环试图会刷新指定位置的列表项。
  • notifyDataSetChanged:通知适配器整个列表的数据发生了变化。

动态更新列表项不仅只是功能上的增强,在更新之时还能展示变更动画,这是循环视图在用户体验上的优化,自从有了变更动画,列表项的增删动作看起来更加柔和,不再像列表视图或者网格视图那么呆板了,总之,只要用上了循环试图,你一定会对它爱不释手。
以公众号消息列表的更新操作为例,往循环视图顶部添加一条消息的步骤如下:

public class RecyclerDynamicActivity extends AppCompatActivity implements View.OnClickListener {
    private RecyclerView rv_dynamic; // 声明一个循环视图对象
    private LinearDynamicAdapter mAdapter; // 声明一个线性适配器对象
    private List<NewsInfo> mPublicList = NewsInfo.getDefaultList(); // 当前的公众号信息列表
    private List<NewsInfo> mOriginList = NewsInfo.getDefaultList(); // 原始的公众号信息列表

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_dynamic);
        findViewById(R.id.btn_recycler_add).setOnClickListener(this);
        initRecyclerDynamic(); // 初始化动态线性布局的循环视图
    }

    // 初始化动态线性布局的循环视图
    private void initRecyclerDynamic() {
        // 从布局文件中获取名叫rv_dynamic的循环视图
        rv_dynamic = findViewById(R.id.rv_dynamic);
        // 创建一个垂直方向的线性布局管理器
        LinearLayoutManager manager = new LinearLayoutManager(
                this, RecyclerView.VERTICAL, false);
        rv_dynamic.setLayoutManager(manager); // 设置循环视图的布局管理器
        // 构建一个公众号列表的线性适配器
        mAdapter = new LinearDynamicAdapter(this, mPublicList);
        mAdapter.setOnItemClickListener(this); // 设置线性列表的点击监听器
        mAdapter.setOnItemLongClickListener(this); // 设置线性列表的长按监听器
        mAdapter.setOnItemDeleteClickListener(this); // 设置线性列表的删除按钮监听器
        rv_dynamic.setAdapter(mAdapter); // 设置循环视图的线性适配器
        rv_dynamic.setItemAnimator(new DefaultItemAnimator());  // 设置循环视图的动画效果
        rv_dynamic.addItemDecoration(new SpacesDecoration(1));  // 设置循环视图的空白装饰
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_recycler_add) {
            int position = new Random().nextInt(mOriginList.size()-1); // 获取一个随机位置
            NewsInfo old_item = mOriginList.get(position);
            NewsInfo new_item = new NewsInfo(old_item.pic_id, old_item.title, old_item.desc);
            mPublicList.add(0, new_item); // 在顶部添加一条公众号消息
            mAdapter.notifyItemInserted(0); // 通知适配器列表在第一项插入数据
            rv_dynamic.scrollToPosition(0); // 让循环视图滚动到第一项所在的位置
        }
    }
}

运行App,观察到动态添加消息的界面效果如下图。其中,下图左侧为公众号消息列表的初始界面;点击“增加新聊天”按钮,在列表顶部新增一条公众号消息,注意在添加消息的时候会显示变更动画,下图右侧为动画结束后的公众号界面。
在这里插入图片描述

升级版翻页

本章介绍如何使用循环视图的扩展功能:首先引入下拉刷新布局SwipeRefreshLayout,并说明如何通过下拉刷新动态更新循环视图;然后描述第二代翻页视图ViewPager2的基本用法,以及如何给ViewPager2搭档循环适配器;最后阐述如何给ViewPager2搭档专门的翻页适配器,以及如何集成标签布局。

下拉刷新布局SwipeRefreshLayout

电商App在商品列表页面提供了下拉功能,在屏幕顶端向下滑动即可触发页面的刷新操作,该功能用到了下拉刷新布局SwipeRefreshLayout。在使用SwipeRefreshLayout前要修改build.gradle.kts,在dependencies节点中加入以下配置表示导入swiperefreshlayout库:

implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

下面是SwipeRefreshLayout的常用方法说明。

  • setOnRefreshListener:设置刷新监听器。需要重写监听器OnRefreshListener的onRefresh方法,该方法在下拉松开时触发。
  • setRefreshing:设置刷新的状态。true表示正在刷新,false表示结束刷新。
  • isRefreshing:判断是否正在刷新。
  • setColorSchemeColors:设置进度圆圈的圆环颜色。

在XML文件中,SwipeRefreshLayout节点内部有且仅有一个直接子视图。如果存在多个直接子视图,那么只会展示第一个子视图,后面的子视图将不予展示。并且直接子视图必须允许滚动,包括滚动视图ScrollView、列表视图ListView、网格视图GridView、循环视图RecyclerView等。如果不是这些视图,当前界面就不支持滚动,更不支持下拉刷新。以循环视图为例,通过下拉刷新动态添加列表记录,从而省去一个控制按钮,避免按钮太多显得界面凌乱。
下面是结合SwipeRefreshLayout与RecyclerView的XML文件例子:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp">
    <!-- 注意SwipeRefreshLayout要使用全路径 -->
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/srl_dynamic"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <!-- 注意RecyclerView要使用全路径 -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_dynamic"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#aaaaff" />
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

与上面的XML文件对应的活动页面代码示例如下:

public class SwipeRecyclerActivity extends AppCompatActivity implements OnRefreshListener {
    private SwipeRefreshLayout srl_dynamic; // 声明一个下拉刷新布局对象
    private RecyclerView rv_dynamic; 		// 声明一个循环视图对象
    private LinearDynamicAdapter mAdapter;  // 声明一个线性适配器对象
    private List<NewsInfo> mPublicList = NewsInfo.getDefaultList(); // 当前的公众号信息列表
    private List<NewsInfo> mOriginList = NewsInfo.getDefaultList(); // 原始的公众号信息列表

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_swipe_recycler);
        // 从布局文件中获取名叫srl_dynamic的下拉刷新布局
        srl_dynamic = findViewById(R.id.srl_dynamic);
        srl_dynamic.setOnRefreshListener(this); // 设置下拉布局的下拉刷新监听器
        // 设置下拉刷新布局的进度圆圈颜色
        srl_dynamic.setColorSchemeResources(
                R.color.red, R.color.orange, R.color.green, R.color.blue);
        initRecyclerDynamic(); // 初始化动态线性布局的循环视图
    }

    // 初始化动态线性布局的循环视图
    private void initRecyclerDynamic() {
        // 从布局文件中获取名叫rv_dynamic的循环视图
        rv_dynamic = findViewById(R.id.rv_dynamic);
        // 创建一个垂直方向的线性布局管理器
        LinearLayoutManager manager = new LinearLayoutManager(
                this, RecyclerView.VERTICAL, false);
        rv_dynamic.setLayoutManager(manager); // 设置循环视图的布局管理器
        // 构建一个公众号列表的线性适配器
        mAdapter = new LinearDynamicAdapter(this, mPublicList);
        rv_dynamic.setAdapter(mAdapter);  // 设置循环视图的线性适配器
    }

    // 一旦在下拉刷新布局内部往下拉动页面,就触发下拉监听器的onRefresh方法
    public void onRefresh() {
        mHandler.postDelayed(mRefresh, 2000); // 模拟网络耗时,延迟若干秒后启动刷新任务
    }

    private Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象
    // 定义一个刷新任务
    private Runnable mRefresh = new Runnable() {
        @Override
        public void run() {
            srl_dynamic.setRefreshing(false); // 结束下拉刷新布局的刷新动作
            int position = new Random().nextInt(mOriginList.size()-1); // 获取一个随机位置
            NewsInfo old_item = mOriginList.get(position);
            NewsInfo new_item = new NewsInfo(old_item.pic_id, old_item.title, old_item.desc);
            mPublicList.add(0, new_item); // 在顶部添加一条公众号消息
            mAdapter.notifyItemInserted(0); // 通知适配器列表在第一项插入数据
            rv_dynamic.scrollToPosition(0); // 让循环视图滚动到第一项所在的位置
        }
    };
}

运行App,打开公众号列表界面,在屏幕顶端下拉再松手,此时页面上方弹出转圈提示正在刷新,如下图左侧;稍等片刻结束刷新,此时列表顶端增加了一条新消息,如下图右侧所示。
在这里插入图片描述

第二代翻页视图ViewPager2

正如RecyclerView横空出世取代ListView个GridView那样,Android也推出了二代翻页视图ViewPager2,打算替换原来的翻页视图ViewPager2。与ViewPager相比,ViewPager2支持更丰富的界面特效,包括但不限于下列几点:

  1. 不但支持水平方向翻页,还支持垂直方向翻页。
  2. 支持RecyclerView.Adapter,也允许调用适配器对象的notifyItem***方法,从而动态刷新某个页面项。
  3. 除了展示当前页,也支持展示左、右两页的部分区域。
  4. 支持在翻页过程中展示自定义的切换动画。

虽然ViewPager2增加了这么棒的功能,但它用起来很简单,掌握下面几个方法就够了:

  • setAdapter:设置二代翻页视图的页面适配器。
  • setOrientation:设置二代翻页视图的翻页方向。其中ORIENTATION_HORIZONTAL表示水平方向,ORIENTATION_VERTICAL表示垂直方向。
  • setPageTransformer:设置二代翻页视图的页面转换器,以便展示切换动画。

接下来利用循环适配器搭档二代翻页视图,演示看看ViewPager2的界面效果。注意,因为RecyclerView与ViewPager2拥有各自的依赖库,所以需要修改模块build.gradle.kts,在dependencies节点内部补充以下两行依赖配置:

implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.viewpager2:viewpager2:1.1.0")

接着建一个活动页面,往该页面XML文件中添加如下所示的ViewPager2标签:

<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/vp2_content"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />

由于ViewPager2仍然需要适配器,因此首先编写每个页面项的布局文件,下面便是一个页面项的XML例子,页面上方时图像视图,下方是文本视图。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/iv_pic"
        android:layout_width="match_parent"
        android:layout_height="360dp"
        android:scaleType="fitCenter" />
    <TextView
        android:id="@+id/tv_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="17sp" />
</LinearLayout>

然后给上面的页面项补充对应的循环适配器代码,在适配器的构造方法中传入一个商品列表,再展示每个商品的图片与文字描述。循环适配器的代码示例如下:

public class MobileRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext; // 声明一个上下文对象
    private List<GoodsInfo> mGoodsList = new ArrayList<GoodsInfo>(); // 声明一个商品列表

    public MobileRecyclerAdapter(Context context, List<GoodsInfo> goodsList) {
        mContext = context;
        mGoodsList = goodsList;
    }
    
    // 创建列表项的视图持有者
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup vg, int viewType) {
        // 根据布局文件item_mobile.xml生成视图对象
        View v = LayoutInflater.from(mContext).inflate(R.layout.item_mobile, vg, false);
        return new ItemHolder(v);
    }

    // 绑定列表项的视图持有者
    public void onBindViewHolder(RecyclerView.ViewHolder vh, final int position) {
        ItemHolder holder = (ItemHolder) vh;
        holder.iv_pic.setImageResource(mGoodsList.get(position).pic);
        holder.tv_desc.setText(mGoodsList.get(position).desc);
    }

    // 定义列表项的视图持有者
    public class ItemHolder extends RecyclerView.ViewHolder {
        public ImageView iv_pic; // 声明列表项图标的图像视图
        public TextView tv_desc; // 声明列表项描述的文本视图

        public ItemHolder(View v) {
            super(v);
            iv_pic = v.findViewById(R.id.iv_pic);
            tv_desc = v.findViewById(R.id.tv_desc);
        }
    }
}

回到测试页面的Java代码,把二代翻页视图的排列方向设为水平方向,并将它的适配器设置为上述的循环适配器。只要以下几行代码就搞定了:

// 从布局文件中获取名叫vp2_content的二代翻页视图
ViewPager2 vp2_content = findViewById(R.id.vp2_content);
// 设置二代翻页视图的排列方向为水平方向
vp2_content.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
// 构建一个商品信息列表的循环适配器
MobileRecyclerAdapter adapter = new MobileRecyclerAdapter(this, GoodsInfo.getDefaultList());
vp2_content.setAdapter(adapter); // 设置二代翻页视图的适配器

运行App,观察二代翻页视图的展示效果,其中水平方向的翻页过程如下图所示:
在这里插入图片描述
现在我们再在页面代码中添加以下代码:

// ViewPager2支持展示左右两页的部分区域
RecyclerView cv_content = (RecyclerView) vp2_content.getChildAt(0);
cv_content.setPadding(Utils.dip2px(this, 60), 0, Utils.dip2px(this, 60), 0);
cv_content.setClipToPadding(false); // false表示不裁剪下级视图

重新编译运行App,此时页面效果如下图,可见除了商品之外,左右两边呈现了边缘区域。
在这里插入图片描述
撤销刚刚添加的边缘特效代码,再给测试页面的活动代码补充下面几行代码:

// ViewPager2支持在翻页时展示切换动画,通过页面转换器计算切换动画的各项参数
ViewPager2.PageTransformer animator = new ViewPager2.PageTransformer() {
    @Override
    public void transformPage(@NonNull View page, float position) {
        page.setRotation(position * 360); // 设置页面的旋转角度
    }
};
vp2_content.setPageTransformer(animator); // 设置二代翻页视图的页面转换器

重新运行App,此时翻页过程如下图所示,从中可见翻页时展示了旋转动画。
在这里插入图片描述

给ViewPager2集成标签布局

ViewPager2不仅支持循环适配器,同样支持翻页适配器,还是新的哦!原先ViewPager采用的翻页适配器叫做FragmentPagerAdapter,而ViewPager2采用了FragmentStateAdapter,开起来仅仅差了个“Pager”,实际上差得远了,因为新适配器FragmentStateAdapter继承了循环适配器RecyclerView.Adapter。尽管它们都支持碎片Fragment,但具体的方法就不一样了。新、旧适配器的实现方法对比表如下:

旧翻页适配器的方法来自FragmentPagerAdapter新翻页适配器的方法来自FragmentStateAdapter说明
FragmentPagerAdapter(fm: FragmentManager)FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity)构造方法
getCountgetItemCount获取碎片个数
getItemcreateFragment创建指定位置的碎片
getPageTitle获得指定位置的标题

比如下面是采用FragmentStateAdapter的翻页适配器代码例子:

public class MobilePagerAdapter extends FragmentStateAdapter {
    private List<GoodsInfo> mGoodsList = new ArrayList<GoodsInfo>(); // 声明一个商品列表
    // 碎片页适配器的构造方法,传入碎片管理器与商品信息列表
    public MobilePagerAdapter(FragmentActivity fa, List<GoodsInfo> goodsList) {
        super(fa);
        mGoodsList = goodsList;
    }
    // 创建指定位置的碎片Fragmen
    @Override
    public Fragment createFragment(int position) {
        return MobileFragment.newInstance(position,
                mGoodsList.get(position).pic, mGoodsList.get(position).desc);
    }
    // 获取碎片Fragment的个数
    @Override
    public int getItemCount() {
        return mGoodsList.size();
    }
}

新适配器集成的碎片MobileFragment代码参见fragment\MobileFragment.java,该碎片的功能同上一节,依旧传入商品列表,然后展示每个商品的图片与文字描述。运行测试App观察到的界面效果跟循环适配器差不多,因为展示商品信息的场景比较简单,所以循环适配器和翻页适配器看不出区别。就实际开发而言,简单的业务场景适合采用循环适配器,复杂的业务场景适合采用翻页适配器。
ViewPager有个标签栏搭档PagerTabStrip,然而ViewPager2抛弃了PagerTabStrip,直接跟TabLayout搭配了。可是在“标签布局TabLayout”小节中,为了让ViewPager联动TabLayout,着实费了不少功夫。先给ViewPager添加页面变更监听器,一旦听到翻页事件就切换对应标签;再给TabLayout注册标签选中监听器,一旦监听到标签事件就翻到对应的页面。现在有了ViewPager2,搭配TabLayout便轻松多了,只要一行代码即可绑定ViewPager2与TabLayout。下面是将二者联结起来的操作步骤。

  1. 创建测试页面,并往页面的XML文件先后加入TabLayout标签和ViewPager2标签,具体代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- 标签布局TabLayout节点需要使用完整路径 -->
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <!-- 二代翻页视图ViewPager2节点也需要使用完整路径 -->
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/vp2_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>
  1. 打开该页面的Java代码,分别获取TabLayout和ViewPager2的视图对象,再利用TabLayoutMediator把标签布局跟翻页视图连为一体,完整代码示例如下:
public class ViewPager2FragmentActivity extends AppCompatActivity {
    private List<GoodsInfo> mGoodsList = GoodsInfo.getDefaultList(); // 商品信息列表

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_pager2_fragment);
        // 从布局文件中获取名叫tab_title的标签布局
        TabLayout tab_title = findViewById(R.id.tab_title);
        // 从布局文件中获取名叫vp2_content的二代翻页视图
        ViewPager2 vp2_content = findViewById(R.id.vp2_content);
        // 构建一个商品信息的翻页适配器
        MobilePagerAdapter adapter = new MobilePagerAdapter(this, mGoodsList);
        vp2_content.setAdapter(adapter); // 设置二代翻页视图的适配器
        // 把标签布局跟翻页视图通过指定策略连为一体,二者在页面切换时一起联动
        new TabLayoutMediator(tab_title, vp2_content, new TabLayoutMediator.TabConfigurationStrategy() {
            @Override
            public void onConfigureTab(TabLayout.Tab tab, int position) {
                tab.setText(mGoodsList.get(position).name); // 设置每页的标签文字
            }
        }).attach();
    }
}

重新运行App,初始的演示页面如下图左侧所示;接着点击上方标签,此时页面下方翻到了对应商品。
在这里插入图片描述

工程源码

文章设计所有代码可点击工程源码下载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/671946.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux系统tab键无法补齐命令-已解决

在CentOS中&#xff0c;按下tab键就可以自动补全&#xff0c;但是在最小化安装时&#xff0c;没有安装自动补全的包&#xff0c;需要安装一个包才能解决 bash-completion 1.检查是否安装tab补齐软件包&#xff08;如果是最小化安装&#xff0c;默认没有&#xff09; rpm -q ba…

提莫攻击 ---- 模拟算法

题目链接 题目: 分析: 如果两次攻击的时间差是>中毒的持续时间duration, 那么第一次攻击的中毒时间就是duration如果两次攻击的时间差是< 中毒的持续时间duration, 那么第一次攻击的持续时间就是这个时间差假设攻击了n次, 那么我们从第一次攻击开始计算时间差, 那么当我…

Halo DB 魔法之 pg_pcpu_limit

↑ 关注「少安事务所」公众号&#xff0c;欢迎⭐收藏&#xff0c;不错过精彩内容~ 前情回顾 前面已经介绍了“光环”数据库的基本情况和安装办法&#xff0c;今天来介绍一个新话题。 哈喽&#xff0c;国产数据库&#xff01;Halo DB! 三步走&#xff0c;Halo DB 安装指引 ★ Ha…

C++ A (1020) : 幂运算

文章目录 一、题目描述二、参考代码 一、题目描述 二、参考代码 #include<bits/stdc.h> using namespace std; typedef long long ll;void qq(ll a, ll b, ll m) {if (a 0) cout << 0 << endl;;ll out 1;a % m;while (b > 0){if (b & 1)//奇数的最…

LeetCode17电话号码的字母组合

题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 解析 广度优先遍历或者深度优先遍历两种方式&#xff0c;广度优先…

【OpenHarmony】TypeScript 语法 ④ ( 函数 | TypeScript 具名函数和匿名函数 | 可选参数 | 剩余参数 | 箭头参数 )

文章目录 一、TypeScript 函数1、TypeScript 具名函数和匿名函数2、TypeScript 函数 与 JavaScript 函数对比3、TypeScript 函数 可选参数4、TypeScript 函数 剩余参数5、TypeScript 箭头函数 参考文档 : <HarmonyOS第一课>ArkTS开发语言介绍 一、TypeScript 函数 1、Typ…

PHP MySQL图解学习指南:开启Web开发新篇章

PHP曾经是最流行的Web开发语言&#xff0c;许多世界领先的网站(如Facebook、维基百科和WordPress)都是用它编写的。PHP运行在Web服务器端&#xff0c;通过使用存储在MySQL数据库中的数据&#xff0c;使得网站可以为每一位访问者显示不同的定制页面。书中采用简单、直观的图示化…

bootstrap5-学习笔记2-模态框+弹窗+tooltip+popover+信息提示框

参考&#xff1a; Bootstrap5 教程 | 菜鸟教程 https://www.runoob.com/bootstrap5/bootstrap5-tutorial.html Bootstrap 入门 Bootstrap v5 中文文档 v5.3 | Bootstrap 中文网 https://v5.bootcss.com/docs/getting-started/introduction/ 之前用bootstrap2和3比较多&#x…

15.使用Ollydbg分析处理hp减伤害的函数

上一个内容&#xff1a;14.Ollydbg的基本使用 在 9.游戏中真正的无敌 里找了处理hp减伤害函数的方法 Ollydbg对hp减伤害函数打一个断点&#xff0c;然后一步一步分析数据&#xff0c;下图是进入了hp减伤害函数之后被断点主的样子&#xff0c;然后这时的ecx是人物this地址&…

【刷题】初探递归算法 —— 消除恐惧

送给大家一句话&#xff1a; 有两种东西&#xff0c; 我对它们的思考越是深沉和持久&#xff0c; 它们在我心灵中唤起的惊奇和敬畏就会日新月异&#xff0c; 不断增长&#xff0c; 这就是我头上的星空和心中的道德定律。 -- 康德 《实践理性批判》 初探递归算法 1 递归算…

前端逆向之查看接口调用栈

一、来源 再分析前端请求接口数据的时候&#xff0c;其中有一个sid不知道是前端如何获取的&#xff0c;一般情况下只需要全局搜搜sid这个字符串或者请求接口的名称就可以了&#xff0c;基本都能找到sid的来源&#xff0c;但是今天这个不一样&#xff0c;搜什么都搜不到 接口地…

SAP跨服务器传输请求号

环境一、两台服务器并没有维护连接传输线路&#xff08;DEV和QAS&#xff09; 环境二、需要将外部公司服务器上的请求号传输到内部服务器中 方式&#xff1a;先从开发环境或服务器中下载请求号&#xff0c;再将请求号上传到目标服务器或环境中&#xff0c;在目标服务器使用ST…

分享:重庆耶非凡科技有限公司人力资源项目靠不靠谱?

在当今快速变化的商业环境中&#xff0c;人力资源项目作为企业发展的重要支撑&#xff0c;其专业性和可靠性成为企业选择合作伙伴时的重要考量因素。重庆耶非凡科技有限公司作为一家在行业内颇具影响力的科技企业&#xff0c;其人力资源项目——人力RPO(招聘流程外包)项目&…

实现Redis和数据库数据同步问题(JAVA代码实现)

这里我用到了Redis当中的发布订阅模式实现(JAVA代码实现) 先看图示 下面为代码实现 首先将RedisMessageListenerContainer交给Spring管理. Configuration public class redisConfig {AutowiredRedisConnectionFactory redisConnectionFactory;AutowiredQualifier("car…

Hive安装-内嵌模式

1.官网下在hive3.1.2版本 Index of /dist/hive/hive-3.1.2 2.上传到master节点的/opt/software目录下 3.解压到/opt/module目录下 tar -zxvf apache-hive-3.1.2-bin.tar.gz -C /opt/module/ 检查解压后文件 4.修改名字 改为hive cd /opt/module mv apache-hive-3.1.2-bin…

数据结构 实验 1

题目一&#xff1a;用线性表实现文具店的货品管理问题 问题描述&#xff1a;在文具店的日常经营过程中&#xff0c;存在对各种文具的管理问题。当库存文具不足或缺货时&#xff0c;需要进货。日常销售时需要出库。当盘点货物时&#xff0c;需要查询货物信息。请根据这些要求编…

Crosslink-NX器件应用连载(11): 图像(数据)远程传输

作者&#xff1a;Hello&#xff0c;Panda 大家下午好&#xff0c;晚上好。这里分享一个Lattice Crosslink-NX器件实现图像或数据&#xff08;卫星数据、雷达数据、ToF传感器数据等&#xff09;远程传输的案例&#xff08;因为所描述的内容颇杂&#xff0c;晒图不好晒&#xff…

HCIP的学习(27)

RSTP—802.1W—快速生成树协议 STP缺陷&#xff1a; 1、收敛速度慢----STP的算法是一种被动的算法&#xff0c;依赖于计时器来进行状态变化 2、链路利用率低​ RSTP向下兼容STP协议。&#xff08;STP不兼容RSTP&#xff09; 改进点1—端口角色 802.1D协议---根端口、指定端口…

[数据集][目标检测]猕猴桃检测数据集VOC+YOLO格式1838张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1838 标注数量(xml文件个数)&#xff1a;1838 标注数量(txt文件个数)&#xff1a;1838 标注…

【Java】Java遍历Map方法合集

本文摘要&#xff1a;Java遍历Map方法合集 &#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。公粽号&#xff1a;洲与AI。 &#x1f913; 欢迎大家关注&am…