Hiệu ứng tab cuộn trượt tương tự ứng dụng Toutiao được xây dựng dựa trên sự kết hợp giữa ViewPager, Fragment và TabLayout. Hướng dẫn này cung cấp giải pháp toàn diện để triển khai giao diện động, mượt mà và dễ bảo trì.
Cấu hình môi trường (phiên bản 2023)
// build.gradle (Module)
dependencies {
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.9.0'
}
Nên ưu tiên sử dụng ViewPager2 vì hỗ trợ cuộn dọc và bố cục RTL. Bài viết cũng bao gồm hướng dẫn tương thích với ViewPager truyền thống.
Triển khai ViewPager cơ bản
1. Layout XML
<androidx.viewpager.widget.ViewPager
android:id="@+id/mainPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
2. PagerAdapter tùy chỉnh
public class DynamicPagerAdapter extends PagerAdapter {
private List<View> pageList;
public DynamicPagerAdapter(List<View> pages) {
this.pageList = pages;
}
@Override
public int getCount() {
return pageList != null ? pageList.size() : 0;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view.equals(object);
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int index) {
View page = pageList.get(index);
container.addView(page);
return page;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int index, @NonNull Object obj) {
container.removeView((View) obj);
}
}
Tải Fragment động theo nhu cầu
1. Base Fragment
public class DynamicContentFragment extends Fragment {
private static final String KEY_SECTION = "section_name";
public static DynamicContentFragment create(String sectionTitle) {
DynamicContentFragment fragment = new DynamicContentFragment();
Bundle args = new Bundle();
args.putString(KEY_SECTION, sectionTitle);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent,
@Nullable Bundle state) {
return inflater.inflate(R.layout.fragment_content, parent, false);
}
}
2. Adapter quản lý Fragment
public class SectionPagerAdapter extends FragmentStateAdapter {
private final List<String> sectionTitles;
public SectionPagerAdapter(@NonNull FragmentActivity fa, List<String> titles) {
super(fa);
this.sectionTitles = titles;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return DynamicContentFragment.create(sectionTitles.get(position));
}
@Override
public int getItemCount() {
return sectionTitles.size();
}
}
Kết nối liền mạch với TabLayout
1. Layout tích hợp
<com.google.android.material.tabs.TabLayout
android:id="@+id/mainTabs"
android:layout_width="match_parent"
android:layout_height="48dp"
app:tabMode="scrollable"
app:tabGravity="center" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/mainPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
2. Liên kết hai chiều
ViewPager2 pager = findViewById(R.id.mainPager);
TabLayout tabs = findViewById(R.id.mainTabs);
SectionPagerAdapter adapter = new SectionPagerAdapter(this, categoryList);
pager.setAdapter(adapter);
pager.setOffscreenPageLimit(2);
new TabLayoutMediator(tabs, pager, (tab, pos) -> {
tab.setText(categoryList.get(pos));
tab.setIcon(getTabIcon(pos));
}).attach();
3. Tùy chỉnh giao diện Tab
<style name="CustomTabStyle" parent="Widget.MaterialComponents.TabLayout">
<item name="tabIndicatorHeight">3dp</item>
<item name="tabIndicatorColor">@color/brand_accent</item>
<item name="tabTextAppearance">@style/CustomTabText</item>
</style>
<style name="CustomTabText" parent="TextAppearance.Design.Tab">
<item name="android:textSize">15sp</item>
<item name="android:fontFamily">sans-serif-medium</item>
</style>
Mở rộng tính năng nâng cao
1. Hiệu ứng chuyển trang 3D
pager.setPageTransformer(new ZoomOutPageTransformer());
static class ZoomOutPageTransformer implements ViewPager2.PageTransformer {
private static final float MIN_SCALE = 0.85f;
private static final float MIN_ALPHA = 0.5f;
public void transformPage(View view, float position) {
int pageWidth = view.getWidth();
int pageHeight = view.getHeight();
if (position < -1 || position > 1) {
view.setAlpha(0f);
} else if (position <= 1) {
float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
float vertMargin = pageHeight * (1 - scaleFactor) / 2;
float horzMargin = pageWidth * (1 - scaleFactor) / 2;
if (position < 0) {
view.setTranslationX(horzMargin - vertMargin / 2);
} else {
view.setTranslationX(-horzMargin + vertMargin / 2);
}
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
}
}
}
2. Tải dữ liệu khi tab hiển thị
@Override
public void onResume() {
super.onResume();
if (getUserVisibleHint()) {
lazyLoadData();
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && isResumed()) {
lazyLoadData();
}
}
Xử lý lỗi thường gặp
- Mất trạng thái Fragment: Dùng
FragmentStateAdapterthay vìFragmentPagerAdapter. - Chỉ báo Tab không đồng bộ: Gọi
TabLayoutMediator.attach()sau khi thiết lập adapter cho ViewPager. - Giật lag khi cuộn:
- Bật tăng tốc phần cứng trong manifest
- Tránh lồng RecyclerView/NestedScrollView bên trong ViewPager
- Sử dụng ViewHolder pattern và hạn chế inflate layout nặng