// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.resources.dynamics;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
/**
* ViewResourceInflater is a utility class that facilitates using an Android View as a dynamic
* resource, which can be later used as a compositor layer. This class assumes that the View
* is defined declaratively, using a XML Layout file, and that the View that is going to be
* inflated is the single top-level View of the layout (its root).
*
* By default, the View is inflated without being attached to the hierarchy, which allows
* subclasses to read/modify the View "offscreen", via the method {@link #onFinishInflate()}.
* When a new snapshot of the View is required, which is triggered when the method
* {@link #invalidate()} is called, the View is drawn and automatically detached from the
* hierarchy after the snapshot has been captured. View drawing and capturing is done async,
* so when calling {@link #invalidate()} the caller may want to wait until onCaptureEnd has been
* called to make sure a new snapshot has been captured in cases where rendering an older snapshot
* for a frame or two would be problematic. This can be done by overriding onCaptureEnd.
*
* There's also an option to not attach to the hierarchy at all, by overriding the method
* {@link #shouldAttachView()} and making it return false (the default is true). In this case
* the changes to the View will always be "offscreen". By default an unspecified value of
* {@link View.MeasureSpec} will be used to determine the width and height of the View.
* It's possible to specify custom size constraints by overriding the methods
* {@link #getWidthMeasureSpec()} and {@link #getHeightMeasureSpec()}.
*/
public class ViewResourceInflater {
/** The id of the XML Layout that describes the View. */
private int mLayoutId;
/** The id of the View being inflated, which must be the root of the given Layout. */
private int mViewId;
/** The Context used to inflate the View. */
private Context mContext;
/** The ViewGroup container used to inflate the View. */
private ViewGroup mContainer;
/** The DynamicResourceLoader used to manage resources generated dynamically. */
private DynamicResourceLoader mResourceLoader;
/** The ViewResourceAdapter used to capture snapshots of the View. */
private ViewResourceAdapter mResourceAdapter;
/** The inflated View. */
private View mView;
/** Whether the View needs a layout update. */
private boolean mNeedsLayoutUpdate;
/** Whether the View is invalided. */
private boolean mIsInvalidated;
/** Whether the View is attached to the hierarchy. */
private boolean mIsAttached;
/** The ViewInflaterOnDrawListener used to track changes in the View when attached. */
private ViewInflaterOnDrawListener mOnDrawListener;
/** The invalid ID. */
private static final int INVALID_ID = -1;
/**
* @param layoutId The XML Layout that declares the View.
* @param viewId The id of the root View of the Layout.
* @param context The Android Context used to inflate the View.
* @param container The container View used to inflate the View.
* @param resourceLoader The resource loader that will handle the snapshot capturing.
*/
public ViewResourceInflater(
int layoutId,
int viewId,
Context context,
ViewGroup container,
DynamicResourceLoader resourceLoader) {
mLayoutId = layoutId;
mViewId = viewId;
mContext = context;
mContainer = container;
mResourceLoader = resourceLoader;
}
/** Inflate the layout. */
public void inflate() {
if (mView != null) return;
// Inflate the View without attaching to hierarchy (attachToRoot param is false).
mView = LayoutInflater.from(mContext).inflate(mLayoutId, mContainer, false);
// Make sure the View we just inflated is the right one.
assert mView.getId() == mViewId;
// Allow subclasses to access/modify the View before it's attached
// to the hierarchy (if allowed) or snapshots are captured.
onFinishInflate();
registerResource();
mNeedsLayoutUpdate = true;
}
/** Invalidate the inflated View, causing a snapshot of the View to be captured. */
public void invalidate() {
invalidate(false);
}
/**
* Invalidate the inflated View, causing a snapshot of the View to be captured.
*
* @param didViewSizeChange Whether the View's size has changed..
*/
public void invalidate(boolean didViewSizeChange) {
// View must be inflated at this point. If it's not, do it now.
if (mView == null) {
inflate();
}
mIsInvalidated = true;
if (!mIsAttached && shouldAttachView()) {
// TODO(pedrosimonetti): investigate if complex views can be rendered offline.
// NOTE(pedrosimonetti): it seems that complex views don't get rendered
// properly if not attached to the hierarchy. The problem seem to be related
// to the use of the property "layout_gravity: end", possibly in combination
// of other things like elastic views (layout_weight: 1) and/or fading edges.
attachView();
}
if (mIsAttached) {
// Update the View's layout params, which will trigger a re-layout.
if (didViewSizeChange || mNeedsLayoutUpdate) {
updateLayoutParams();
}
} else {
// When the View is not attached, we need to manually layout the View and
// invalidate the resource in order to capture a new snapshot.
layout();
invalidateResource();
}
mNeedsLayoutUpdate = false;
}
/** Destroy the instance. */
public void destroy() {
if (mView == null) return;
unregisterResource();
// Ensure the view isn't detached in the middle of a layout pass by posting. See
// https://crbug.com/1234713 for details.
mView.post(
() -> {
detachView();
mView = null;
mContainer = null;
});
mLayoutId = INVALID_ID;
mViewId = INVALID_ID;
mContext = null;
mResourceLoader = null;
}
/**
* @return The measured width of the inflated View.
*/
public int getMeasuredWidth() {
// View must be inflated at this point.
assert mView != null;
return mView.getMeasuredWidth();
}
/**
* @return The measured height of the inflated View.
*/
public int getMeasuredHeight() {
// View must be inflated at this point.
assert mView != null;
return mView.getMeasuredHeight();
}
/**
* @return The id of View, which is used as an identifier for the resource loader.
*/
public int getViewId() {
return mViewId;
}
/**
* The callback called after inflating the View, allowing subclasses to access/modify
* the View before it's attached to the hierarchy (if allowed) or snapshots are captured.
*/
protected void onFinishInflate() {}
/**
* NOTE(pedrosimonetti): Complex views don't fully work when not attached to the hierarchy.
* @return Whether the View should be attached to the hierarchy after being inflated.
* Subclasses should override this method to change the default behavior.
*/
protected boolean shouldAttachView() {
return true;
}
/**
* @return Whether the View should be detached from the hierarchy after being captured.
* Subclasses should override this method to change the default behavior.
*/
protected boolean shouldDetachViewAfterCapturing() {
return true;
}
/**
* @return The MeasureSpec used for calculating the width of the offscreen View.
* Subclasses should override this method to specify measurements.
* By default, this method returns an unspecified MeasureSpec.
*/
protected int getWidthMeasureSpec() {
return getUnspecifiedMeasureSpec();
}
/**
* @return The MeasureSpec used for calculating the height of the offscreen View.
* Subclasses should override this method to specify measurements.
* By default, this method returns an unspecified MeasureSpec.
*/
protected int getHeightMeasureSpec() {
return getUnspecifiedMeasureSpec();
}
/** Lays out the View. */
protected void layout() {
mView.measure(getWidthMeasureSpec(), getHeightMeasureSpec());
mView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
/**
* @return The View resource.
*/
protected View getView() {
return mView;
}
/**
* @return The Context used to inflate the View.
*/
protected Context getContext() {
return mContext;
}
/** Attach the View to the hierarchy. */
private void attachView() {
if (!mIsAttached) {
assert mView.getParent() == null;
mContainer.addView(mView);
mIsAttached = true;
if (mOnDrawListener == null) {
// Add a draw listener. For now on, changes in the View will cause a
// new snapshot to be captured, if the ViewResourceInflater was invalidated.
mOnDrawListener = new ViewInflaterOnDrawListener();
mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
}
}
}
/** Detach the View from the hierarchy. */
private void detachView() {
if (mIsAttached) {
if (mOnDrawListener != null) {
mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
mOnDrawListener = null;
}
assert mView.getParent() != null;
mContainer.removeView(mView);
mIsAttached = false;
}
}
/** Lay out the view according to the current width and height measure specs. */
private void updateLayoutParams() {
// View must be inflated at this point.
assert mView != null;
// Update LayoutParams according to the current measure spec.
final int widthMeasureSpec = getWidthMeasureSpec();
int width = ViewGroup.LayoutParams.WRAP_CONTENT;
if (View.MeasureSpec.getMode(widthMeasureSpec) == View.MeasureSpec.EXACTLY) {
width = View.MeasureSpec.getSize(widthMeasureSpec);
}
final int heightMeasureSpec = getHeightMeasureSpec();
int height = ViewGroup.LayoutParams.WRAP_CONTENT;
if (View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.EXACTLY) {
height = View.MeasureSpec.getSize(heightMeasureSpec);
}
ViewGroup.LayoutParams params = mView.getLayoutParams();
params.width = width;
params.height = height;
mView.setLayoutParams(params);
}
/**
* @return An unspecified MeasureSpec value.
*/
private int getUnspecifiedMeasureSpec() {
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
/** Register the resource and creates an adapter for it. */
private void registerResource() {
if (mResourceAdapter == null) {
mResourceAdapter = new ViewInflaterAdapter(mView.findViewById(mViewId));
}
if (mResourceLoader != null) {
mResourceLoader.registerResource(mViewId, mResourceAdapter);
}
}
/** Unregister the resource and destroys the adapter. */
private void unregisterResource() {
if (mResourceLoader != null) {
mResourceLoader.unregisterResource(mViewId);
}
mResourceAdapter = null;
}
/** Invalidate the resource, which will cause a new snapshot to be captured. */
private void invalidateResource() {
if (mIsInvalidated && mView != null && mResourceAdapter != null) {
mIsInvalidated = false;
mResourceAdapter.invalidate(null);
}
}
/** A custom {@link ViewResourceAdapter} that calls the method {@link #onCaptureEnd()}. */
private class ViewInflaterAdapter extends ViewResourceAdapter {
public ViewInflaterAdapter(View view) {
super(view);
}
@Override
public void onCaptureEnd() {
ViewResourceInflater.this.onCaptureEnd();
}
}
/** Called when a snapshot is captured. */
protected void onCaptureEnd() {
if (shouldDetachViewAfterCapturing()) {
detachView();
}
}
/**
* A custom {@link ViewTreeObserver.OnDrawListener} that invalidates the resource when
* the view is drawn.
*/
private class ViewInflaterOnDrawListener implements ViewTreeObserver.OnDrawListener {
@Override
public void onDraw() {
invalidateResource();
}
}
}