Android: How to make a circular view as a thumbnail of OpenTok?

I have a requirement as the image below.

Nhan Cao
Jul 6 · 3 min read

Because OpenTok using GLSurfaceView for a renderer, and with multi GLSurfaceView on screen, we must use .setZOrderOnTop(true); for all thumbnail function. That is the problem.

I tried with a circle drawable XML place at parent view as background, it does not work.

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">

<solid
android:color="#666666"/>

<size
android:width="120dp"
android:height="120dp"/>
</shape>

I tried with customizing ViewGroup such as FrameLayout and it does not work also.

public class CircleFrameLayout extends FrameLayout {
private Path clippingPath;
public CircleFrameLayout(Context context) {
this(context, null, 0, 0);
}
public CircleFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}

public CircleFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CircleFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
int radius = Math.min(w, h) / 2;
clippingPath = new Path();
clippingPath.addCircle(w / 2, h / 2, radius, Path.Direction.CW);
}
}

@Override
protected void dispatchDraw(Canvas canvas) {
int count = canvas.save();
canvas.clipPath(clippingPath);
super.dispatchDraw(canvas);
canvas.restoreToCount(count);
}
}

I tried with circle Card view, the result is the same as the way above, not work

<android.support.v7.widget.CardView
android:id="@+id/cardView"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center|center"
app:cardCornerRadius="60dp"
app:cardElevation="2dp"
/>

The ViewGroup was a circle as initial, but after GLSurfaceView added, it’s wrong. If you turn off setZOrderOnTop, it works fine. But with multi thumbnail stream, it’s not stable at rendering part, sometimes we get a blank or pending view.

My solution is a custom renderer view of OpenTok with a little change at shader code

+ "  float radius = 0.5;\n"
+ " vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);\n"
+ " vec4 color1 = vec4(r, g, b, 1.0);\n"
+ " vec2 st = (gl_FragCoord.xy/radiusDp.xy);"
+ " float dist = radius - distance(st,vec2(0.5));\n"
+ " float t = 1.0;\n"
+ " if (dist < 0.0) t = 0.0;\n"
+ " gl_FragColor = mix(color0, color1, t);\n"

I assume the thumbnail size is 90dp

<FrameLayout
android:id="@+id/publisher_container"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="bottom|end"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
/>

ThumbnailCircleVideoRenderer.java

Config opentok publishser view at activity

mPublisherViewContainer = findViewById(R.id.publisher_container);mPublisher = new Publisher.Builder(this)
.name("publisher")
.renderer(new ThumbnailCircleVideoRenderer(MainActivity.this))
.build();
if (mPublisher.getView() instanceof GLSurfaceView) {
((GLSurfaceView) (mPublisher.getView())).setZOrderOnTop(true);
}
mPublisherViewContainer.addView(mPublisher.getView());

And it work

For improving using, I make a DynamicVideoRenderer provide the way switch between fullscreen to thumbnail circle.

I create a new thumbnail circle place bottom left.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>

<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
>

<FrameLayout
android:id="@+id/subscriber_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>

<FrameLayout
android:id="@+id/publisher_container"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="bottom|end"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
/>

<FrameLayout
android:id="@+id/thumbnail_container"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="bottom|start"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
/>
</FrameLayout>
</android.support.constraint.ConstraintLayout>

DynamicVideoRenderer.java

My approach render view to subscriber_container after received video data of subscriber, then switch to thumbnail_container view after 4 seconds and repeat switch to subscriber_container

@Override
public void onVideoDataReceived(SubscriberKit subscriberKit) {
mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL);
mSubscriberViewContainer.addView(mSubscriber.getView());
switchToThumbnailCircle();
}

// TODO: 2019-07-06 Change view to thumbnail circle
public void switchToThumbnailCircle() {
thumbnailContainer.postDelayed(() -> {
View view = mSubscriber.getView();
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}

if (view instanceof GLSurfaceView) {
((GLSurfaceView) (view)).setZOrderOnTop(true);
if (mSubscriber.getRenderer() instanceof DynamicVideoRenderer) {
((DynamicVideoRenderer) (mSubscriber.getRenderer())).enableThumbnailCircle(true);
thumbnailContainer.addView(view);
}
}
switchToFullScreenView();
}, 4000);
}

// TODO: 2019-07-06 Restore view fullscreen
public void switchToFullScreenView() {
mSubscriberViewContainer.postDelayed(() -> {
View view = mSubscriber.getView();
if (view.getParent() != null) {
((ViewGroup) view.getParent()).removeView(view);
}

if (view instanceof GLSurfaceView) {
((GLSurfaceView) (view)).setZOrderOnTop(false);
if (mSubscriber.getRenderer() instanceof DynamicVideoRenderer) {
((DynamicVideoRenderer) (mSubscriber.getRenderer())).enableThumbnailCircle(false);
mSubscriberViewContainer.addView(view);
}
}
switchToThumbnailCircle();
}, 4000);
}

The results here

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade