blob: aca60f4ca02e2b8793b59cedbf26bfd223fe7a66 [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.plugin.platform;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.util.ViewUtils;
/**
* Wraps a platform view to intercept gestures and project this view onto a {@link
* PlatformViewRenderTarget}.
*
* <p>An Android platform view is composed by the engine using a {@code TextureLayer}. The view is
* embeded to the Android view hierarchy like a normal view, but it's projected onto a {@link
* PlatformViewRenderTarget}, so it can be efficiently composed by the engine.
*
* <p>Since the view is in the Android view hierarchy, keyboard and accessibility interactions
* behave normally.
*/
@TargetApi(23)
public class PlatformViewWrapper extends FrameLayout {
private static final String TAG = "PlatformViewWrapper";
private int prevLeft;
private int prevTop;
private int left;
private int top;
private AndroidTouchProcessor touchProcessor;
private PlatformViewRenderTarget renderTarget;
private ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
public PlatformViewWrapper(@NonNull Context context) {
super(context);
setWillNotDraw(false);
}
public PlatformViewWrapper(
@NonNull Context context, @NonNull PlatformViewRenderTarget renderTarget) {
this(context);
this.renderTarget = renderTarget;
}
/**
* Sets the touch processor that allows to intercept gestures.
*
* @param newTouchProcessor The touch processor.
*/
public void setTouchProcessor(@Nullable AndroidTouchProcessor newTouchProcessor) {
touchProcessor = newTouchProcessor;
}
/**
* Sets the layout parameters for this view.
*
* @param params The new parameters.
*/
public void setLayoutParams(@NonNull FrameLayout.LayoutParams params) {
super.setLayoutParams(params);
left = params.leftMargin;
top = params.topMargin;
}
public void resizeRenderTarget(int width, int height) {
if (renderTarget != null) {
renderTarget.resize(width, height);
}
}
public int getRenderTargetWidth() {
if (renderTarget != null) {
return renderTarget.getWidth();
}
return 0;
}
public int getRenderTargetHeight() {
if (renderTarget != null) {
return renderTarget.getHeight();
}
return 0;
}
/** Releases resources. */
public void release() {
if (renderTarget != null) {
renderTarget.release();
renderTarget = null;
}
}
@Override
public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
return true;
}
@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
final View embeddedView = getChildAt(0);
if (embeddedView != null
&& embeddedView.getImportantForAccessibility()
== View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
return false;
}
// Forward the request only if the embedded view is in the Flutter accessibility tree.
// The embedded view may be ignored when the framework doesn't populate a SemanticNode
// for the current platform view.
// See AccessibilityBridge for more.
return super.requestSendAccessibilityEvent(child, event);
}
/** Used on Android O+, {@link invalidateChildInParent} used for previous versions. */
@SuppressLint("NewApi")
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
super.onDescendantInvalidated(child, target);
invalidate();
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidate();
return super.invalidateChildInParent(location, dirty);
}
@Override
@SuppressLint("NewApi")
public void draw(Canvas canvas) {
if (renderTarget == null) {
super.draw(canvas);
Log.e(TAG, "Platform view cannot be composed without a RenderTarget.");
return;
}
final Canvas targetCanvas = renderTarget.lockHardwareCanvas();
if (targetCanvas == null) {
// Cannot render right now.
invalidate();
return;
}
try {
// Fill the render target with transparent pixels. This is needed for platform views that
// expect a transparent background.
targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Override the canvas that this subtree of views will use to draw.
super.draw(targetCanvas);
} finally {
renderTarget.unlockCanvasAndPost(targetCanvas);
}
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (touchProcessor == null) {
return super.onTouchEvent(event);
}
final Matrix screenMatrix = new Matrix();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
prevLeft = left;
prevTop = top;
screenMatrix.postTranslate(left, top);
break;
case MotionEvent.ACTION_MOVE:
// While the view is dragged, use the left and top positions as
// they were at the moment the touch event fired.
screenMatrix.postTranslate(prevLeft, prevTop);
prevLeft = left;
prevTop = top;
break;
case MotionEvent.ACTION_UP:
default:
screenMatrix.postTranslate(left, top);
break;
}
return touchProcessor.onTouchEvent(event, screenMatrix);
}
@VisibleForTesting
public ViewTreeObserver.OnGlobalFocusChangeListener getActiveFocusListener() {
return this.activeFocusListener;
}
public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
unsetOnDescendantFocusChangeListener();
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener == null) {
activeFocusListener =
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
userFocusListener.onFocusChange(
PlatformViewWrapper.this, ViewUtils.childHasFocus(PlatformViewWrapper.this));
}
};
observer.addOnGlobalFocusChangeListener(activeFocusListener);
}
}
public void unsetOnDescendantFocusChangeListener() {
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener != null) {
final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener;
activeFocusListener = null;
observer.removeOnGlobalFocusChangeListener(currFocusListener);
}
}
}