ภาพรวม
สร้าง **Horizontal List** (รายการแนวนอน) ด้วย **RecyclerView + LinearLayoutManager.HORIZONTAL** เป็นมาตรฐานปัจจุบัน แทน HorizontalScrollView (ไม่มี View Recycling) และ ListView (Deprecated)
ขั้นตอนการสร้าง
1. Dependency (build.gradle)
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.11.0'
}
2. Item Layout (res/layout/item_horizontal.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:card_view="https://schemas.android.com/apk/res-auto"
android:layout_width="160dp" <!-- Fixed Width สำคัญ! -->
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="12dp"
card_view:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:gravity="center">
<ImageView
android:id="@+id/ivIcon"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher"
android:contentDescription="@string/item_image" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Item Name"
android:textSize="14sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center" />
<TextView
android:id="@+id/tvDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textSize="12sp"
android:textColor="#757575"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center" />
</LinearLayout>
</androidx.cardview.widget.CardView>
จุดสำคัญ: android:layout_width="160dp" (Fixed Width) ไม่ใช่ match_parent เพื่อให้ Scroll แนวนอนได้
3. Adapter (HorizontalAdapter.java)
package com.example.myapplication; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class HorizontalAdapter extends RecyclerView.Adapter{ private final List - items; private final OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(Item item, int position); } public HorizontalAdapter(List
- items, OnItemClickListener listener) { this.items = items; this.listener = listener; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_horizontal, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.bind(items.get(position)); } @Override public int getItemCount() { return items.size(); } static class ViewHolder extends RecyclerView.ViewHolder { ImageView ivIcon; TextView tvName, tvDesc; ViewHolder(View view) { super(view); ivIcon = view.findViewById(R.id.ivIcon); tvName = view.findViewById(R.id.tvName); tvDesc = view.findViewById(R.id.tvDesc); } void bind(Item item) { tvName.setText(item.name); tvDesc.setText(item.description); ivIcon.setImageResource(item.iconRes); itemView.setOnClickListener(v -> { if (listener != null) listener.onItemClick(item, getAdapterPosition()); }); } } public static class Item { public final int iconRes; public final String name; public final String description; public Item(int iconRes, String name, String description) { this.iconRes = iconRes; this.name = name; this.description = description; } } }
4. Activity Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Horizontal RecyclerView Demo"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:clipToPadding="false"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scrollbars="horizontal"
android:overScrollMode="never" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="← เลื่อนซ้าย-ขวาเพื่อดูรายการ →"
android:textColor="#9E9E9E"
android:textSize="14sp" />
</LinearLayout>
5. MainActivity.java
package com.example.myapplication;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
// Horizontal LayoutManager
LinearLayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, false
);
recyclerView.setLayoutManager(layoutManager);
// Data
List items = Arrays.asList(
new HorizontalAdapter.Item(R.drawable.img1, "Android", "Mobile OS"),
new HorizontalAdapter.Item(R.drawable.img2, "Kotlin", "Modern Language"),
new HorizontalAdapter.Item(R.drawable.img3, "Jetpack Compose", "Modern UI Toolkit"),
new HorizontalAdapter.Item(R.drawable.img4, "Firebase", "Backend Platform"),
new HorizontalAdapter.Item(R.drawable.img5, "Material Design", "Design System"),
new HorizontalAdapter.Item(R.drawable.img6, "Coroutines", "Async Programming"),
new HorizontalAdapter.Item(R.drawable.img7, "Flow", "Reactive Streams"),
new HorizontalAdapter.Item(R.drawable.img8, "Room", "Local Database")
);
HorizontalAdapter adapter = new HorizontalAdapter(items, (item, pos) ->
Toast.makeText(this, "Clicked: " + item.name, Toast.LENGTH_SHORT).show()
);
recyclerView.setAdapter(adapter);
// Optional: Snap to Center (Pager-like)
// new PagerSnapHelper().attachToRecyclerView(recyclerView);
}
}
เพิ่ม Snap Effect (PagerSnapHelper / LinearSnapHelper)
// เพิ่มใน onCreate หลัง setAdapter // 1. PagerSnapHelper: Snap ทีละหน้า (Page) - เหมาะ Item กว้างเต็มจอ new PagerSnapHelper().attachToRecyclerView(recyclerView); // 2. LinearSnapHelper: Snap ไป Item ใกล้สุด - เหมาะ Item แคบ (Carousel) new LinearSnapHelper().attachToRecyclerView(recyclerView); // 3. Custom SnapHelper: Snap ไป Center (ต้อง Custom เอง หรือใช้ 라이브รารี)
เพิ่ม Divider/Spacing ระหว่าง Item
// ItemDecoration สำหรับ Horizontal Spacing
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
int position = parent.getChildAdapterPosition(view);
int count = parent.getAdapter().getItemCount();
outRect.left = position == 0 ? dpToPx(16) : dpToPx(8); // First item padding
outRect.right = position == count - 1 ? dpToPx(16) : dpToPx(8); // Last item padding
}
});
private int dpToPx(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density);
}
Horizontal RecyclerView ใน Vertical RecyclerView (Nested)
ใช้ใน Home Screen: Section แนวตั้ง แต่ละ Section เป็น Horizontal List
// Parent Adapter (Vertical)
@Override
public void onBindViewHolder(@NonNull SectionViewHolder holder, int position) {
Section section = sections.get(position);
holder.tvTitle.setText(section.title);
// Child Horizontal RecyclerView
holder.recyclerView.setLayoutManager(
new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
);
holder.recyclerView.setAdapter(new HorizontalAdapter(section.items, listener));
holder.recyclerView.setHasFixedSize(true); // Optimization
holder.recyclerView.setNestedScrollingEnabled(false); // Important!
}
Best Practices
- Fixed Width Item: Item Width ต้องคงที่ (dp) ไม่ใช่ wrap_content/match_parent
- clipToPadding="false" + padding: ให้ Item แรก/สุดท้ายอยู่กึ่งกลางจอ
- setHasFixedSize(true): หาก Item Size ไม่เปลี่ยน (Performance)
- NestedScrollingEnabled(false): สำหรับ Horizontal RV ใน Vertical RV
- SnapHelper: เพิ่ม UX ให้ Snap ไป Item ใกล้สุด
- ItemDecoration: จัด Spacing ระหว่าง Item แทน Margin ใน Layout
สรุป
Horizontal List = RecyclerView + LinearLayoutManager.HORIZONTAL + Fixed Width Items + SnapHelper (Optional) เป็นมาตรฐานปัจจุบัน แทน HorizontalScrollView ให้ Performance ดี รองรับ Animation, DiffUtil, Paging, Nested Scrolling ครบครัน