blob: b13bf55cec26bab87ce7431d6f87081fffd7d9fa [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.embedding.engine.renderer.RenderSurface;
/**
* Paints a Flutter UI on a {@link android.view.Surface}.
*
* <p>To begin rendering a Flutter UI, the owner of this {@code FlutterSurfaceView} must invoke
* {@link #attachToRenderer(FlutterRenderer)} with the desired {@link FlutterRenderer}.
*
* <p>To stop rendering a Flutter UI, the owner of this {@code FlutterSurfaceView} must invoke
* {@link #detachFromRenderer()}.
*
* <p>A {@code FlutterSurfaceView} is intended for situations where a developer needs to render a
* Flutter UI, but does not require any keyboard input, gesture input, accessibility integrations or
* any other interactivity beyond rendering. If standard interactivity is desired, consider using a
* {@link FlutterView} which provides all of these behaviors and utilizes a {@code
* FlutterSurfaceView} internally.
*/
public class FlutterSurfaceView extends SurfaceView implements RenderSurface {
private static final String TAG = "FlutterSurfaceView";
private final boolean renderTransparently;
private boolean isSurfaceAvailableForRendering = false;
private boolean isAttachedToFlutterRenderer = false;
@Nullable private FlutterRenderer flutterRenderer;
// Connects the {@code Surface} beneath this {@code SurfaceView} with Flutter's native code.
// Callbacks are received by this Object and then those messages are forwarded to our
// FlutterRenderer, and then on to the JNI bridge over to native Flutter code.
private final SurfaceHolder.Callback surfaceCallback =
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.v(TAG, "SurfaceHolder.Callback.startRenderingToSurface()");
isSurfaceAvailableForRendering = true;
if (isAttachedToFlutterRenderer) {
connectSurfaceToRenderer();
}
}
@Override
public void surfaceChanged(
@NonNull SurfaceHolder holder, int format, int width, int height) {
Log.v(TAG, "SurfaceHolder.Callback.surfaceChanged()");
if (isAttachedToFlutterRenderer) {
changeSurfaceSize(width, height);
}
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
Log.v(TAG, "SurfaceHolder.Callback.stopRenderingToSurface()");
isSurfaceAvailableForRendering = false;
if (isAttachedToFlutterRenderer) {
disconnectSurfaceFromRenderer();
}
}
};
private final FlutterUiDisplayListener flutterUiDisplayListener =
new FlutterUiDisplayListener() {
@Override
public void onFlutterUiDisplayed() {
Log.v(TAG, "onFlutterUiDisplayed()");
// Now that a frame is ready to display, take this SurfaceView from transparent to opaque.
setAlpha(1.0f);
if (flutterRenderer != null) {
flutterRenderer.removeIsDisplayingFlutterUiListener(this);
}
}
@Override
public void onFlutterUiNoLongerDisplayed() {
// no-op
}
};
/** Constructs a {@code FlutterSurfaceView} programmatically, without any XML attributes. */
public FlutterSurfaceView(@NonNull Context context) {
this(context, null, false);
}
/**
* Constructs a {@code FlutterSurfaceView} programmatically, without any XML attributes, and with
* control over whether or not this {@code FlutterSurfaceView} renders with transparency.
*/
public FlutterSurfaceView(@NonNull Context context, boolean renderTransparently) {
this(context, null, renderTransparently);
}
/** Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner. */
public FlutterSurfaceView(@NonNull Context context, @NonNull AttributeSet attrs) {
this(context, attrs, false);
}
private FlutterSurfaceView(
@NonNull Context context, @Nullable AttributeSet attrs, boolean renderTransparently) {
super(context, attrs);
this.renderTransparently = renderTransparently;
init();
}
private void init() {
// If transparency is desired then we'll enable a transparent pixel format and place
// our Window above everything else to get transparent background rendering.
if (renderTransparently) {
getHolder().setFormat(PixelFormat.TRANSPARENT);
setZOrderOnTop(true);
}
// Grab a reference to our underlying Surface and register callbacks with that Surface so we
// can monitor changes and forward those changes on to native Flutter code.
getHolder().addCallback(surfaceCallback);
// Keep this SurfaceView transparent until Flutter has a frame ready to render. This avoids
// displaying a black rectangle in our place.
setAlpha(0.0f);
}
@Nullable
@Override
public FlutterRenderer getAttachedRenderer() {
return flutterRenderer;
}
/**
* Invoked by the owner of this {@code FlutterSurfaceView} when it wants to begin rendering a
* Flutter UI to this {@code FlutterSurfaceView}.
*
* <p>If an Android {@link android.view.Surface} is available, this method will give that {@link
* android.view.Surface} to the given {@link FlutterRenderer} to begin rendering Flutter's UI to
* this {@code FlutterSurfaceView}.
*
* <p>If no Android {@link android.view.Surface} is available yet, this {@code FlutterSurfaceView}
* will wait until a {@link android.view.Surface} becomes available and then give that {@link
* android.view.Surface} to the given {@link FlutterRenderer} to begin rendering Flutter's UI to
* this {@code FlutterSurfaceView}.
*/
public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) {
Log.v(TAG, "Attaching to FlutterRenderer.");
if (this.flutterRenderer != null) {
Log.v(
TAG,
"Already connected to a FlutterRenderer. Detaching from old one and attaching to new one.");
this.flutterRenderer.stopRenderingToSurface();
this.flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
}
this.flutterRenderer = flutterRenderer;
isAttachedToFlutterRenderer = true;
this.flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
// If we're already attached to an Android window then we're now attached to both a renderer
// and the Android window. We can begin rendering now.
if (isSurfaceAvailableForRendering) {
Log.v(
TAG,
"Surface is available for rendering. Connecting FlutterRenderer to Android surface.");
connectSurfaceToRenderer();
}
}
/**
* Invoked by the owner of this {@code FlutterSurfaceView} when it no longer wants to render a
* Flutter UI to this {@code FlutterSurfaceView}.
*
* <p>This method will cease any on-going rendering from Flutter to this {@code
* FlutterSurfaceView}.
*/
public void detachFromRenderer() {
if (flutterRenderer != null) {
// If we're attached to an Android window then we were rendering a Flutter UI. Now that
// this FlutterSurfaceView is detached from the FlutterRenderer, we need to stop rendering.
// TODO(mattcarroll): introduce a isRendererConnectedToSurface() to wrap "getWindowToken() !=
// null"
if (getWindowToken() != null) {
Log.v(TAG, "Disconnecting FlutterRenderer from Android surface.");
disconnectSurfaceFromRenderer();
}
// Make the SurfaceView invisible to avoid showing a black rectangle.
setAlpha(0.0f);
this.flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
flutterRenderer = null;
isAttachedToFlutterRenderer = false;
} else {
Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached.");
}
}
// FlutterRenderer and getSurfaceTexture() must both be non-null.
private void connectSurfaceToRenderer() {
if (flutterRenderer == null || getHolder() == null) {
throw new IllegalStateException(
"connectSurfaceToRenderer() should only be called when flutterRenderer and getHolder() are non-null.");
}
flutterRenderer.startRenderingToSurface(getHolder().getSurface());
}
// FlutterRenderer must be non-null.
private void changeSurfaceSize(int width, int height) {
if (flutterRenderer == null) {
throw new IllegalStateException(
"changeSurfaceSize() should only be called when flutterRenderer is non-null.");
}
Log.v(
TAG,
"Notifying FlutterRenderer that Android surface size has changed to "
+ width
+ " x "
+ height);
flutterRenderer.surfaceChanged(width, height);
}
// FlutterRenderer must be non-null.
private void disconnectSurfaceFromRenderer() {
if (flutterRenderer == null) {
throw new IllegalStateException(
"disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null.");
}
flutterRenderer.stopRenderingToSurface();
}
}