• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.P;
4 import static android.os.Build.VERSION_CODES.Q;
5 
6 import android.content.Context;
7 import android.content.res.Resources;
8 import android.content.res.TypedArray;
9 import android.graphics.Bitmap;
10 import android.graphics.Canvas;
11 import android.graphics.HardwareRenderer;
12 import android.graphics.PixelFormat;
13 import android.graphics.RenderNode;
14 import android.media.Image;
15 import android.media.Image.Plane;
16 import android.media.ImageReader;
17 import android.util.DisplayMetrics;
18 import android.view.Surface;
19 import android.view.View;
20 import android.view.ViewRootImpl;
21 import com.android.internal.R;
22 import com.google.common.base.Preconditions;
23 import java.util.WeakHashMap;
24 import org.robolectric.RuntimeEnvironment;
25 import org.robolectric.annotation.GraphicsMode;
26 import org.robolectric.util.ReflectionHelpers;
27 
28 /**
29  * Helper class to provide hardware rendering-based screenshot to {@link ShadowPixelCopy} and {@link
30  * ShadowUiAutomation}.
31  */
32 public final class HardwareRenderingScreenshot {
33 
34   // It is important to reuse HardwareRenderer objects, and ensure that after a HardwareRenderer is
35   // collected, no associated views in the same View hierarchy will be rendered as well.
36   private static final WeakHashMap<ViewRootImpl, HardwareRenderer> hardwareRenderers =
37       new WeakHashMap<>();
38 
39   static final String PIXEL_COPY_RENDER_MODE = "robolectric.pixelCopyRenderMode";
40 
HardwareRenderingScreenshot()41   private HardwareRenderingScreenshot() {}
42 
43   /**
44    * Indicates whether {@link #takeScreenshot(View, Bitmap)} can run, by validating the API level,
45    * the value of the {@link #PIXEL_COPY_RENDER_MODE} property, and the {@link GraphicsMode}.
46    */
canTakeScreenshot(View view)47   static boolean canTakeScreenshot(View view) {
48     return RuntimeEnvironment.getApiLevel() >= P
49         && "hardware".equalsIgnoreCase(System.getProperty(PIXEL_COPY_RENDER_MODE, "hardware"))
50         && ShadowView.useRealGraphics()
51         && view.canHaveDisplayList();
52   }
53 
54   /**
55    * Generates a bitmap given the current view using hardware accelerated canvases with native
56    * graphics calls. Requires API 28+ (S).
57    *
58    * <p>This code mirrors the behavior of LayoutLib's RenderSessionImpl.renderAndBuildResult(); see
59    * https://googleplex-android.googlesource.com/platform/frameworks/layoutlib/+/refs/heads/master-layoutlib-native/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java#573
60    */
takeScreenshot(View view, Bitmap destBitmap)61   static void takeScreenshot(View view, Bitmap destBitmap) {
62     int width = view.getWidth();
63     int height = view.getHeight();
64 
65     try (ImageReader imageReader =
66         ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)) {
67       ViewRootImpl viewRootImpl = view.getViewRootImpl();
68       Preconditions.checkNotNull(viewRootImpl, "View not attached");
69       Surface surface = imageReader.getSurface();
70 
71       if (RuntimeEnvironment.getApiLevel() >= Q) {
72         // HardwareRenderer is only available on API 29+ (Q).
73         HardwareRenderer renderer =
74             hardwareRenderers.computeIfAbsent(viewRootImpl, k -> new HardwareRenderer());
75         renderer.setSurface(surface);
76         setupRendererShadowProperties(renderer, view);
77         RenderNode node = getRenderNode(view);
78         renderer.setContentRoot(node);
79         renderer.createRenderRequest().syncAndDraw();
80       } else {
81         // Note this API does not set any light source properties, so it will not render
82         // drop shadows.
83         Canvas canvas = surface.lockHardwareCanvas();
84         view.draw(canvas);
85         surface.unlockCanvasAndPost(canvas);
86       }
87       Image nativeImage = imageReader.acquireNextImage();
88       Plane[] planes = nativeImage.getPlanes();
89       destBitmap.copyPixelsFromBuffer(planes[0].getBuffer());
90       surface.release();
91     }
92   }
93 
getRenderNode(View view)94   private static RenderNode getRenderNode(View view) {
95     return ReflectionHelpers.callInstanceMethod(view, "updateDisplayListIfDirty");
96   }
97 
setupRendererShadowProperties(HardwareRenderer renderer, View view)98   private static void setupRendererShadowProperties(HardwareRenderer renderer, View view) {
99     Context context = view.getContext();
100     Resources resources = context.getResources();
101     DisplayMetrics displayMetrics = resources.getDisplayMetrics();
102 
103     // Get the LightSourceGeometry and LightSourceAlpha from resources.
104     // The default values are the ones recommended by the getLightSourceGeometry() and
105     // getLightSourceAlpha() documentation.
106     // This matches LayoutLib's RenderSessionImpl#renderAndBuildResult() implementation.
107 
108     TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
109     float lightX = displayMetrics.widthPixels / 2f;
110     float lightY = a.getDimension(R.styleable.Lighting_lightY, 0f);
111     float lightZ = a.getDimension(R.styleable.Lighting_lightZ, 600f * displayMetrics.density);
112     float lightRadius =
113         a.getDimension(R.styleable.Lighting_lightRadius, 800f * displayMetrics.density);
114     float ambientShadowAlpha = a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0.039f);
115     float spotShadowAlpha = a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0.19f);
116     a.recycle();
117 
118     renderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
119     renderer.setLightSourceAlpha(ambientShadowAlpha, spotShadowAlpha);
120   }
121 }
122