Skip to main content
Build a custom participant list UI that displays real-time participant information with full control over layout and interactions. This guide demonstrates how to hide the default participant list and create your own using participant events and actions.

Overview

The SDK provides participant data through events, allowing you to build custom UIs for:
  • Participant roster with search and filtering
  • Custom participant cards with role badges or metadata
  • Moderation dashboards with quick access to controls
  • Attendance tracking and engagement monitoring

Prerequisites

  • CometChat Calls SDK installed and initialized
  • Active call session (see Join Session)
  • Basic understanding of RecyclerView and adapters

Step 1: Hide Default Participant List

Configure session settings to hide the default participant list button:
val sessionSettings = CometChatCalls.SessionSettingsBuilder()
    .hideParticipantListButton(true)
    .build()

Step 2: Create Participant List Layout

Create a layout with RecyclerView for displaying participants:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Call View Container -->
    <FrameLayout
        android:id="@+id/callContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Custom Participant List Panel -->
    <LinearLayout
        android:id="@+id/participantPanel"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_alignParentEnd="true"
        android:background="#F5F5F5"
        android:orientation="vertical"
        android:padding="16dp"
        android:visibility="gone">

        <!-- Header -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingBottom="16dp">

            <TextView
                android:id="@+id/participantCount"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Participants (0)"
                android:textColor="#000000"
                android:textSize="18sp"
                android:textStyle="bold" />

            <ImageButton
                android:id="@+id/closeButton"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:contentDescription="Close"
                android:src="@android:drawable/ic_menu_close_clear_cancel" />
        </LinearLayout>

        <!-- Search Bar -->
        <EditText
            android:id="@+id/searchInput"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Search participants..."
            android:padding="12dp"
            android:background="@android:color/white"
            android:layout_marginBottom="16dp" />

        <!-- Participant List -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/participantRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    </LinearLayout>

    <!-- Toggle Button -->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/toggleParticipantListButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_margin="16dp"
        android:contentDescription="Participants"
        android:src="@android:drawable/ic_menu_agenda" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="12dp"
    android:background="?attr/selectableItemBackground">

    <!-- Avatar -->
    <ImageView
        android:id="@+id/participantAvatar"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginEnd="12dp"
        android:contentDescription="Avatar" />

    <!-- Info -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/participantName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000000"
            android:textSize="16sp"
            android:textStyle="bold" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/statusIndicator"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:textColor="#666666" />
        </LinearLayout>
    </LinearLayout>

    <!-- Action Buttons -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageButton
            android:id="@+id/muteButton"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Mute"
            android:src="@android:drawable/ic_lock_silent_mode" />

        <ImageButton
            android:id="@+id/videoPauseButton"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Pause Video"
            android:src="@android:drawable/ic_menu_camera" />

        <ImageButton
            android:id="@+id/pinButton"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:contentDescription="Pin"
            android:src="@android:drawable/btn_star" />
    </LinearLayout>
</LinearLayout>

Step 3: Create Participant Adapter

Build a RecyclerView adapter to display participant data:
class ParticipantAdapter(
    private val onMuteClick: (Participant) -> Unit,
    private val onPauseVideoClick: (Participant) -> Unit,
    private val onPinClick: (Participant) -> Unit
) : RecyclerView.Adapter<ParticipantAdapter.ViewHolder>() {

    private var participants = listOf<Participant>()
    private var filteredParticipants = listOf<Participant>()

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val avatar: ImageView = view.findViewById(R.id.participantAvatar)
        val name: TextView = view.findViewById(R.id.participantName)
        val status: TextView = view.findViewById(R.id.statusIndicator)
        val muteButton: ImageButton = view.findViewById(R.id.muteButton)
        val videoPauseButton: ImageButton = view.findViewById(R.id.videoPauseButton)
        val pinButton: ImageButton = view.findViewById(R.id.pinButton)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_participant, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val participant = filteredParticipants[position]

        // Set name
        holder.name.text = participant.name

        // Load avatar (use your image loading library)
        // Glide.with(holder.avatar).load(participant.avatar).into(holder.avatar)

        // Build status text
        val statusParts = mutableListOf<String>()
        if (participant.isAudioMuted) statusParts.add("🔇 Muted")
        if (participant.isVideoPaused) statusParts.add("📹 Video Off")
        if (participant.isPresenting) statusParts.add("🖥️ Presenting")
        if (participant.raisedHandTimestamp > 0) statusParts.add("✋ Hand Raised")
        if (participant.isPinned) statusParts.add("📌 Pinned")
        
        holder.status.text = if (statusParts.isEmpty()) "Active" else statusParts.joinToString(" • ")

        // Action buttons
        holder.muteButton.setOnClickListener { onMuteClick(participant) }
        holder.videoPauseButton.setOnClickListener { onPauseVideoClick(participant) }
        holder.pinButton.setOnClickListener { onPinClick(participant) }

        // Update button states
        holder.muteButton.alpha = if (participant.isAudioMuted) 0.5f else 1.0f
        holder.videoPauseButton.alpha = if (participant.isVideoPaused) 0.5f else 1.0f
        holder.pinButton.alpha = if (participant.isPinned) 1.0f else 0.5f
    }

    override fun getItemCount() = filteredParticipants.size

    fun updateParticipants(newParticipants: List<Participant>) {
        participants = newParticipants
        filteredParticipants = newParticipants
        notifyDataSetChanged()
    }

    fun filter(query: String) {
        filteredParticipants = if (query.isEmpty()) {
            participants
        } else {
            participants.filter { 
                it.name.contains(query, ignoreCase = true) 
            }
        }
        notifyDataSetChanged()
    }
}

Step 4: Implement Participant Events

Listen for participant updates and handle actions in your Activity:
class CallActivity : AppCompatActivity() {

    private lateinit var participantAdapter: ParticipantAdapter
    private lateinit var callSession: CallSession
    private var isParticipantPanelVisible = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_call)

        callSession = CallSession.getInstance()

        // Setup RecyclerView
        val recyclerView = findViewById<RecyclerView>(R.id.participantRecyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        
        participantAdapter = ParticipantAdapter(
            onMuteClick = { participant ->
                callSession.muteParticipant(participant.uid)
            },
            onPauseVideoClick = { participant ->
                callSession.pauseParticipantVideo(participant.uid)
            },
            onPinClick = { participant ->
                if (participant.isPinned) {
                    callSession.unPinParticipant()
                } else {
                    callSession.pinParticipant(participant.uid)
                }
            }
        )
        recyclerView.adapter = participantAdapter

        // Setup search
        val searchInput = findViewById<EditText>(R.id.searchInput)
        searchInput.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                participantAdapter.filter(s.toString())
            }
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        })

        // Setup toggle button
        findViewById<FloatingActionButton>(R.id.toggleParticipantListButton).setOnClickListener {
            toggleParticipantPanel()
        }

        // Setup close button
        findViewById<ImageButton>(R.id.closeButton).setOnClickListener {
            toggleParticipantPanel()
        }

        // Listen for participant events
        setupParticipantListener()
    }

    private fun setupParticipantListener() {
        callSession.addParticipantEventListener(this, object : ParticipantEventListener() {
            override fun onParticipantListChanged(participants: List<Participant>) {
                runOnUiThread {
                    participantAdapter.updateParticipants(participants)
                    updateParticipantCount(participants.size)
                }
            }

            override fun onParticipantJoined(participant: Participant) {
                Log.d(TAG, "${participant.name} joined")
            }

            override fun onParticipantLeft(participant: Participant) {
                Log.d(TAG, "${participant.name} left")
            }

            override fun onParticipantAudioMuted(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }

            override fun onParticipantAudioUnmuted(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }

            override fun onParticipantVideoPaused(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }

            override fun onParticipantVideoResumed(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }

            override fun onParticipantHandRaised(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }

            override fun onParticipantHandLowered(participant: Participant) {
                // Adapter will update automatically via onParticipantListChanged
            }
        })
    }

    private fun toggleParticipantPanel() {
        val panel = findViewById<LinearLayout>(R.id.participantPanel)
        isParticipantPanelVisible = !isParticipantPanelVisible
        panel.visibility = if (isParticipantPanelVisible) View.VISIBLE else View.GONE
    }

    private fun updateParticipantCount(count: Int) {
        findViewById<TextView>(R.id.participantCount).text = "Participants ($count)"
    }

    companion object {
        private const val TAG = "CallActivity"
    }
}

Complete Example

Here’s the full implementation with all components:
class CallActivity : AppCompatActivity() {

    private lateinit var participantAdapter: ParticipantAdapter
    private lateinit var callSession: CallSession
    private var isParticipantPanelVisible = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_call)

        callSession = CallSession.getInstance()
        setupUI()
        setupParticipantListener()
        joinCall()
    }

    private fun setupUI() {
        // Setup RecyclerView
        val recyclerView = findViewById<RecyclerView>(R.id.participantRecyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        
        participantAdapter = ParticipantAdapter(
            onMuteClick = { callSession.muteParticipant(it.uid) },
            onPauseVideoClick = { callSession.pauseParticipantVideo(it.uid) },
            onPinClick = { 
                if (it.isPinned) callSession.unPinParticipant() 
                else callSession.pinParticipant(it.uid)
            }
        )
        recyclerView.adapter = participantAdapter

        // Setup search
        findViewById<EditText>(R.id.searchInput).addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                participantAdapter.filter(s.toString())
            }
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        })

        // Setup buttons
        findViewById<FloatingActionButton>(R.id.toggleParticipantListButton)
            .setOnClickListener { toggleParticipantPanel() }
        findViewById<ImageButton>(R.id.closeButton)
            .setOnClickListener { toggleParticipantPanel() }
    }

    private fun setupParticipantListener() {
        callSession.addParticipantEventListener(this, object : ParticipantEventListener() {
            override fun onParticipantListChanged(participants: List<Participant>) {
                runOnUiThread {
                    participantAdapter.updateParticipants(participants)
                    findViewById<TextView>(R.id.participantCount).text = 
                        "Participants (${participants.size})"
                }
            }

            override fun onParticipantJoined(participant: Participant) {
                Toast.makeText(this@CallActivity, 
                    "${participant.name} joined", Toast.LENGTH_SHORT).show()
            }

            override fun onParticipantLeft(participant: Participant) {
                Toast.makeText(this@CallActivity, 
                    "${participant.name} left", Toast.LENGTH_SHORT).show()
            }
        })
    }

    private fun joinCall() {
        val sessionSettings = CometChatCalls.SessionSettingsBuilder()
            .hideParticipantListButton(true)
            .setTitle("Team Meeting")
            .build()

        val callContainer = findViewById<FrameLayout>(R.id.callContainer)

        CometChatCalls.joinSession(
            sessionId = "SESSION_ID",
            sessionSettings = sessionSettings,
            view = callContainer,
            context = this,
            listener = object : CometChatCalls.CallbackListener<Void>() {
                override fun onSuccess(p0: Void?) {
                    Log.d(TAG, "Joined call successfully")
                }

                override fun onError(exception: CometChatException) {
                    Log.e(TAG, "Failed to join: ${exception.message}")
                    Toast.makeText(this@CallActivity, 
                        "Failed to join call", Toast.LENGTH_SHORT).show()
                }
            }
        )
    }

    private fun toggleParticipantPanel() {
        val panel = findViewById<LinearLayout>(R.id.participantPanel)
        isParticipantPanelVisible = !isParticipantPanelVisible
        panel.visibility = if (isParticipantPanelVisible) View.VISIBLE else View.GONE
    }

    companion object {
        private const val TAG = "CallActivity"
    }
}
PropertyTypeDescription
uidStringUnique identifier (CometChat user ID)
nameStringDisplay name
avatarStringURL of avatar image
pidStringParticipant ID for this call session
roleStringRole in the call
audioMutedBooleanWhether audio is muted
videoPausedBooleanWhether video is paused
isPinnedBooleanWhether pinned in layout
isPresentingBooleanWhether screen sharing
raisedHandTimestampLongTimestamp when hand was raised (0 if not raised)