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ộnandroid: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.

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 + CollapsingToolbarLayoutcho 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