blob: d46f5346d7ab377b2abc392f62ba0b52df573782 [file] [log] [blame]
package io.flutter.embedding.engine.mutatorsstack;
import static android.view.View.OnFocusChangeListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.android.AndroidTouchProcessor;
/**
* A view that applies the {@link io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack} to
* its children.
*/
public class FlutterMutatorView extends FrameLayout {
private FlutterMutatorsStack mutatorsStack;
private float screenDensity;
private int left;
private int top;
private int prevLeft;
private int prevTop;
private final AndroidTouchProcessor androidTouchProcessor;
/**
* Initialize the FlutterMutatorView. Use this to set the screenDensity, which will be used to
* correct the final transform matrix.
*/
public FlutterMutatorView(
@NonNull Context context,
float screenDensity,
@Nullable AndroidTouchProcessor androidTouchProcessor) {
super(context, null);
this.screenDensity = screenDensity;
this.androidTouchProcessor = androidTouchProcessor;
}
/** Initialize the FlutterMutatorView. */
public FlutterMutatorView(@NonNull Context context) {
this(context, 1, /* androidTouchProcessor=*/ null);
}
/**
* Determines if the current view or any descendant view has focus.
*
* @param root The root view.
* @return True if the current view or any descendant view has focus.
*/
@VisibleForTesting
public static boolean childHasFocus(@Nullable View root) {
if (root == null) {
return false;
}
if (root.hasFocus()) {
return true;
}
if (root instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) root;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
if (childHasFocus(viewGroup.getChildAt(idx))) {
return true;
}
}
}
return false;
}
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
/**
* Sets a focus change listener that notifies when the current view or any of its descendant views
* have received focus.
*
* <p>If there's an active focus listener, it will first remove the current listener, and then add
* the new one.
*
* @param userFocusListener A user provided focus listener.
*/
public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
unsetOnDescendantFocusChangeListener();
final View mutatorView = this;
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener == null) {
activeFocusListener =
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
}
};
observer.addOnGlobalFocusChangeListener(activeFocusListener);
}
}
/** Unsets any active focus listener. */
public void unsetOnDescendantFocusChangeListener() {
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener != null) {
final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener;
activeFocusListener = null;
observer.removeOnGlobalFocusChangeListener(currFocusListener);
}
}
/**
* Pass the necessary parameters to the view so it can apply correct mutations to its children.
*/
public void readyToDisplay(
@NonNull FlutterMutatorsStack mutatorsStack, int left, int top, int width, int height) {
this.mutatorsStack = mutatorsStack;
this.left = left;
this.top = top;
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
layoutParams.leftMargin = left;
layoutParams.topMargin = top;
setLayoutParams(layoutParams);
setWillNotDraw(false);
}
@Override
public void draw(Canvas canvas) {
// Apply all clippings on the parent canvas.
canvas.save();
for (Path path : mutatorsStack.getFinalClippingPaths()) {
// Reverse the current offset.
//
// The frame of this view includes the final offset of the bounding rect.
// We need to apply all the mutators to the view, which includes the mutation that leads to
// the final offset. We should reverse this final offset, both as a translate mutation and to
// all the clipping paths
Path pathCopy = new Path(path);
pathCopy.offset(-left, -top);
canvas.clipPath(pathCopy);
}
super.draw(canvas);
canvas.restore();
}
@Override
public void dispatchDraw(Canvas canvas) {
// Apply all the transforms on the child canvas.
canvas.save();
canvas.concat(getPlatformViewMatrix());
super.dispatchDraw(canvas);
canvas.restore();
}
private Matrix getPlatformViewMatrix() {
Matrix finalMatrix = new Matrix(mutatorsStack.getFinalMatrix());
// Reverse scale based on screen scale.
//
// The Android frame is set based on the logical resolution instead of physical.
// (https://developer.android.com/training/multiscreen/screendensities).
// However, flow is based on the physical resolution. For example, 1000 pixels in flow equals
// 500 points in Android. And until this point, we did all the calculation based on the flow
// resolution. So we need to scale down to match Android's logical resolution.
finalMatrix.preScale(1 / screenDensity, 1 / screenDensity);
// Reverse the current offset.
//
// The frame of this view includes the final offset of the bounding rect.
// We need to apply all the mutators to the view, which includes the mutation that leads to
// the final offset. We should reverse this final offset, both as a translate mutation and to
// all the clipping paths
finalMatrix.postTranslate(-left, -top);
return finalMatrix;
}
/** Intercept the events here and do not propagate them to the child platform views. */
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return true;
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) {
if (androidTouchProcessor == 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 androidTouchProcessor.onTouchEvent(event, screenMatrix);
}
}