openplanning

Hướng dẫn và ví dụ Android ViewPager2

  1. Android ViewPager2 vs ViewPager
  2. Example: ViewPager2
  3. Example: Transformation

1. Android ViewPager2 vs ViewPager

Android ViewPager là một thành phần giao diện được giới thiệu trong thư viện hỗ trợ của Android, nó cho phép người dùng vuốt (swipe) sang trái hoặc trang phải để xem một trang (màn hình) hoàn toàn mới.
ViewPager/ViewPager2
Một ViewPager/ViewPager2 mà mỗi trang của nó lấp đầy màn hình, người dùng sẽ vuốt (swipe) sang trái để di chuyển tới trang tiếp theo, hoặc vuốt sang phải để lùi lại một trang.
Một ViewPager/ViewPager2 nhìn thấy trên ứng dụng học ngôn ngữ của Duolingo.
ViewPager2 vs ViewPager
Như chúng ta đã biết, đội ngũ Android liên tục phát hành các bản cập nhật và cải tiến mới cho Android Framework. Một trong những thành phần nhận được các bản cập nhật lớn là ViewPager2. ViewPager2 là sự thay thế của ViewPager và có một số cải tiến về hiệu suất và các chức năng bổ sung.
Vậy có gì mới trong ViewPager2?
  • ViewPager2 là một phiên bản ngẫu hứng của ViewPager cung cấp các chức năng bổ sung và giải quyết các vấn đề thường gặp trong ViewPager.
  • ViewPager2 được xây dựng trên RecyclerView. Vì vậy, bạn sẽ có những lợi thế tuyệt vời mà RecyclerView có. Ví dụ: bạn có thể tận dụng DiffUtils để tính toán hiệu quả sự khác biệt giữa các tập dữ liệu và cập nhật ViewPager bằng hình ảnh động.
  • ViewPager2 hỗ trợ hướng thẳng đứng (Vertical Orientation). Nếu sử dụng ViewPager, bạn phải viết các tùy biến bổ xung cho ViewPager để có được kết quả tương tự.
  • ViewPager2 hỗ trợ Right-to-Left (RTL) và điều này sẽ được bật tự động dựa trên App Locale.
  • Khi làm việc với một tập hợp các Fragment. Nếu một trong các Fragment thay đổi giao diện của nó, bạn chỉ cần gọi phương thức notifyDatasetChanged() để cập nhập giao diện của ứng dụng một cách đúng cách.
  • ViewPager2 hỗ trợ sự chuyển đổi trang (transformation), tức là bạn có thể cung cấp hình ảnh động khi chuyển đổi giữa các trang. Bạn cũng có thể viết PageTransformer tùy chỉnh của riêng mình.
  • PagerAdapter được thay thế bằng RecyclerView.
  • FragmentStatePagerAdapter được thay thế bởi FragmentStateAdapter.
Library
ViewPager2 là một thành phần không sẵn có trong thư viện tiêu chuẩn của Android, vì vậy nếu bạn muốn sử dụng nó bạn phải cài đặt nó vào project của bạn.
Bạn có thể cài đặt ViewPager2 từ Palette của cửa sổ thiết kế.
Sau khi cài đặt xong bạn sẽ thấy thư viện ViewPager2 được khai báo trong build.gradle (Module: app).
implementation 'androidx.viewpager2:viewpager2:1.0.0'

2. Example: ViewPager2

Bây giờ chúng ta sẽ thực hành một ví dụ về ViewPager2 và sử dụng Fragment như một bản thiết kế của mỗi trang. Dưới đây là hình ảnh xem trước của ví dụ:
  • Tập tin fragment_employee_page.xml là bản thiết kế giao diện của một trang (Page).
  • Lớp EmployeePageFragment chứa dữ liệu của một đối tượng Employee và hiển thị nó lên một trang (Page).
Trên Android Studio tạo mới một project:
  • File > New > New Project > Empty Activity
    • Name: ViewPager2Example
    • Package name: org.o7planning.viewpager2example
    • Language: Java
Như đã đề cập ở trên ViewPager2 là một thành phần không có sẵn trong thư viện tiêu chuẩn của Android, vì vậy, bước tiếp theo bạn cần cài đặt nó vào project của bạn từ Palette của cửa sổ thiết kế.
Và tạo một Fragment (EmployeePageFragment), nó tương ứng với một trang (Page) của ViewPager2:
  • File > New > Fragment > Fragment (Blank)
  • Fragment Name: EmployeePageFragment
  • Fragment Layout Name: fragment_page_employee
  • Source Language: Java
Lúc này bạn sẽ nhìn thấy hai tập tin được tạo ra:
Mở tập tin fragment_employee_page.xml để thiết kế giao diện.
fragment_employee_page.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView_fullName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:background="#C4CFB9"
        android:gravity="center_horizontal"
        android:text="Fullname"
        android:textSize="22sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView71"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="Position:"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView_fullName" />

    <TextView
        android:id="@+id/textView_position"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="(Position)"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView71"
        app:layout_constraintTop_toBottomOf="@+id/textView_fullName" />

    <TextView
        android:id="@+id/textView72"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="Email:"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView71" />

    <TextView
        android:id="@+id/textView_email"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="(Email)"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView72"
        app:layout_constraintTop_toBottomOf="@+id/textView71" />

</androidx.constraintlayout.widget.ConstraintLayout>
EmployeePageFragment.java
package org.o7planning.viewpager2example;

import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class EmployeePageFragment  extends Fragment {

    private static final String LOG_TAG = "AndroidExample";

    private Employee employee;

    private TextView textViewEmail;
    private TextView textViewPosition;
    private TextView textViewFullName;

    private static int counter = 0;

    // IMPORTANT:
    // Required default public constructor.
    // If configuration change.
    // For example: User rotate the Phone,
    //  Android will create new Fragment (EmployeePageFragment) via default Constructor
    //  so this.employee will be null.
    public EmployeePageFragment() {

    }

    public EmployeePageFragment(Employee employee) {
        this.employee = employee;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = (ViewGroup) inflater.inflate(
                R.layout.fragment_employee_page, container, false);

        counter++;
        if(counter % 2 == 0) {
            view.setBackgroundColor(Color.parseColor("#ebdef0"));
        } else  {
            view.setBackgroundColor(Color.parseColor("#e8f8f5"));
        }

        this.textViewFullName = view.findViewById(R.id.textView_fullName);
        this.textViewPosition = view.findViewById(R.id.textView_position);
        this.textViewEmail = view.findViewById(R.id.textView_email);

        return view;
    }


    // Called when configuration change.
    // For example: User rotate the Phone,
    //  Android will create new Fragment (EmployeePageFragment) object via default Constructor
    //  so this.employee will be null.
    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        Log.i(LOG_TAG, "onSaveInstanceState: save employee data to Bundle");
        // Convert employee object to Bundle.
        Bundle dataBundle = this.employeeToBundle(this.employee);
        outState.putAll(dataBundle);

        super.onSaveInstanceState(outState);
    }


    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        Log.i(LOG_TAG, "onViewStateRestored");

        super.onViewStateRestored(savedInstanceState);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Log.i(LOG_TAG, "onViewCreated");
        super.onViewCreated(view, savedInstanceState);

        if(this.employee == null)  {
            Log.i(LOG_TAG, "Get employee data from savedInstanceState");
            // The state was saved by onSaveInstanceState(Bundle outState) method.
            this.employee = this.bundleToEmployee(savedInstanceState);
        }
        this.showInGUI(this.employee);
    }

    // Call where View ready.
    private void showInGUI(Employee employee)  {
        this.textViewFullName.setText(employee.getFullName());
        this.textViewPosition.setText(employee.getPosition());
        this.textViewEmail.setText(employee.getEmail());
    }

    private Bundle employeeToBundle(Employee employee)  {
        Bundle bundle = new Bundle();
        bundle.putString("fullName", employee.getFullName());
        bundle.putString("position", employee.getPosition());
        bundle.putString("email", employee.getEmail());

        return bundle;
    }

    private Employee bundleToEmployee(Bundle savedInstanceState) {
        String fullName = savedInstanceState.getString("fullName");
        String position = savedInstanceState.getString("position");
        String email = savedInstanceState.getString("email");
        return new Employee(fullName, email, position);
    }

}
EmployeeFragmentStateAdapter.java
package org.o7planning.viewpager2example;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import java.util.ArrayList;
import java.util.List;

public class EmployeeFragmentStateAdapter extends FragmentStateAdapter {

    private List<Employee> employees;


    public EmployeeFragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);

        this.employees = this.intDatas();
    }

    private List<Employee> intDatas()  {
        Employee emp1 = new Employee("James Smith", "jamessmith@example.com", "Web Designer");
        Employee emp2 = new Employee("Elizabeth Johnson", "elizabethjohnson@example.com", "Project Manager");
        Employee emp3 = new Employee("Catherine Johnson", "catherinejohnson@example.com", "President of Sales");

        List<Employee> list = new ArrayList<Employee>();
        list.add(emp1);
        list.add(emp2);
        list.add(emp3);
        return list;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        Employee employee = this.employees.get(position);
        return new EmployeePageFragment(employee);
    }


    @Override
    public int getItemCount() {
        return this.employees.size();
    }
}
Employee.java
package org.o7planning.viewpager2example;

public class Employee {

    private String fullName;
    private String email;
    private String position;

    public Employee(String fullName, String email, String position) {
        this.fullName = fullName;
        this.email = email;
        this.position = position;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

}
Giao diện chính của ứng dụng:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager2_employee"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
package org.o7planning.viewpager2example;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    private ViewPager2 viewPager2Employee;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.viewPager2Employee = findViewById(R.id.viewPager2_employee);

        // Employee FragmentStateAdapter.
        EmployeeFragmentStateAdapter adapter = new EmployeeFragmentStateAdapter(this);
        this.viewPager2Employee.setAdapter(adapter);
    }

}

3. Example: Transformation

Để hiển thị hiệu ứng hoạt hình khi người dùng chuyển đổi giữa các trang, bạn cần viết một lớp thực hiện (implements) interface ViewPager2.PageTransformer và cung cấp nó cho đối tượng ViewPager2.
ViewPager2 viewPager = findViewById(R.id.pager);
...
viewPager.setPageTransformer(new YourPageTransformer());
Interface ViewPager2.PageTransformer chỉ có duy nhất một phương thức transformPage(), phương thức này được gọi một lần cho mỗi quá trình chuyển đổi (transition).
/**
 * Apply a property transformation to the given page.
 *
 * @param page Apply the transformation to this page
 * @param position Position of page relative to the current front-and-center
 *                 position of the pager. 0 is front and center. 1 is one full
 *                 page position to the right, and -2 is two pages to the left.
 *                 Minimum / maximum observed values depend on how many pages we keep
 *                 attached, which depends on offscreenPageLimit.
 *
 * @see #setOffscreenPageLimit(int)
 */
void transformPage(@NonNull View page, float position);
Tiếp tục với ví dụ ở trên, chúng ta sẽ thêm hiệu ứng hoạt hình vào cho nó.
Zoom Out Page Transformer
ZoomOutPageTransformer.java
package org.o7planning.viewpager2example.transformer;

import android.view.View;

import androidx.viewpager2.widget.ViewPager2;

public 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) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 1) { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            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);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA +
                    (scaleFactor - MIN_SCALE) /
                            (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}
MainActivity.java (Zoom Out Page Transformer)
package org.o7planning.viewpager2example;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;

import android.os.Bundle;
import org.o7planning.viewpager2example.transformer.ZoomOutPageTransformer;

public class MainActivity extends AppCompatActivity {

    private ViewPager2 viewPager2Employee;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.viewPager2Employee = findViewById(R.id.viewPager2_employee);

        // Employee FragmentStateAdapter.
        EmployeeFragmentStateAdapter adapter = new EmployeeFragmentStateAdapter(this);
        this.viewPager2Employee.setAdapter(adapter);

        // PageTransformer
        this.viewPager2Employee.setPageTransformer(new ZoomOutPageTransformer());

    }
}
Depth Page Transformer
DepthPageTransformer.java
package org.o7planning.viewpager2example.transformer;

import android.view.View;

import androidx.annotation.RequiresApi;
import androidx.viewpager2.widget.ViewPager2;

@RequiresApi(21)
public class DepthPageTransformer implements ViewPager2.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1f);
            view.setTranslationX(0f);
            view.setTranslationZ(0f);
            view.setScaleX(1f);
            view.setScaleY(1f);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);
            // Move it behind the left page
            view.setTranslationZ(-1f);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}
MainActivity.java (Depth Page Transformer)
package org.o7planning.viewpager2example;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;

import android.os.Build;
import android.os.Bundle;

import org.o7planning.viewpager2example.transformer.DepthPageTransformer;

public class MainActivity extends AppCompatActivity {

    private ViewPager2 viewPager2Employee;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.viewPager2Employee = findViewById(R.id.viewPager2_employee);

        // Employee FragmentStateAdapter.
        EmployeeFragmentStateAdapter adapter = new EmployeeFragmentStateAdapter(this);
        this.viewPager2Employee.setAdapter(adapter);

        // PageTransformer  
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Api 21+.
           this.viewPager2Employee.setPageTransformer(new DepthPageTransformer());
        }
    }

}

Các hướng dẫn lập trình Android

Show More