import React, { useState } from "react";
import { Call, CallContent } from "@stream-io/video-react-native-sdk";
import { StyleSheet, View } from "react-native";
const VideoCallUI = () => {
const { selectedLayout } = useLayout();
let call: Call;
// your logic to create a new call or get an existing call
return (
<StreamCall call={call}>
<CallContent layout={selectedLayout} />
</StreamCall>
);
};
Runtime layout switching
Runtime Layout Switching is basically switching the participant’s layout from the app. We currently support switching between grid
and spotlight
layout modes through our SDK.
Switching the layout from the App
To switch the layout from the app we can take the help of the layout
prop of the CallContent component.
We will create a state variable in the app to track the state of the current layout and pass the state to the layout
prop of the CallContent component. This is done below:
Creating the CallHeader with Layout switching Modal/Component
We will create a component that renders the Button which on press opens up a Modal to switch the Layout. Clicking on the layout item will switch the layout and set the state for the selectedLayout
state in the VideoCallUI
component that we created above.
import React, { useState } from "react";
import {
CallControlsButton,
useTheme,
} from "@stream-io/video-react-native-sdk";
import { IconWrapper } from "@stream-io/video-react-native-sdk/src/icons";
import LayoutSwitcherModal from "./LayoutSwitcherModal";
import { ColorValue } from "react-native";
import { Grid } from "../../assets/Grid";
import { SpotLight } from "../../assets/Spotlight";
import { useLayout } from "../../contexts/LayoutContext";
export type LayoutSwitcherButtonProps = {
/**
* Handler to be called when the layout switcher button is pressed.
* @returns void
*/
onPressHandler?: () => void;
};
const getIcon = (selectedButton: string, color: ColorValue, size: number) => {
switch (selectedButton) {
case "grid":
return <Grid color={color} size={size} />;
case "spotlight":
return <SpotLight color={color} size={size} />;
default:
return "grid";
}
};
/**
* The layout switcher Button can be used to switch different layout arrangements
* of the call participants.
*/
export const LayoutSwitcherButton = ({
onPressHandler,
}: LayoutSwitcherButtonProps) => {
const {
theme: { colors, variants },
} = useTheme();
const { selectedLayout } = useLayout();
const [isModalVisible, setIsModalVisible] = useState(false);
const [anchorPosition, setAnchorPosition] = useState<{
x: number;
y: number;
width: number;
height: number;
} | null>(null);
const buttonColor = isModalVisible
? colors.iconSecondary
: colors.iconPrimary;
const handleOpenModal = () => setIsModalVisible(true);
const handleCloseModal = () => setIsModalVisible(false);
const handleLayout = (event: any) => {
const { x, y, width, height } = event.nativeEvent.layout;
setAnchorPosition({ x, y: y + height, width, height });
};
return (
<CallControlsButton
size={variants.roundButtonSizes.md}
onLayout={handleLayout}
onPress={() => {
handleOpenModal();
if (onPressHandler) {
onPressHandler();
}
setIsModalVisible(!isModalVisible);
}}
color={colors.sheetPrimary}
>
<IconWrapper>
{getIcon(selectedLayout, buttonColor, variants.iconSizes.lg)}
</IconWrapper>
<LayoutSwitcherModal
isVisible={isModalVisible}
anchorPosition={anchorPosition}
onClose={handleCloseModal}
/>
</CallControlsButton>
);
};
import React, { useEffect, useState, useMemo } from "react";
import {
View,
TouchableOpacity,
Text,
StyleSheet,
Dimensions,
Modal,
} from "react-native";
import { useTheme } from "@stream-io/video-react-native-sdk";
import { Grid } from "../../assets/Grid";
import { SpotLight } from "../../assets/Spotlight";
import { Layout, useLayout } from "../../contexts/LayoutContext";
interface AnchorPosition {
x: number;
y: number;
height: number;
}
interface PopupComponentProps {
anchorPosition?: AnchorPosition | null;
isVisible: boolean;
onClose: () => void;
}
const LayoutSwitcherModal: React.FC<PopupComponentProps> = ({
isVisible,
onClose,
anchorPosition,
}) => {
const { theme } = useTheme();
const styles = useStyles();
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
const { selectedLayout, onLayoutSelection } = useLayout();
const topInset = theme.variants.insets.top;
const leftInset = theme.variants.insets.left;
useEffect(() => {
if (isVisible && anchorPosition) {
const windowHeight = Dimensions.get("window").height;
const windowWidth = Dimensions.get("window").width;
let top = anchorPosition.y + anchorPosition.height / 2 + topInset;
let left = anchorPosition.x + leftInset;
// Ensure the popup stays within the screen bounds
if (top + 150 > windowHeight) {
top = anchorPosition.y - 150;
}
if (left + 200 > windowWidth) {
left = windowWidth - 200;
}
setPopupPosition({ top, left });
}
}, [isVisible, anchorPosition, topInset, leftInset]);
if (!isVisible || !anchorPosition) {
return null;
}
const onPressHandler = (layout: Layout) => {
onLayoutSelection(layout);
onClose();
};
return (
<Modal
transparent
visible={isVisible}
onRequestClose={onClose}
supportedOrientations={["portrait", "landscape"]}
>
<TouchableOpacity style={styles.overlay} onPress={onClose}>
<View
style={[
styles.modal,
{ top: popupPosition.top, left: popupPosition.left },
]}
>
<TouchableOpacity
style={[
styles.button,
selectedLayout === "grid" && styles.selectedButton,
]}
onPress={() => onPressHandler("grid")}
>
<Grid
size={theme.variants.iconSizes.md}
color={theme.colors.iconPrimary}
/>
<Text style={styles.buttonText}>Grid</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.button,
selectedLayout === "spotlight" && styles.selectedButton,
]}
onPress={() => onPressHandler("spotlight")}
>
<SpotLight
size={theme.variants.iconSizes.md}
color={theme.colors.iconPrimary}
/>
<Text style={styles.buttonText}>Spotlight</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
);
};
// styles omitted for brevity
export default LayoutSwitcherModal;
Layout context
We can create a context to manage the layout state and pass it to the LayoutSwitcherButton
and LayoutSwitcherModal
components.
import React, {
createContext,
useContext,
useState,
ReactNode,
useCallback,
} from "react";
export type Layout = "grid" | "spotlight";
interface LayoutContextState {
selectedLayout: Layout;
onLayoutSelection: (layout: Layout) => void;
}
const LayoutContext = createContext<LayoutContextState | null>(null);
interface LayoutProviderProps {
children: ReactNode;
}
const LayoutProvider: React.FC<LayoutProviderProps> = ({ children }) => {
const [selectedLayout, setSelectedLayout] = useState<Layout>("grid");
const onLayoutSelection = useCallback((layout: Layout) => {
setSelectedLayout(layout);
}, []);
return (
<LayoutContext.Provider value={{ selectedLayout, onLayoutSelection }}>
{children}
</LayoutContext.Provider>
);
};
const useLayout = (): LayoutContextState => {
const context = useContext(LayoutContext);
if (!context) {
throw new Error("useLayout must be used within a LayoutProvider");
}
return context;
};
export { LayoutProvider, useLayout };
Creating a custom CallHeader
We will create a custom CallHeader
component that renders the LayoutSwitcherButton
as follows.
We need to make sure the props selectedLayout
and setSelectedLayout
passed from the component that renders the CallContent
.
export const CustomCallHeader = () => {
const [topControlsHeight, setTopControlsHeight] = useState<number>(0);
const [topControlsWidth, setTopControlsWidth] = useState<number>(0);
const styles = useStyles();
const onLayout: React.ComponentProps<typeof View>["onLayout"] = (event) => {
const { height, width } = event.nativeEvent.layout;
if (setTopControlsHeight) {
setTopControlsHeight(height);
setTopControlsWidth(width);
}
};
return (
<View>
<TopViewBackground height={topControlsHeight} width={topControlsWidth} />
<View style={styles.content} onLayout={onLayout}>
<View style={styles.centerElement}>
<LayoutSwitcherButton />
</View>
</View>
</View>
);
};
Finally we use the CustomCallHeader
together with the CallContent
component as follows:
import React, { useCallback, useState } from "react";
import { Call, CallContent } from "@stream-io/video-react-native-sdk";
const VideoCallUI = () => {
const { selectedLayout } = useLayout();
let call: Call;
// your logic to create a new call or get an existing call
return (
<StreamCall call={call}>
<CustomCallHeader />
<CallContent layout={selectedLayout} />
</StreamCall>
);
};