Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีสร้าง Horizontal ListView ด้วย RecyclerView ใน Android

ภาพรวม

สร้าง **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 ครบครัน