blob: 4087eda944e0509ab9ea7fe36391434f33e0ac31 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.view;
import android.graphics.HardwareRenderer;
import android.graphics.Matrix;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.RenderNode;
import android.os.Handler;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.view.ScrollCaptureCallback;
import android.view.ScrollCaptureSession;
import android.view.Surface;
import android.view.View;
import java.lang.ref.WeakReference;
import java.util.function.Consumer;
/**
* Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling
* containers.
* <p>
* To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper}
* to the callback.
*
* @param <V> the specific View subclass handled
* @hide
*/
public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback {
private final WeakReference<V> mWeakView;
private final ScrollCaptureViewHelper<V> mViewHelper;
private ViewRenderer mRenderer;
private Handler mUiHandler;
private boolean mStarted;
private boolean mEnded;
static <V extends View> ScrollCaptureCallback createCallback(V view,
ScrollCaptureViewHelper<V> impl) {
return new ScrollCaptureViewSupport<>(view, impl);
}
ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) {
mWeakView = new WeakReference<>(containingView);
mRenderer = new ViewRenderer();
mUiHandler = containingView.getHandler();
mViewHelper = viewHelper;
}
// Base implementation of ScrollCaptureCallback
@Override
public final void onScrollCaptureSearch(Consumer<Rect> onReady) {
V view = mWeakView.get();
mStarted = false;
mEnded = false;
if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) {
onReady.accept(mViewHelper.onComputeScrollBounds(view));
return;
}
onReady.accept(null);
}
@Override
public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
V view = mWeakView.get();
mEnded = false;
mStarted = true;
// Note: If somehow the view is already gone or detached, the first call to
// {@code onScrollCaptureImageRequest} will return an error and request the session to
// end.
if (view != null && view.isVisibleToUser()) {
mRenderer.setSurface(session.getSurface());
mViewHelper.onPrepareForStart(view, session.getScrollBounds());
}
onReady.run();
}
@Override
public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) {
V view = mWeakView.get();
if (view == null || !view.isVisibleToUser()) {
// Signal to the controller that we have a problem and can't continue.
session.notifyBufferSent(0, null);
return;
}
Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(),
requestRect);
mRenderer.renderFrame(view, captureArea, mUiHandler,
() -> session.notifyBufferSent(0, captureArea));
}
@Override
public final void onScrollCaptureEnd(Runnable onReady) {
V view = mWeakView.get();
if (mStarted && !mEnded) {
mViewHelper.onPrepareForEnd(view);
/* empty */
mEnded = true;
mRenderer.trimMemory();
mRenderer.setSurface(null);
}
onReady.run();
}
/**
* Internal helper class which assists in rendering sections of the view hierarchy relative to a
* given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch
* image requests.
*/
static final class ViewRenderer {
// alpha, "reasonable default" from Javadoc
private static final float AMBIENT_SHADOW_ALPHA = 0.039f;
private static final float SPOT_SHADOW_ALPHA = 0.039f;
// Default values:
// lightX = (screen.width() / 2) - windowLeft
// lightY = 0 - windowTop
// lightZ = 600dp
// lightRadius = 800dp
private static final float LIGHT_Z_DP = 400;
private static final float LIGHT_RADIUS_DP = 800;
private static final String TAG = "ViewRenderer";
private HardwareRenderer mRenderer;
private RenderNode mRootRenderNode;
private final RectF mTempRectF = new RectF();
private final Rect mSourceRect = new Rect();
private final Rect mTempRect = new Rect();
private final Matrix mTempMatrix = new Matrix();
private final int[] mTempLocation = new int[2];
private long mLastRenderedSourceDrawingId = -1;
ViewRenderer() {
mRenderer = new HardwareRenderer();
mRootRenderNode = new RenderNode("ScrollCaptureRoot");
mRenderer.setContentRoot(mRootRenderNode);
// TODO: Figure out a way to flip this on when we are sure the source window is opaque
mRenderer.setOpaque(false);
}
public void setSurface(Surface surface) {
mRenderer.setSurface(surface);
}
/**
* Cache invalidation check. If the source view is the same as the previous call (which is
* mostly always the case, then we can skip setting up lighting on each call (for now)
*
* @return true if the view changed, false if the view was previously rendered by this class
*/
private boolean updateForView(View source) {
if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) {
return false;
}
mLastRenderedSourceDrawingId = source.getUniqueDrawingId();
return true;
}
// TODO: may need to adjust lightY based on the virtual canvas position to get
// consistent shadow positions across the whole capture. Or possibly just
// pull lightZ way back to make shadows more uniform.
private void setupLighting(View mSource) {
mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId();
DisplayMetrics metrics = mSource.getResources().getDisplayMetrics();
mSource.getLocationOnScreen(mTempLocation);
final float lightX = metrics.widthPixels / 2f - mTempLocation[0];
final float lightY = metrics.heightPixels - mTempLocation[1];
final int lightZ = (int) (LIGHT_Z_DP * metrics.density);
final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density);
// Enable shadows for elevation/Z
mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA);
}
public void renderFrame(View localReference, Rect sourceRect, Handler handler,
Runnable onFrameCommitted) {
if (updateForView(localReference)) {
setupLighting(localReference);
}
buildRootDisplayList(localReference, sourceRect);
HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
request.setVsyncTime(SystemClock.elapsedRealtimeNanos());
request.setFrameCommitCallback(handler::post, onFrameCommitted);
request.setWaitForPresent(true);
request.syncAndDraw();
}
public void trimMemory() {
mRenderer.clearContent();
}
public void destroy() {
mRenderer.destroy();
}
private void transformToRoot(View local, Rect localRect, Rect outRect) {
mTempMatrix.reset();
local.transformMatrixToGlobal(mTempMatrix);
mTempRectF.set(localRect);
mTempMatrix.mapRect(mTempRectF);
mTempRectF.round(outRect);
}
private void buildRootDisplayList(View source, Rect localSourceRect) {
final View captureSource = source.getRootView();
transformToRoot(source, localSourceRect, mTempRect);
mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height());
RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(),
mTempRect.height());
canvas.translate(-mTempRect.left, -mTempRect.top);
canvas.drawRenderNode(captureSource.updateDisplayListIfDirty());
mRootRenderNode.endRecording();
}
}
}