Customizing the Layout Manager

The layout manager is an IceLink component that manages the lifecycle of UI elements in a video chat. If you have worked with the IceLink examples, you are already familiar with this component. What you may not know, however, is that the layout manager has several customization options. This section describes how to use these customization options to create a custom layout for your application.

Changing the Layout

The simplest way to customize a layout is to make use of the built-in customization options available on the FM.IceLink.LayoutManager class itself. First, create a new instance of one of the built-in layout manager implementations. The exact implementation that you will use depends on your platform. The code samples below review how to initialize a layout manager for each platform. If you need further review, refer back to the section on Capturing Local Media.

System.Windows.Forms.Control container;
var layoutManager = new FM.IceLink.WinForms.LayoutManager(container);
java.awt.Container container;
fm.icelink.java.LayoutManager layoutManager = new fm.icelink.java.LayoutManager(container);
UIView* container;
FMIceLinkCocoaLayoutManager layoutManager = [[FMIceLinkCocoaLayoutManager layoutManagerWithContainer: container] retain];
UIView container;
var layoutManager = FMIceLinkCocoaLayoutManager(container: container)
var container;
var layoutManager = new fm.icelink.DomLayoutManager(container);

Once the layout manager instance is instantiated, you can modify any of the built-in properties to customize the layout. The next sections will describe some of the available properties and what effect they have on the layout.

Changing the Layout Origin

The layout origin determines which point in a layout is (0, 0). The default is that (0, 0) is at the top left corner. If you reverse this, by specifying the top right corner as (0, 0), you will get a mirrored layout. In this context, mirrored does not mean that the video feeds themselves will be mirrored. It means that the position of the video feeds in their container will be mirrored. For example, normally, the local video feed is shown in the bottom right hand corner and the remote video feeds fill the remaining area. If you specify the FM.IceLink.LayoutOrigin.TopRight layout origin, the local video feed will instead displayed in the bottom left corner.

To change the layout origin, modify the value of the LayoutOrigin property on your FM.IceLink.LayoutManager instance. Once the origin is set, you must invoke the UpdateLayout method to refresh the UI. This is shown below.

layoutManager.LayoutOrigin = FM.IceLink.LayoutOrigin.TopRight;
layoutManager.UpdateLayout();
layoutManager.setLayoutOrigin(fm.icelink.LayoutOrigin.TopRight);
layoutManager.updateLayout();
[layoutManager setLayoutOrigin:FMIceLinkLayoutOriginTopRight];
[layoutManager updateLayout];
layoutManager.setLayoutOrigin(FMIceLinkLayoutOriginTopRight)
layoutManager.updateLayout()
layoutManager.setLayoutOrigin(fm.icelink.LayoutOrigin.TopRight);
layoutManager.updateLayout();

Changing the Layout Mode

The layout mode determines how the local and remote video feeds are arranged in the video container. The default value of this property is FM.IceLink.LayoutMode.FloatLocal. In this mode, the remote video feeds expand to fill the visible space, and the local video feed is displayed in the bottom right corner, "floating" above the other feeds.

To change the layout mode, modify the value of the LayoutMode property on your FM.IceLink.LayoutManager instance. Once the mode is changed, you must invoke the UpdateLayout method to refresh the UI, as demonstrated below.

layoutManager.LayoutMode = FM.IceLink.LayoutMode.FloatRemote;
layoutManager.UpdateLayout();
layoutManager.setLayoutMode(fm.icelink.LayoutMode.FloatRemote);
layoutManager.updateLayout();
[layoutManager setLayoutMode:FMIceLinkLayoutModeFloatRemote];
[layoutManager updateLayout];
layoutManager.setLayoutMode(FMIceLinkLayoutModeFloatRemote)
layoutManager.updateLayout()
layoutManager.setLayoutMode(fm.icelink.LayoutMode.FloatRemote);
layoutManager.updateLayout();


There are four layout modes that you can select from:

  • FloatLocal: The local video feed floats over top of the remote video feeds, which expand to take up most of the visible space.
  • FloatRemote: The remote video feeds float over top of the local video feed, which expands to take up most of the visible space.
  • Block: The local video feed is shown on its own line, with the remote video feeds taking up space above it.
  • Inline: The local video feed is shown to the left of the remote video feeds, with the remote video feeds taking up the space to the right of it.

Using Layout Presets

Developing layouts can be time consuming. If you prefer to use a preset layout, IceLink provides several default layouts that look similar to the video conference layouts in other well-known applications. You can either use one of these presets as is, or you can use them as a starting point to apply further customizations.

To apply a preset, invoke the ApplyPreset method of your FM.IceLink.LayoutManager instance. Once the preset has been applied, invoke the UpdateLayout method to refresh the UI.

layoutManager.ApplyPreset(FM.IceLink.LayoutPreset.Skype);
layoutManager.UpdateLayout();
layoutManager.applyPreset(fm.icelink.LayoutPreset.getSkype());
layoutManager.updateLayout();
[layoutManager applyPreset:[FMIceLinkLayoutPreset skype]];
[layoutManager updateLayout];
layoutManager.applyPreset(FMIceLinkLayoutPreset.skype())
layotuManager.updateLayout()
layoutManager.applyPreset(fm.icelink.LayoutPreset.Skype);
layoutManager.updateLayout();


There are currently three presets. You can select from FM.IceLink.LayoutPreset.Skype, FM.IceLink.LayoutPreset.GoogleHangouts and FM.IceLink.LayoutPreset.Facetime. Each preset uses a layout similar to the service it is named after.

Implementing a Custom Layout Manager

The layout manager customizations, while powerful, have limitations. As an example of a UI that is beyond the limitations of the built-in layout manager, imagine a layout where each remote video feed is arranged in a ring around a local video feed. This, and other complex UI customizations are not supported by the core layout manager. For these, you will need to implement your own custom layout manager.

This is not as difficult as it sounds. There are only four operations that you need to implement: - add a new video control to the video container - remove an existing video control from the video container - dispatch an action to the main thread - draw the layout

You will need to create a new class for your custom layout manager that implements these four operations. The following section will demonstrate the implementation of these operations using layout managers that are included with IceLink. As a starting point, create a new layout manager class that inherits from FM.IceLink.LayoutManager<T>. The generic type parameter, in this case, refers to the type of video control that is used to display a video feed. For JavaScript, no type parameter is necessary.

public class LayoutManager : FM.IceLink.LayoutManager<System.Windows.Forms.Control>
{
    public System.Windows.Forms.Control Container { get; private set; }

    public LayoutManager(System.Windows.Forms.Control container)
    {
        Container = container;
    }
}
public class LayoutManager extends fm.icelink.LayoutManager<java.awt.Component> {
    private java.awt.Container _container;

    public LayoutManager(java.awt.Container container) {
        _container = container;
    ]
}
@interface FMIceLinkCocoaLayoutManager : FMIceLinkLayoutManager<UIView> {
    UIView *_container;
}

- (instancetype)initWithContainer: UIView* container;
@end

@implementation FMIceLinkCocoaLayoutManager
- (instancetype)initWithContainer: UIView* container {
    self = [super initWithPreset:nil];

    _container = [container retain];

    return self;
}
@end
```swift
public class FMIceLinkCocoaLayoutManager : FMIceLinkLayoutManager {
    var _container:UIView

    init(container:UIView) {
        super.init(preset: nil)
    }
}
function DomLayoutManager(container) {
    fm.icelink.LayoutManager.call(this, container);

    this._container = container;
}


Note that whatever constructor you create for your new class must have at least one parameter that contains the video "container" object. This is required because it is required by the parent layout manager's constructor.

Adding a Video Control

When the application layer invokes the AddRemoteView or SetLocalView methods of an FM.IceLink.LayoutManager instance, the layout manager will do two things. First, it will store the view. Second, it will invoke the AddView method to display the new video control. Since you will only be implementing the AddView method, this means that you only have to worry about displaying the video control. This is often as simple as adding the control to the layout manager's video container.

protected override void AddView(System.Windows.Form.Control view)
{
    Container.Controls.Add(view);
}
@Override
protected void addView(java.awt.Component view) {
    container.add(view);
}
- (void)addView: (NSObject *)view {
    [_container addSubview:(UIView *)view];
}
override func addView(view: NSObject) {
    _container.addSubview(view as UIView)
}
function addView(view) {
    this.container.appendChild(view);
}


You do not need to manually call UpdateLayout here. The layout manager will refresh itself, as adding a video control implies that the layout should be updated.

Removing a Video Control

The RemoveView method of the layout manager is the reverse of the AddView method. It is invoked as a result of the application layer invoking the UnsetLocalView and RemoveRemoteView methods. Similar to the AddView method, the only thing your implementation must do is to handle removing the video control from the display.

protected override void RemoveView(System.Windows.Form.Control view)
{
    Container.Controls.Remove(view);
}
@Override
protected void removeView(java.awt.Component view) {
    container.remove(view);
}
- (void)removeView: (NSObject *)view {
    [(UIView *)view removeFromSuperview];
}
override func removeView(view: NSObject) {
    (view as UIView).removeFromSuperview()
}
function addView(view) {
    this.container.removeChild(view);
}


Again, there is no need to call UpdateLayout, as the layout manager will automatically update itself.

Executing on the Main Thread

On several platforms, the UI is manipulated on a single UI thread. This complicates matters for the layout manager. Often, UI work is performed as the result of an event handler, which normally operates on a separate thread. To work around this, the layout manager interface requires you to define a DispatchToMainThread method, which allows it to execute actions on the main application thread.

Some platform specific implementations follow. You can use these verbatim - there is not much else you could add to them, even if you wanted to. JavaScript is single-threaded, so for its implementation of this method you can simply invoke the action directly.


protected override void DispatchToMainThread(Action2<object, object> action, object arg1, object arg2)
{
    if (Container.InvokeRequired)
    {
        Container.BeginInvoke(new Action0(() =>
        {
            action(arg1, arg2);
        }));
    }
    else
    {
        action(arg1, arg2);
    }
}
@Override
protected void dispatchToMainThread(final fm.icelink.IAction2<Object, Object> action, final Object arg1, final Object arg2) {
    final IAction0 wrappedAction = () -> {
        action.invoke(arg1, arg2);
    };

    if (java.awt.EventQueue.isDispatchThread()) {
        wrappedAction.invoke();
    } else {
        java.awt.EventQueue.invokeLater(wrappedAction);
    }
}
- (void)dispatchToMainThreadWithAction:(FMIceLinkAction2<NSObject*, NSObject*> *)action arg1:(NSObject *)arg1 arg2:(NSObject *)arg2 {
    dispatch_async(dispatch_get_main_queue(), ^ {
        [action invokeWithP1:arg1 p2:arg2];
    });
}
override func dispatchToMainThread(action: FMIceLinkAction2) {
    DispatchQueue.main.async {
        action.invoke(P1: args1, P2: arg2)
    }
}
function dispatchToMainThread(action, arg1, arg2) {
    action(arg1, arg2);
}

Drawing the Layout

The last step is to actually define how the layout should be drawn. This guide cannot help you decide how you want your layout to look but it can help by providing you a rough outline of how the default layout managers work. Examine the following code from the internal layout managers.

public override void Layout()
{
    var localView = GetLocalView();
    var remoteViews = GetRemoteViews();

    if (localView != null)
    {
        // draw the local view
    }

    for (var i = 0; i < remoteViews.Length; i++)
    {
        // draw each remote view
    }
}
@Override
public void layout() {
    java.awt.Component localView = getLocalView();
    java.util.List<java.awt.Component> remoteViews = getRemoteViews();

    if (localView != null) {
        // draw the local view
    }

    for (int i = 0; i <remoteViews.length; i++) {
        // draw each remote view
    }
}
- (void)layout {
    UIView *localView = (UIView *)[self getLocalView];
    NSArray *remoteViews = [self getRemoteViews];

    if (localView) {
        // draw the local view
    }

    for i in 0..<remoteViews.count {
        // draw each remote view
    }
}
override func layout() {
    var localView = self.getLocalView() as UIView
    var remoteViews = self.getRemoteViews()

    if (localView) {
        // draw the local view
    }

    for remoteView in remoteViews {
        // draw each remote view
    }
}
function layout() {
    var localView = this.getLocalView();
    var remoteViews = this.getRemoteViews();

    if (localView) {
        // draw the local view
    }

    for (var i = 0; i < remoteViews.length; i++) {
        // draw each remote view
    }
}


Note that you must always check that the local view is not null before attempting to use it. The Layout method is called at many points during the lifecycle of a video conference and often the local video feed is not immediately available.

Wrapping Up

You should now have a much better understanding of the IceLink layout manager. If you're still a bit unsure over how this is actually used in an application, you should review the section on Capturing Local Media, which describes how a layout manager is used in a typical application.