UI preview of selected element

Summary:
This is a prototype for view preview within Flipper.
If enabled, a preview of the selected element is rendered in the attribute inspector.

Changelog: Add view preview/snapshot for the Layout plugin on Android.

Reviewed By: mweststrate

Differential Revision: D35009246

fbshipit-source-id: a442ff7f57093f463016811f0f451b52f579b448
This commit is contained in:
Lorenzo Blasa
2022-03-28 06:51:53 -07:00
committed by Facebook GitHub Bot
parent aed7e7e6f2
commit cfdb363ab4
19 changed files with 245 additions and 10 deletions

View File

@@ -10,6 +10,8 @@ package com.facebook.flipper.plugins.inspector;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -24,6 +26,7 @@ import com.facebook.flipper.core.FlipperReceiver;
import com.facebook.flipper.core.FlipperResponder;
import com.facebook.flipper.plugins.common.MainThreadFlipperReceiver;
import com.facebook.flipper.plugins.inspector.descriptors.ApplicationDescriptor;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -37,6 +40,7 @@ public class InspectorFlipperPlugin implements FlipperPlugin {
private DescriptorMapping mDescriptorMapping;
private ObjectTracker mObjectTracker;
private String mHighlightedId;
private boolean mHighlightedAlignmentMode;
TouchOverlayView mTouchOverlay;
private FlipperConnection mConnection;
private @Nullable List<ExtensionCommand> mExtensionCommands;
@@ -147,6 +151,7 @@ public class InspectorFlipperPlugin implements FlipperPlugin {
connection.receive("getSearchResults", mGetSearchResults);
connection.receive("getAXRoot", mGetAXRoot);
connection.receive("getAXNodes", mGetAXNodes);
connection.receive("getSnapshot", mGetSnapshot);
connection.receive("onRequestAXFocus", mOnRequestAXFocus);
connection.receive(
"shouldShowLithoAccessibilitySettings", mShouldShowLithoAccessibilitySettings);
@@ -414,6 +419,45 @@ public class InspectorFlipperPlugin implements FlipperPlugin {
setHighlighted(nodeId, true, isAlignmentMode);
}
mHighlightedId = nodeId;
mHighlightedAlignmentMode = isAlignmentMode;
}
};
final FlipperReceiver mGetSnapshot =
new MainThreadFlipperReceiver() {
@Override
public void onReceiveOnMainThread(final FlipperObject params, FlipperResponder responder)
throws Exception {
final String nodeId = params.getString("id");
if (mHighlightedId != null) {
setHighlighted(mHighlightedId, false, false);
}
try {
final Bitmap bitmap = getSnapshot(nodeId, true);
if (bitmap != null) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
final String base64 = Base64.encodeToString(byteArray, Base64.DEFAULT);
responder.success(
new FlipperObject.Builder().put("id", nodeId).put("snapshot", base64).build());
} else {
throw new Exception("An error occurred whilst trying to encode snapshot");
}
} catch (Exception ex) {
responder.error(
new FlipperObject.Builder()
.put("error", "unable to obtain snapshpt for object")
.put("id", nodeId)
.build());
} finally {
if (mHighlightedId != null) {
setHighlighted(mHighlightedId, true, mHighlightedAlignmentMode);
}
}
}
};
@@ -546,6 +590,34 @@ public class InspectorFlipperPlugin implements FlipperPlugin {
descriptor.setHighlighted(obj, highlighted, isAlignmentMode);
}
private @Nullable Bitmap getSnapshot(final String id, final boolean includeChildren)
throws Exception {
if (id == null) {
return null;
}
final Object obj = mObjectTracker.get(id);
if (obj == null) {
return null;
}
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
if (descriptor == null) {
return null;
}
try {
return descriptor.getSnapshot(obj, includeChildren);
} catch (java.lang.AbstractMethodError error) {
/* There may be some descriptors which may not provide an implementation.
* In those cases, java.lang.AbstractMethodError will be thrown which
* does not inherit from java.lang.Exception hence will not be caught
* by the try/catch block of the caller.
*/
return null;
}
}
private boolean hasAXNode(FlipperObject node) {
FlipperObject extraInfo = node.getObject("extraInfo");
return extraInfo != null && extraInfo.getBoolean("linkedNode");

View File

@@ -7,6 +7,7 @@
package com.facebook.flipper.plugins.inspector;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import com.facebook.flipper.core.ErrorReportingRunnable;
import com.facebook.flipper.core.FlipperArray;
@@ -175,6 +176,13 @@ public abstract class NodeDescriptor<T> {
public abstract void setHighlighted(T node, boolean selected, boolean isAlignmentMode)
throws Exception;
/**
* Get a snapshot of this node.
*
* @return A Bitmap representation of the specified node.
*/
public abstract @Nullable Bitmap getSnapshot(T node, boolean includeChildren) throws Exception;
/**
* Perform hit testing on the given node. Either continue the search in a child with {@link
* Touch#continueWithOffset(int, int, int)} or finish the hit testing on this node with {@link

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Activity;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.Window;
import com.facebook.flipper.core.FlipperDynamic;
@@ -93,6 +94,12 @@ public class ActivityDescriptor extends NodeDescriptor<Activity> {
descriptor.setHighlighted(node.getWindow(), selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(Activity node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(Window.class);
return descriptor.getSnapshot(node.getWindow(), includeChildren);
}
@Override
public void hitTest(Activity node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Activity;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
@@ -241,6 +242,19 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
}
}
@Override
public Bitmap getSnapshot(ApplicationWrapper node, boolean includeChildren) throws Exception {
final int childCount = getChildCount(node);
if (childCount > 0) {
final Object topChild = getChildAt(node, childCount - 1);
final NodeDescriptor descriptor = descriptorForClass(topChild.getClass());
if (descriptor != null) {
return descriptor.getSnapshot(topChild, includeChildren);
}
}
return null;
}
private void runHitTest(ApplicationWrapper node, Touch touch, boolean ax) {
final int childCount = getChildCount(node);

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Dialog;
import android.graphics.Bitmap;
import android.view.Window;
import com.facebook.flipper.core.FlipperDynamic;
import com.facebook.flipper.core.FlipperObject;
@@ -68,6 +69,12 @@ public class DialogDescriptor extends NodeDescriptor<Dialog> {
descriptor.setHighlighted(node.getWindow(), selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(Dialog node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(Window.class);
return descriptor.getSnapshot(node.getWindow(), includeChildren);
}
@Override
public void hitTest(Dialog node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -10,6 +10,7 @@ package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.graphics.Bitmap;
import com.facebook.flipper.core.FlipperDynamic;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.plugins.inspector.Named;
@@ -84,6 +85,15 @@ public class DialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
}
}
@Override
public Bitmap getSnapshot(DialogFragment node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
if (node.getDialog() != null) {
return descriptor.getSnapshot(node.getDialog(), includeChildren);
}
return null;
}
@Override
public void hitTest(DialogFragment node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -7,7 +7,10 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
@@ -132,6 +135,30 @@ public class DrawableDescriptor extends NodeDescriptor<Drawable> {
}
}
@Override
public Bitmap getSnapshot(Drawable node, boolean includeChildren) throws Exception {
Bitmap bitmap = null;
if (node instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) node;
if (bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if (node.getIntrinsicWidth() <= 0 || node.getIntrinsicHeight() <= 0) {
return null;
} else {
bitmap =
Bitmap.createBitmap(
node.getIntrinsicWidth(), node.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
node.draw(canvas);
return bitmap;
}
@Override
public void hitTest(Drawable node, Touch touch) {
touch.finish();

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Fragment;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import com.facebook.flipper.core.FlipperDynamic;
@@ -108,6 +109,16 @@ public class FragmentDescriptor extends NodeDescriptor<Fragment> {
descriptor.setHighlighted(node.getView(), selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(Fragment node, boolean includeChildren) throws Exception {
if (node.getView() == null) {
return null;
}
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node.getView(), includeChildren);
}
@Override
public void hitTest(Fragment node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -7,6 +7,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.graphics.Bitmap;
import android.view.View;
import android.widget.ImageView;
import com.facebook.flipper.core.FlipperDynamic;
@@ -149,6 +150,12 @@ public class ImageViewDescriptor extends NodeDescriptor<ImageView> {
descriptor.setHighlighted(node, selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(ImageView node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node, includeChildren);
}
@Override
public void hitTest(ImageView node, Touch touch) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);

View File

@@ -7,6 +7,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.graphics.Bitmap;
import com.facebook.flipper.core.FlipperDynamic;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.plugins.inspector.Named;
@@ -62,6 +63,11 @@ public class ObjectDescriptor extends NodeDescriptor<Object> {
@Override
public void setHighlighted(Object node, boolean selected, boolean isAlignmentMode) {}
@Override
public Bitmap getSnapshot(Object node, boolean includeChildren) throws Exception {
return null;
}
@Override
public void hitTest(Object node, Touch touch) {
touch.finish();

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.app.Dialog;
import android.graphics.Bitmap;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import com.facebook.flipper.core.FlipperDynamic;
@@ -84,6 +85,15 @@ public class SupportDialogFragmentDescriptor extends NodeDescriptor<DialogFragme
}
}
@Override
public Bitmap getSnapshot(DialogFragment node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
if (node.getDialog() != null) {
return descriptor.getSnapshot(node.getDialog(), includeChildren);
}
return null;
}
@Override
public void hitTest(DialogFragment node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -7,6 +7,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import androidx.fragment.app.Fragment;
@@ -98,6 +99,16 @@ public class SupportFragmentDescriptor extends NodeDescriptor<Fragment> {
descriptor.setHighlighted(node.getView(), selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(Fragment node, boolean includeChildren) throws Exception {
if (node.getView() == null) {
return null;
}
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node.getView(), includeChildren);
}
@Override
public void hitTest(Fragment node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -11,6 +11,7 @@ import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Color;
import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Number;
import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Text;
import android.graphics.Bitmap;
import android.view.View;
import android.widget.TextView;
import com.facebook.flipper.core.FlipperDynamic;
@@ -149,6 +150,12 @@ public class TextViewDescriptor extends NodeDescriptor<TextView> {
descriptor.setHighlighted(node, selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(TextView node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node, includeChildren);
}
@Override
public void hitTest(TextView node, Touch touch) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);

View File

@@ -11,6 +11,8 @@ import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Color;
import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Enum;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -513,6 +515,17 @@ public class ViewDescriptor extends NodeDescriptor<View> {
HighlightedOverlay.setHighlighted(targetView, margin, padding, contentBounds, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(View node, boolean includeChildren) throws Exception {
if (node.getWidth() == 0 || node.getHeight() == 0) {
return null;
}
Bitmap bitmap = Bitmap.createBitmap(node.getWidth(), node.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
node.draw(c);
return bitmap;
}
@Override
public void hitTest(View node, Touch touch) {
touch.finish();

View File

@@ -12,6 +12,7 @@ import static androidx.core.view.ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS;
import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Boolean;
import static com.facebook.flipper.plugins.inspector.InspectorValue.Type.Enum;
import android.graphics.Bitmap;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
@@ -260,6 +261,12 @@ public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
descriptor.setHighlighted(node, selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(ViewGroup node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node, true);
}
private void runHitTest(ViewGroup node, Touch touch) {
boolean finish = true;
for (int i = node.getChildCount() - 1; i >= 0; i--) {

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.inspector.descriptors;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
@@ -130,6 +131,12 @@ public class WindowDescriptor extends NodeDescriptor<Window> {
descriptor.setHighlighted(node.getDecorView(), selected, isAlignmentMode);
}
@Override
public Bitmap getSnapshot(Window node, boolean includeChildren) throws Exception {
final NodeDescriptor descriptor = descriptorForClass(View.class);
return descriptor.getSnapshot(node.getDecorView(), true);
}
@Override
public void hitTest(Window node, Touch touch) {
touch.continueWithOffset(0, 0, 0);

View File

@@ -10,6 +10,7 @@ package com.facebook.flipper.plugins.inspector;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import android.graphics.Bitmap;
import com.facebook.flipper.core.FlipperConnection;
import com.facebook.flipper.core.FlipperDynamic;
import com.facebook.flipper.core.FlipperObject;
@@ -72,6 +73,11 @@ public class DescriptorMappingTest {
@Override
public void setHighlighted(T t, boolean b, boolean b1) throws Exception {}
@Override
public Bitmap getSnapshot(T t, boolean b) throws Exception {
return null;
}
@Override
public void hitTest(T node, Touch touch) {}

View File

@@ -14,6 +14,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
import android.app.Application;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
@@ -415,6 +416,11 @@ public class InspectorFlipperPluginTest {
testNode.highlighted = b;
}
@Override
public Bitmap getSnapshot(TestNode testNode, boolean includeChildren) {
return null;
}
@Override
public void hitTest(TestNode node, Touch touch) {
boolean finish = true;