Skip to main content
Monitor layout changes with LayoutListeners. This listener provides callbacks for call layout changes, participant list visibility, and Picture-in-Picture (PiP) mode state changes.

Prerequisites

Register Listener

Set the layoutListener on the CallSession to receive layout event callbacks:
final callSession = CallSession.getInstance();

callSession?.layoutListener = LayoutListeners(
  onCallLayoutChanged: (LayoutType layoutType) {
    debugPrint("Layout changed to: $layoutType");
  },
  onParticipantListVisible: () {
    debugPrint("Participant list is now visible");
  },
  onParticipantListHidden: () {
    debugPrint("Participant list is now hidden");
  },
  onPictureInPictureLayoutEnabled: () {
    debugPrint("PiP mode enabled");
  },
  onPictureInPictureLayoutDisabled: () {
    debugPrint("PiP mode disabled");
  },
);
Flutter listeners are not lifecycle-aware. You must manually remove the layout listener in your widget’s dispose() method to prevent memory leaks.

Callbacks

onCallLayoutChanged

Triggered when the call layout changes between Tile and Spotlight modes.
onCallLayoutChanged: (LayoutType layoutType) {
  debugPrint("Layout changed to: $layoutType");

  switch (layoutType) {
    case LayoutType.tile:
      // Update UI for tile layout
      setState(() => _currentLayout = LayoutType.tile);
      break;
    case LayoutType.spotlight:
      // Update UI for spotlight layout
      setState(() => _currentLayout = LayoutType.spotlight);
      break;
  }
}
LayoutType Values:
ValueDescription
LayoutType.tileGrid layout showing all participants equally
LayoutType.spotlightFocus on active speaker with others in sidebar
Use Cases:
  • Update layout toggle button icon
  • Adjust custom UI overlays
  • Log layout preference analytics

onParticipantListVisible

Triggered when the participant list panel becomes visible.
onParticipantListVisible: () {
  debugPrint("Participant list opened");
  // Track analytics
  // Adjust UI if needed
  setState(() => _isParticipantListVisible = true);
}
Use Cases:
  • Log analytics events
  • Adjust custom UI elements
  • Pause other UI animations

onParticipantListHidden

Triggered when the participant list panel is hidden.
onParticipantListHidden: () {
  debugPrint("Participant list closed");
  // Restore UI
  setState(() => _isParticipantListVisible = false);
}
Use Cases:
  • Restore UI elements
  • Resume animations
  • Update button states

onPictureInPictureLayoutEnabled

Triggered when Picture-in-Picture (PiP) mode is enabled.
onPictureInPictureLayoutEnabled: () {
  debugPrint("PiP mode enabled");
  // Hide non-essential UI elements
  setState(() => _isPipEnabled = true);
}
Use Cases:
  • Hide call control buttons
  • Simplify UI for small window
  • Track PiP feature usage

onPictureInPictureLayoutDisabled

Triggered when Picture-in-Picture (PiP) mode is disabled.
onPictureInPictureLayoutDisabled: () {
  debugPrint("PiP mode disabled");
  // Restore full UI
  setState(() => _isPipEnabled = false);
}
Use Cases:
  • Restore call control buttons
  • Show full call UI
  • Resume normal layout

Complete Example

Here’s a complete example handling all layout events:
import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart';
import 'package:flutter/material.dart';

class CallScreen extends StatefulWidget {
  const CallScreen({super.key});

  @override
  State<CallScreen> createState() => _CallScreenState();
}

class _CallScreenState extends State<CallScreen> {
  CallSession? _callSession;
  LayoutType _currentLayout = LayoutType.tile;
  bool _isParticipantListVisible = false;
  bool _isPipEnabled = false;

  @override
  void initState() {
    super.initState();
    _callSession = CallSession.getInstance();
    _setupLayoutListener();
  }

  void _setupLayoutListener() {
    _callSession?.layoutListener = LayoutListeners(
      onCallLayoutChanged: (LayoutType layoutType) {
        if (mounted) {
          setState(() => _currentLayout = layoutType);
        }
      },
      onParticipantListVisible: () {
        if (mounted) {
          setState(() => _isParticipantListVisible = true);
          debugPrint("Participant list visible");
        }
      },
      onParticipantListHidden: () {
        if (mounted) {
          setState(() => _isParticipantListVisible = false);
          debugPrint("Participant list hidden");
        }
      },
      onPictureInPictureLayoutEnabled: () {
        if (mounted) {
          setState(() => _isPipEnabled = true);
          debugPrint("PiP enabled");
        }
      },
      onPictureInPictureLayoutDisabled: () {
        if (mounted) {
          setState(() => _isPipEnabled = false);
          debugPrint("PiP disabled");
        }
      },
    );
  }

  @override
  void dispose() {
    // Must manually remove listener to prevent memory leaks
    _callSession?.layoutListener = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // Call UI content here
          if (!_isPipEnabled)
            Positioned(
              bottom: 16,
              left: 0,
              right: 0,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // Layout toggle button
                  IconButton(
                    icon: Icon(
                      _currentLayout == LayoutType.tile
                          ? Icons.grid_view
                          : Icons.person,
                    ),
                    tooltip: _currentLayout == LayoutType.tile
                        ? "Switch to spotlight"
                        : "Switch to tile",
                    onPressed: () async {
                      final newLayout = _currentLayout == LayoutType.tile
                          ? LayoutType.spotlight
                          : LayoutType.tile;
                      await _callSession?.setLayout(newLayout);
                    },
                  ),
                  // PiP button
                  IconButton(
                    icon: const Icon(Icons.picture_in_picture),
                    tooltip: "Enter PiP mode",
                    onPressed: () async {
                      await _callSession?.enterPipMode();
                    },
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

Remove Listener

Remove the layout listener in your widget’s dispose() method to prevent memory leaks:
@override
void dispose() {
  _callSession?.layoutListener = null;
  super.dispose();
}

Controlling Layout Programmatically

You can change the layout and PiP state programmatically:

Change Layout

// Switch to tile layout
await CallSession.getInstance()?.setLayout(LayoutType.tile);

// Switch to spotlight layout
await CallSession.getInstance()?.setLayout(LayoutType.spotlight);

Enable/Disable PiP

// Enable Picture-in-Picture
await CallSession.getInstance()?.enablePictureInPictureLayout();

// Disable Picture-in-Picture
await CallSession.getInstance()?.disablePictureInPictureLayout();

Initial Layout Configuration

Set the initial layout when joining a session:
final sessionSettings = CometChatCalls.SessionSettingsBuilder()
    .setLayout(LayoutType.tile)  // or LayoutType.spotlight
    .hideChangeLayoutButton(false)  // Allow users to change layout
    .build();

Callbacks Summary

CallbackParameterDescription
onCallLayoutChangedLayoutTypeCall layout changed (tile/spotlight)
onParticipantListVisible-Participant list panel opened
onParticipantListHidden-Participant list panel closed
onPictureInPictureLayoutEnabled-PiP mode was enabled
onPictureInPictureLayoutDisabled-PiP mode was disabled

Next Steps

Layout & UI

Control layout programmatically

Session Settings

Configure initial layout settings