Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách sử dụng RecyclerView bên trong NestedScrollView trong Android

Giới thiệu

Bài viết này hướng dẫn cách đặt RecyclerView bên trong NestedScrollView để cuộn cùng lúc với các view khác. Vấn đề chính là conflict cuộn (scroll conflict) và đo lường chiều cao (measurement).

Các bước thực hiện

Bước 1: Thêm dependencies (build.gradle - Module: app)

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.10.0'
    implementation 'androidx.recyclerview:recyclerview:1.3.1'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'com.intuit.sdp:sdp-android:1.0.6'
}

Bước 2: Layout chính (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none"
        android:fillViewport="true"> <!-- Quan trọng -->

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:focusableInTouchMode="true"> <!-- Giúp focus đúng -->

            <ImageView
                android:id="@+id/ivHeader"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:adjustViewBounds="true"
                android:scaleType="centerCrop"
                android:src="@drawable/iphone"
                android:contentDescription="@string/app_name" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rvProductList"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scrollbars="vertical"
                android:nestedScrollingEnabled="false" /> <!-- QUAN TRỌNG -->

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

</LinearLayout>

Giải quyết conflict cuộn - 2 thuộc tính quan trọng:

  • android:nestedScrollingEnabled="false" trên RecyclerView: Tắt nested scroll của RV, để NestedScrollView xử lý toàn bộ cuộn
  • android:fillViewport="true" trên NestedScrollView: Mở rộng content đầy đủ viewport

Bước 3: Layout item (res/layout/list_item.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardElevation="2dp"
    app:cardUseCompatPadding="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="8dp">

        <ImageView
            android:id="@+id/ivPhone"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:adjustViewBounds="true"
            android:contentDescription="@string/app_name"
            android:src="@drawable/iphone2" />

        <TextView
            android:id="@+id/tvPhoneName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="IPHONE"
            android:textColor="@color/colorPrimaryDark"
            android:textSize="14sp"
            android:textStyle="bold" />

    </LinearLayout>

</androidx.cardview.widget.CardView>

Bước 4: Các class Adapter/Model

ProductObject.java (Model)

public class ProductObject {
    private final String name;
    private final String imageResName; // Tên resource drawable (không có extension)

    public ProductObject(String name, String imageResName) {
        this.name = name;
        this.imageResName = imageResName;
    }

    public String getName() { return name; }
    public String getImageResName() { return imageResName; }
}

PhoneViewHolder.java

import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;

class PhoneViewHolder extends RecyclerView.ViewHolder {
    ImageView ivPhone;
    TextView tvName;

    PhoneViewHolder(View itemView) {
        super(itemView);
        ivPhone = itemView.findViewById(R.id.ivPhone);
        tvName = itemView.findViewById(R.id.tvPhoneName);
    }
}

PhoneAdapter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;

public class PhoneAdapter extends RecyclerView.Adapter<PhoneViewHolder> {

    private final Context context;
    private final List<ProductObject> productList;

    public PhoneAdapter(Context context, List<ProductObject> productList) {
        this.context = context;
        this.productList = productList;
    }

    @NonNull
    @Override
    public PhoneViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.list_item, parent, false);
        return new PhoneViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull PhoneViewHolder holder, int position) {
        ProductObject product = productList.get(position);
        holder.tvName.setText(product.getName());

        // Load drawable by name từ resources
        int resId = context.getResources().getIdentifier(
            product.getImageResName(), "drawable", context.getPackageName()
        );
        if (resId != 0) {
            holder.ivPhone.setImageResource(resId);
        }
    }

    @Override
    public int getItemCount() {
        return productList.size();
    }
}

Bước 5: MainActivity (src/MainActivity.java)

package app.com.sample;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

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

        RecyclerView rv = findViewById(R.id.rvProductList);

        // GridLayoutManager 2 cột
        GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
        rv.setLayoutManager(gridLayoutManager);

        // Tối ưu performance
        rv.setHasFixedSize(true); // Kích thước item không thay đổi
        rv.setItemViewCacheSize(20); // Cache view holder
        rv.setDrawingCacheEnabled(true);
        rv.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

        PhoneAdapter adapter = new PhoneAdapter(this, getProductData());
        rv.setAdapter(adapter);
    }

    private List<ProductObject> getProductData() {
        List<ProductObject> list = new ArrayList<>();
        list.add(new ProductObject("iPhone 6", "iphone2"));
        list.add(new ProductObject("iPhone 6S", "iphone2"));
        list.add(new ProductObject("iPhone 8", "iphone2"));
        list.add(new ProductObject("iPhone X", "iphone2"));
        list.add(new ProductObject("iPhone XR", "iphone2"));
        list.add(new ProductObject("iPhone XS", "iphone2"));
        list.add(new ProductObject("iPhone 11", "iphone2"));
        list.add(new ProductObject("iPhone 12", "iphone2"));
        return list;
    }
}

Bước 6: Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="app.com.sample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Chạy ứng dụng

Chạy app, màn hình sẽ cuộn mượt mà từ header image xuống danh sách sản phẩm grid 2 cột.

Cách sử dụng RecyclerView bên trong NestedScrollView trong Android

Lưu ý hiệu năng

  • RecyclerView trong NestedScrollView không recycle view khi nestedScrollingEnabled=false (tất cả item được tạo ngay)
  • Chỉ dùng cho danh sách ít item (≤ 20-30)
  • Danh sách lớn: Dùng ConcatAdapter hoặc RecyclerView với Header/Footer view type
  • Hoặc dùng CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout cho pattern collapsing header

Giải pháp thay thế cho danh sách lớn

// Trong Adapter, override getItemViewType để thêm header
@Override
public int getItemViewType(int position) {
    return position == 0 ? TYPE_HEADER : TYPE_ITEM;
}

// Tạo 2 ViewHolder: HeaderViewHolder, ItemViewHolder
// onCreateViewHolderinflate layout khác nhau theo type