1094 words
5 minutes
Android Java - 簡易今日頭條 - Tabs 頻道頁面切換

前言#

上一篇文章我寫了關於底部導覽頁的切換,這次換到關於簡易今日頭條中的 Tab Channel 切換

項目倉庫#

簡易今日頭條 - Github

Display#

Display_tabbed.gif

具體實現#

這裡我一開始使用的是舊的 API 方法(ViewPagerFragmentPagerAdapter),如果你使用的是:

  • androidx.viewpager.widget.ViewPager
  • androidx.fragment.app.FragmentPagerAdapter

你會得到一個提示是這 兩個 API 是已經棄用(deprecated) 的,尤其是在寫 FragmentPagerAdapter 類的時候,Android Studio 會在父類類名上刪除線(Strikethrough),所以我們需要切換到新的 API:

  • androidx.viewpager2.widget.ViewPager2
  • androidx.viewpager2.adapter.FragmentStateAdapter

那我們來開始看具體寫法,可以參考官方文檔Create swipe views with tabs using ViewPager2 - Android developers

註:由於這個的 tabbed 項目需求的頁面是相同的,都是展示新聞卡片,所以我們就只需要設計一個 Fragment 類,但是如果我們需要每個新聞頻道 Tab 頁面有不同的設計,就需要設計多個 Fragment 類。

layout/fragment_home.xml#

我需要在 Home Fragment 展示我的 Tabs,然後在 NewsChannelFragment 顯示各頁面內容,這裡我們有 TabLayout 和最新的 ViewPager2

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.PopupOverlay"
android:background="@color/tabbed_bg">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginEnd="10dp"
app:layout_scrollFlags="scroll|enterAlways">
<com.example.toutiao.ui.searchBar.SearchView
android:id="@+id/search_view_news"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:layout_marginEnd="30dp" />
<Button
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:background="@drawable/ic_baseline_add_circle_24"
android:backgroundTint="@color/white"
android:clickable="true"
android:focusable="true"
tools:ignore="RtlHardcoded" />
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="#FFFFFF"
app:tabMode="scrollable"
app:tabRippleColor="#00FFFFFF"
app:tabSelectedTextColor="@color/white"
app:tabTextColor="#D1D1D1">
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_channel"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.home.HomeFragment"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

layout/fragment_news_channel.xml#

這裡就沒有比較需要注意的東西。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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/fragment_news_channel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.page.newsChannel.NewsChannelFragment">
<!-- ... -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>

ui/page/newsChannel/newsChannelFragment.java#

我在 ui/page/newsChannel 目錄下加了三個文件,分別是 newsChannelFragment.javaPageViewModel.javaSectionsPagerAdapter.java。代表我們需要的 Fragment 頻道展示頁面、Fragment 的 ViewModel、ViewPager 的適配器。

這裡的注意點是在 public static NewsChannelFragment newInstance(String category, int index) 方法中返回一個自己的 Fragment 實例對象,還有 onCreate 中的傳參數獲取。

package com.example.toutiao.ui.page.newsChannel;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
/**
* A simple {@link Fragment} subclass.
* Use the {@link NewsChannelFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class NewsChannelFragment extends Fragment {
private String mCategory;
private int mIndex;
public NewsChannelFragment() {
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*/
public static NewsChannelFragment newInstance(String category, int index) {
NewsChannelFragment fragment = new NewsChannelFragment();
Bundle bundle = new Bundle();
bundle.putString("category", category);
bundle.putInt("index", index);
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPageViewModel = new ViewModelProvider(this).get(PageViewModel.class);
if (getArguments() != null) {
mCategory = getArguments().getString("category");
mIndex = getArguments().getInt("index");
}
mPageViewModel.setCategory(mCategory);
mPageViewModel.setIndex(mIndex);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_news_channel, container, false);
// ...
return view;
}
}

ui/page/newsChannel/PageViewModel.java#

package com.example.toutiao.ui.page.newsChannel;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
/**
* A [ViewModel] for NewsChannelFragment
*/
public class PageViewModel extends ViewModel {
private final MutableLiveData<Integer> mIndex = new MutableLiveData<>();
private final MutableLiveData<String> mCategory = new MutableLiveData<>();
public MutableLiveData<String> getCategory() {
return mCategory;
}
public void setCategory(String category) {
mCategory.setValue(category);
}
public MutableLiveData<Integer> getIndex() {
return mIndex;
}
public void setIndex(int index) {
mIndex.setValue(index);
}
}

ui/page/newsChannel/SectionsPagerAdapter.java#

這裡使用的是最新的 API:FragmentStateAdapter。成員變量 mArrayList 是我們的 Fragment 數組,我們渲染多少頻道就有多大。addFragment 方法添加新的 fragment

package com.example.toutiao.ui.page.newsChannel;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
/**
* A [FragmentStateAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentStateAdapter {
private final ArrayList<Fragment> mArrayList = new ArrayList<>();
public SectionsPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
}
public void addFragment(Fragment fragment) {
mArrayList.add(fragment);
}
@NonNull
@NotNull
@Override
public Fragment createFragment(int position) {
// return your fragment that corresponds to this 'position'
return mArrayList.get(position);
}
@Override
public int getItemCount() {
// Show total pages.
return mArrayList.size();
}
}

ui/home/HomeFragment.java#

這是最關鍵的文件,關於設定 TabLayoutViewPager2

package com.example.toutiao.ui.home;
// ...
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;
import com.example.toutiao.R;
import com.example.toutiao.activity.SearchActivity;
import com.example.toutiao.ui.page.newsChannel.NewsChannelFragment;
import com.example.toutiao.ui.page.newsChannel.SectionsPagerAdapter;
import com.example.toutiao.ui.searchBar.SearchView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
/**
* A simple {@link Fragment} subclass.
* Use the {@link com.example.toutiao.ui.home.HomeFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class HomeFragment extends Fragment {
private SectionsPagerAdapter mSectionsPagerAdapter;
@StringRes
private static final int[] TAB_TITLES = new int[]{
R.string.title__all__,
R.string.title_news_tech,
// R.string.title_news_image,
R.string.title_news_hot,
R.string.title_news_entertainment,
R.string.title_news_game,
R.string.title_news_sports,
R.string.title_news_finance,
R.string.title_digital,
};
private ViewPager2 mViewPagerChannel;
private SearchView mNewsSearchView;
private TabLayout mTabsChannel;
// // TODO: Rename parameter arguments, choose names that match
//
// // TODO: Rename and change types of parameters
public HomeFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @return A new instance of fragment AccountFragment.
*/
// TODO: Rename and change types and number of parameters
public static HomeFragment newInstance() {
HomeFragment fragment = new HomeFragment();
Bundle args = new Bundle();
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// setting status bar's color
Window mWindow = getActivity().getWindow();
mWindow.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
mWindow.setStatusBarColor(getResources().getColor(R.color.tabbed_bg));
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_home, container, false);
Context context = container.getContext();
// setting tabbed
mSectionsPagerAdapter = new SectionsPagerAdapter(getChildFragmentManager(), getLifecycle());
mViewPagerChannel = view.findViewById(R.id.view_pager_channel);
// add Fragments in your ViewPagerFragmentAdapter class
for (int i = 0; i < TAB_TITLES.length; i++) {
String category = context.getString(TAB_TITLES[i]);
mSectionsPagerAdapter.addFragment(NewsChannelFragment.newInstance(category, i));
}
// setting switch Orientation
mViewPagerChannel.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
// setting adapter to view pager
mViewPagerChannel.setAdapter(mSectionsPagerAdapter);
mTabsChannel = view.findViewById(R.id.tabs_channel);
// setting tabbed
new TabLayoutMediator(mTabsChannel, mViewPagerChannel,
(tab, position) -> tab.setText(context.getString(TAB_TITLES[position]))
).attach();
return view;
}
}

Reference#

Android Java - 簡易今日頭條 - Tabs 頻道頁面切換
https://huangno1.github.io/posts/android_java_swip_views_with_tabs/
Author
HuangNO1
Published at
2021-06-15
License
CC BY-NC-SA 4.0