blob: ebede4650d4efc15f7cf26208fa7e3802965a346 [file] [log] [blame]
/*
* Copyright (C) 2013 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.tools.idea.rendering;
import com.android.ide.common.rendering.HardwareConfigHelper;
import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.rendering.RenderParamsFlags;
import com.android.ide.common.rendering.RenderSecurityManager;
import com.android.ide.common.rendering.api.*;
import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.resources.configuration.LayoutDirectionQualifier;
import com.android.resources.LayoutDirection;
import com.android.resources.ResourceFolderType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Device;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.model.ManifestInfo.ActivityAttributes;
import com.android.tools.idea.rendering.multi.CompatibilityRenderTarget;
import com.android.tools.idea.rendering.multi.RenderPreviewMode;
import com.google.common.collect.Maps;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Element;
import org.xmlpull.v1.XmlPullParserException;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.intellij.lang.annotation.HighlightSeverity.ERROR;
/**
* The {@link RenderTask} provides rendering and layout information for
* Android layouts. This is a wrapper around the layout library.
*/
public class RenderTask implements IImageFactory {
@NotNull
private final RenderService myRenderService;
@Nullable
private XmlFile myPsiFile;
@NotNull
private final RenderLogger myLogger;
@NotNull
private final LayoutlibCallbackImpl myLayoutlibCallback;
private final AndroidVersion myMinSdkVersion;
private final AndroidVersion myTargetSdkVersion;
@NotNull
private final LayoutLibrary myLayoutLib;
@NotNull
private final HardwareConfigHelper myHardwareConfigHelper;
@Nullable
private IncludeReference myIncludedWithin;
@NotNull
private RenderingMode myRenderingMode = RenderingMode.NORMAL;
@Nullable
private Integer myOverrideBgColor;
private boolean myShowDecorations = true;
@NotNull
private final Configuration myConfiguration;
@NotNull
private final AssetRepositoryImpl myAssetRepository;
private long myTimeout;
@Nullable
private Set<XmlTag> myExpandNodes;
@Nullable
private RenderContext myRenderContext;
@NotNull
private final Locale myLocale;
private final Object myCredential;
private ResourceFolderType myFolderType;
private boolean myProvideCookiesForIncludedViews = false;
/**
* Don't create this task directly; obtain via {@link com.android.tools.idea.rendering.RenderService}
*/
RenderTask(@NotNull RenderService renderService,
@NotNull Configuration configuration,
@NotNull RenderLogger logger,
@NotNull LayoutLibrary layoutLib,
@NotNull Device device,
@NotNull Object credential) {
myRenderService = renderService;
myLogger = logger;
myCredential = credential;
myConfiguration = configuration;
AndroidFacet facet = renderService.getFacet();
Module module = facet.getModule();
myAssetRepository = new AssetRepositoryImpl(facet);
myHardwareConfigHelper = new HardwareConfigHelper(device);
myHardwareConfigHelper.setOrientation(configuration.getFullConfig().getScreenOrientationQualifier().getValue());
myLayoutLib = layoutLib;
AppResourceRepository appResources = AppResourceRepository.getAppResources(facet, true);
ActionBarHandler actionBarHandler = new ActionBarHandler(this, myCredential);
myLayoutlibCallback = new LayoutlibCallbackImpl(this, myLayoutLib, appResources, module, facet, myLogger, myCredential, actionBarHandler);
myLayoutlibCallback.loadAndParseRClass();
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(facet);
myMinSdkVersion = moduleInfo.getMinSdkVersion();
myTargetSdkVersion = moduleInfo.getTargetSdkVersion();
myLocale = configuration.getLocale();
}
public void setPsiFile(final @NotNull PsiFile psiFile) {
if (!(psiFile instanceof XmlFile)) {
throw new IllegalArgumentException("Can only render XML files: " + psiFile.getClass().getName());
}
myPsiFile = (XmlFile)psiFile;
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
myFolderType = ResourceHelper.getFolderType(myPsiFile);
}
});
}
@Nullable
public AndroidPlatform getPlatform() {
return myRenderService.getPlatform();
}
/**
* Returns the {@link ResourceResolver} for this editor
*
* @return the resolver used to resolve resources for the current configuration of
* this editor, or null
*/
@Nullable
public ResourceResolver getResourceResolver() {
return myConfiguration.getResourceResolver();
}
@NotNull
public Configuration getConfiguration() {
return myConfiguration;
}
@Nullable
public ResourceFolderType getFolderType() {
return myFolderType;
}
public void setFolderType(@NotNull ResourceFolderType folderType) {
myFolderType = folderType;
}
@NotNull
public Module getModule() {
return myRenderService.getModule();
}
@NotNull
public RenderLogger getLogger() {
return myLogger;
}
@Nullable
public Set<XmlTag> getExpandNodes() {
return myExpandNodes;
}
@NotNull
public HardwareConfigHelper getHardwareConfigHelper() {
return myHardwareConfigHelper;
}
public boolean getShowDecorations() {
return myShowDecorations;
}
public void dispose() {
myLayoutlibCallback.setLogger(null);
myLayoutlibCallback.setResourceResolver(null);
}
/**
* Overrides the width and height to be used during rendering (which might be adjusted if
* the {@link #setRenderingMode(com.android.ide.common.rendering.api.SessionParams.RenderingMode)} is
* {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#FULL_EXPAND}.
* <p/>
* A value of -1 will make the rendering use the normal width and height coming from the
* {@link Configuration#getDevice()} object.
*
* @param overrideRenderWidth the width in pixels of the layout to be rendered
* @param overrideRenderHeight the height in pixels of the layout to be rendered
* @return this (such that chains of setters can be stringed together)
*/
public RenderTask setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) {
myHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight);
return this;
}
/**
* Sets the max width and height to be used during rendering (which might be adjusted if
* the {@link #setRenderingMode(com.android.ide.common.rendering.api.SessionParams.RenderingMode)} is
* {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#FULL_EXPAND}.
* <p/>
* A value of -1 will make the rendering use the normal width and height coming from the
* {@link Configuration#getDevice()} object.
*
* @param maxRenderWidth the max width in pixels of the layout to be rendered
* @param maxRenderHeight the max height in pixels of the layout to be rendered
* @return this (such that chains of setters can be stringed together)
*/
public RenderTask setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) {
myHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight);
return this;
}
/**
* Sets the {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode} to be used during rendering. If none is specified,
* the default is {@link com.android.ide.common.rendering.api.SessionParams.RenderingMode#NORMAL}.
*
* @param renderingMode the rendering mode to be used
* @return this (such that chains of setters can be stringed together)
*/
public RenderTask setRenderingMode(@NotNull RenderingMode renderingMode) {
myRenderingMode = renderingMode;
return this;
}
/** Returns the {@link RenderingMode} to be used */
@NotNull
public RenderingMode getRenderingMode() {
return myRenderingMode;
}
public RenderTask setTimeout(long timeout) {
myTimeout = timeout;
return this;
}
/**
* Sets the overriding background color to be used, if any. The color should be a
* bitmask of AARRGGBB. The default is null.
*
* @param overrideBgColor the overriding background color to be used in the rendering,
* in the form of a AARRGGBB bitmask, or null to use no custom background.
* @return this (such that chains of setters can be stringed together)
*/
@NotNull
public RenderTask setOverrideBgColor(@Nullable Integer overrideBgColor) {
myOverrideBgColor = overrideBgColor;
return this;
}
/**
* Sets whether the rendering should include decorations such as a system bar, an
* application bar etc depending on the SDK target and theme. The default is true.
*
* @param showDecorations true if the rendering should include system bars etc.
* @return this (such that chains of setters can be stringed together)
*/
public RenderTask setDecorations(boolean showDecorations) {
myShowDecorations = showDecorations;
return this;
}
/**
* Gets the context for the usage of this {@link RenderTask}, which can
* control for example how {@code <fragment/>} tags are processed when missing
* preview data
*/
@Nullable
public RenderContext getRenderContext() {
return myRenderContext;
}
/**
* Sets the context for the usage of this {@link RenderTask}, which can
* control for example how {@code <fragment/>} tags are processed when missing
* preview data
*
* @param renderContext the render context
* @return this, for constructor chaining
*/
@Nullable
public RenderTask setRenderContext(@Nullable RenderContext renderContext) {
myRenderContext = renderContext;
return this;
}
/**
* Sets the nodes to expand during rendering. These will be padded with approximately
* 20 pixels. The default is null.
*
* @param nodesToExpand the nodes to be expanded
* @return this (such that chains of setters can be stringed together)
*/
@NotNull
public RenderTask setNodesToExpand(@Nullable Set<XmlTag> nodesToExpand) {
myExpandNodes = nodesToExpand;
return this;
}
/**
* Sets the {@link IncludeReference} to an outer layout that this layout should be rendered
* within. The outer layout <b>must</b> contain an include tag which points to this
* layout. If not set explicitly to {@link IncludeReference#NONE}, it will look at the
* root tag of the rendered layout and if {@link IncludeReference#ATTR_RENDER_IN} has
* been set it will use that layout.
*
* @param includedWithin a reference to an outer layout to render this layout within
* @return this (such that chains of setters can be stringed together)
*/
@NotNull
public RenderTask setIncludedWithin(@Nullable IncludeReference includedWithin) {
myIncludedWithin = includedWithin;
return this;
}
/**
* Returns the layout to be included
*/
@NotNull
public IncludeReference getIncludedWithin() {
return myIncludedWithin != null ? myIncludedWithin : IncludeReference.NONE;
}
/** Returns whether this parser will provide view cookies for included views. */
public boolean getProvideCookiesForIncludedViews() {
return myProvideCookiesForIncludedViews;
}
/** Sets whether this parser will provide view cookies for included views. */
public void setProvideCookiesForIncludedViews(boolean provideCookiesForIncludedViews) {
myProvideCookiesForIncludedViews = provideCookiesForIncludedViews;
}
/**
* Renders the model and returns the result as a {@link com.android.ide.common.rendering.api.RenderSession}.
*
* @param factory Factory for images which would be used to render layouts to.
* @return the {@link RenderResult resulting from rendering the current model
*/
@Nullable
private RenderResult createRenderSession(@NotNull IImageFactory factory) {
if (myPsiFile == null) {
throw new IllegalStateException("createRenderSession shouldn't be called on RenderTask without PsiFile");
}
ResourceResolver resolver = getResourceResolver();
if (resolver == null) {
// Abort the rendering if the resources are not found.
return null;
}
ILayoutPullParser modelParser = LayoutPullParserFactory.create(this);
if (modelParser == null) {
return null;
}
myLayoutlibCallback.reset();
ILayoutPullParser includingParser = getIncludingLayoutParser(resolver, modelParser);
if (includingParser != null) {
modelParser = includingParser;
}
IAndroidTarget target = myConfiguration.getTarget();
int simulatedPlatform = target instanceof CompatibilityRenderTarget ? target.getVersion().getApiLevel() : 0;
Module module = myRenderService.getModule();
HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig();
final SessionParams params =
new SessionParams(modelParser, myRenderingMode, module /* projectKey */, hardwareConfig, resolver, myLayoutlibCallback,
myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger, simulatedPlatform);
params.setAssetRepository(myAssetRepository);
params.setFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG, AndroidPsiUtils.getRootTagName(myPsiFile));
params.setFlag(RenderParamsFlags.FLAG_KEY_RECYCLER_VIEW_SUPPORT, true);
// Request margin and baseline information.
// TODO: Be smarter about setting this; start without it, and on the first request
// for an extended view info, re-render in the same session, and then set a flag
// which will cause this to create extended view info each time from then on in the
// same session
params.setExtendedViewInfoMode(true);
ManifestInfo manifestInfo = ManifestInfo.get(module);
LayoutDirectionQualifier qualifier = myConfiguration.getFullConfig().getLayoutDirectionQualifier();
if (qualifier != null && qualifier.getValue() == LayoutDirection.RTL) {
params.setRtlSupport(true);
// We don't have a flag to force RTL regardless of locale, so just pick a RTL locale (note that
// this is decoupled from resource lookup)
params.setLocale("ur");
} else {
params.setLocale(myLocale.toLocaleId());
try {
params.setRtlSupport(manifestInfo.isRtlSupported());
} catch (Exception e) {
// ignore.
}
}
// Don't show navigation buttons on older platforms
Device device = myConfiguration.getDevice();
if (!myShowDecorations || HardwareConfigHelper.isWear(device)) {
params.setForceNoDecor();
}
else {
try {
params.setAppLabel(manifestInfo.getApplicationLabel());
params.setAppIcon(manifestInfo.getApplicationIcon());
String activity = myConfiguration.getActivity();
if (activity != null) {
params.setActivityName(activity);
ActivityAttributes attributes = manifestInfo.getActivityAttributes(activity);
if (attributes != null) {
if (attributes.getLabel() != null) {
params.setAppLabel(attributes.getLabel());
}
if (attributes.getIcon() != null) {
params.setAppIcon(attributes.getIcon());
}
}
}
}
catch (Exception e) {
// ignore.
}
}
if (myOverrideBgColor != null) {
params.setOverrideBgColor(myOverrideBgColor.intValue());
} else if (requiresTransparency()) {
params.setOverrideBgColor(0);
}
params.setImageFactory(factory);
if (myTimeout > 0) {
params.setTimeout(myTimeout);
}
try {
myLayoutlibCallback.setLogger(myLogger);
myLayoutlibCallback.setResourceResolver(resolver);
RenderResult result = ApplicationManager.getApplication().runReadAction(new Computable<RenderResult>() {
@NotNull
@Override
public RenderResult compute() {
Module module = myRenderService.getModule();
RenderSecurityManager securityManager = RenderSecurityManagerFactory.create(module, getPlatform());
securityManager.setActive(true, myCredential);
try {
int retries = 0;
RenderSession session = null;
while (retries < 10) {
session = myLayoutLib.createSession(params);
Result result = session.getResult();
if (result.getStatus() != Result.Status.ERROR_TIMEOUT) {
// Sometimes happens at startup; treat it as a timeout; typically a retry fixes it
if (!result.isSuccess() && "The main Looper has already been prepared.".equals(result.getErrorMessage())) {
retries++;
continue;
}
break;
}
retries++;
}
return new RenderResult(RenderTask.this, session, myPsiFile, myLogger);
}
finally {
securityManager.dispose(myCredential);
}
}
});
addDiagnostics(result.getSession());
result.setIncludedWithin(myIncludedWithin);
return result;
}
catch (RuntimeException t) {
// Exceptions from the bridge
myLogger.error(null, t.getLocalizedMessage(), t, null);
throw t;
}
}
@Nullable
private ILayoutPullParser getIncludingLayoutParser(ResourceResolver resolver, ILayoutPullParser modelParser) {
if (myPsiFile == null) {
throw new IllegalStateException("getIncludingLayoutParser shouldn't be called on RenderTask without PsiFile");
}
// Code to support editing included layout
if (myIncludedWithin == null) {
String layout = IncludeReference.getIncludingLayout(myPsiFile);
Module module = myRenderService.getModule();
myIncludedWithin = layout != null ? IncludeReference.get(module, myPsiFile, resolver) : IncludeReference.NONE;
}
if (myIncludedWithin != IncludeReference.NONE) {
assert Comparing.equal(myIncludedWithin.getToFile(), myPsiFile.getVirtualFile());
// TODO: Validate that we're really including the same layout here!
//ResourceValue contextLayout = resolver.findResValue(myIncludedWithin.getFromResourceUrl(), false /* forceFrameworkOnly*/);
//if (contextLayout != null) {
// File layoutFile = new File(contextLayout.getValue());
// if (layoutFile.isFile()) {
//
VirtualFile layoutVirtualFile = myIncludedWithin.getFromFile();
try {
// Get the name of the layout actually being edited, without the extension
// as it's what IXmlPullParser.getParser(String) will receive.
String queryLayoutName = ResourceHelper.getResourceName(myPsiFile);
myLayoutlibCallback.setLayoutParser(queryLayoutName, modelParser);
// Attempt to read from PSI
ILayoutPullParser topParser;
topParser = null;
PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(myRenderService.getProject(), layoutVirtualFile);
if (psiFile instanceof XmlFile) {
LayoutPsiPullParser parser = LayoutPsiPullParser.create((XmlFile)psiFile, myLogger);
// For included layouts, we don't normally see view cookies; we want the leaf to point back to the include tag
parser.setProvideViewCookies(myProvideCookiesForIncludedViews);
topParser = parser;
}
if (topParser == null) {
topParser = LayoutFilePullParser.create(myLayoutlibCallback, myIncludedWithin.getFromPath());
}
return topParser;
}
catch (IOException e) {
myLogger.error(null, String.format("Could not read layout file %1$s", myIncludedWithin.getFromPath()), e);
}
catch (XmlPullParserException e) {
myLogger.error(null, String.format("XML parsing error: %1$s", e.getMessage()), e.getDetail() != null ? e.getDetail() : e);
}
}
return null;
}
@Nullable
public RenderResult render(@NotNull final IImageFactory factory) {
// During development only:
//assert !ApplicationManager.getApplication().isReadAccessAllowed() : "Do not hold read lock during render!";
if (myPsiFile == null) {
throw new IllegalStateException("render shouldn't be called on RenderTask without PsiFile");
}
try {
return RenderService.runRenderAction(new Callable<RenderResult>() {
@Override
public RenderResult call() throws Exception {
return createRenderSession(factory);
}
});
}
catch (final Exception e) {
String message = e.getMessage();
if (message == null) {
message = e.toString();
}
myLogger.addMessage(RenderProblem.createPlain(ERROR, message, myRenderService.getProject(), myLogger.getLinkManager(), e));
return new RenderResult(RenderTask.this, null, myPsiFile, myLogger);
}
}
/**
* Run rendering with default IImageFactory implementation provided by RenderTask
*/
@Nullable
public RenderResult render() {
return render(this);
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private void addDiagnostics(@Nullable RenderSession session) {
if (session == null) {
return;
}
Result r = session.getResult();
if (!myLogger.hasProblems() && !r.isSuccess()) {
if (r.getException() != null || r.getErrorMessage() != null) {
myLogger.error(null, r.getErrorMessage(), r.getException(), null);
} else if (r.getStatus() == Result.Status.ERROR_TIMEOUT) {
myLogger.error(null, "Rendering timed out.", null);
} else {
myLogger.error(null, "Unknown render problem: " + r.getStatus(), null);
}
} else if (myIncludedWithin != null && myIncludedWithin != IncludeReference.NONE) {
ILayoutPullParser layoutEmbeddedParser = myLayoutlibCallback.getLayoutEmbeddedParser();
if (layoutEmbeddedParser != null) { // Should have been nulled out if used
myLogger.error(null, String.format("The surrounding layout (%1$s) did not actually include this layout. " +
"Remove tools:" + IncludeReference.ATTR_RENDER_IN + "=... from the root tag.",
myIncludedWithin.getFromResourceUrl()), null);
}
}
}
/**
* Renders the given resource value (which should refer to a drawable) and returns it
* as an image
*
* @param drawableResourceValue the drawable resource value to be rendered, or null
* @return the image, or null if something went wrong
*/
@Nullable
public BufferedImage renderDrawable(ResourceValue drawableResourceValue) {
if (drawableResourceValue == null) {
return null;
}
HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig();
Module module = myRenderService.getModule();
final DrawableParams params =
new DrawableParams(drawableResourceValue, module, hardwareConfig, getResourceResolver(), myLayoutlibCallback,
myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger);
params.setForceNoDecor();
params.setAssetRepository(myAssetRepository);
try {
Result result = RenderService.runRenderAction(new Callable<Result>() {
@Override
public Result call() throws Exception {
return myLayoutLib.renderDrawable(params);
}
});
if (result != null && result.isSuccess()) {
Object data = result.getData();
if (data instanceof BufferedImage) {
return (BufferedImage)data;
}
}
}
catch (final Exception e) {
// ignore
}
return null;
}
/**
* Renders the given resource value (which should refer to a drawable) and returns it
* as an image
*
* @param drawableResourceValue the drawable resource value to be rendered, or null
* @return the image, or null if something went wrong
*/
@NotNull
@SuppressWarnings("unchecked")
public List<BufferedImage> renderDrawableAllStates(ResourceValue drawableResourceValue) {
if (drawableResourceValue == null) {
return Collections.emptyList();
}
HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig();
Module module = myRenderService.getModule();
final DrawableParams params =
new DrawableParams(drawableResourceValue, module, hardwareConfig, getResourceResolver(), myLayoutlibCallback,
myMinSdkVersion.getApiLevel(), myTargetSdkVersion.getApiLevel(), myLogger);
params.setForceNoDecor();
params.setAssetRepository(myAssetRepository);
final boolean supportsMultipleStates = myLayoutLib.supports(Features.RENDER_ALL_DRAWABLE_STATES);
if (supportsMultipleStates) {
params.setFlag(RenderParamsFlags.FLAG_KEY_RENDER_ALL_DRAWABLE_STATES, Boolean.TRUE);
}
try {
Result result = RenderService.runRenderAction(new Callable<Result>() {
@Override
public Result call() throws Exception {
return myLayoutLib.renderDrawable(params);
}
});
if (result != null && result.isSuccess()) {
Object data = result.getData();
if (supportsMultipleStates && data instanceof List) {
return (List<BufferedImage>)data;
} else if (!supportsMultipleStates && data instanceof BufferedImage) {
return Collections.singletonList((BufferedImage) data);
}
}
}
catch (final Exception e) {
// ignore
}
return Collections.emptyList();
}
@NotNull
public LayoutLibrary getLayoutLib() {
return myLayoutLib;
}
@NotNull
public LayoutlibCallbackImpl getLayoutlibCallback() {
return myLayoutlibCallback;
}
@Nullable
public XmlFile getPsiFile() {
return myPsiFile;
}
public boolean supportsCapability(@MagicConstant(flagsFromClass = Features.class) int capability) {
return myLayoutLib.supports(capability);
}
/** Returns true if this service can render a non-rectangular shape */
public boolean isNonRectangular() {
// Drawable images can have non-rectangular shapes; we need to ensure that we blank out the
// background with full alpha
return getFolderType() == ResourceFolderType.DRAWABLE;
}
/** Returns true if this service requires rendering into a transparent/alpha channel image */
public boolean requiresTransparency() {
// Drawable images can have non-rectangular shapes; we need to ensure that we blank out the
// background with full alpha
return isNonRectangular();
}
// ---- Implements IImageFactory ----
/** TODO: reuse image across subsequent render operations if the size is the same */
@SuppressWarnings("UndesirableClassUsage") // Don't need Retina for layoutlib rendering; will scale down anyway
@Override
public BufferedImage getImage(int width, int height) {
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
/**
* Notifies the render service that it is being used in design mode for this layout.
* For example, that means that when rendering a ScrollView, it should measure the necessary
* vertical space, and size the layout according to the needs rather than the available
* device size.
* <p>
* We don't want to do this when for example offering thumbnail previews of the various
* layouts.
*
* @param file the layout file, if any
*/
public void useDesignMode(@Nullable final PsiFile file) {
if (file == null) {
return;
}
String tagName = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Nullable
@Override
public String compute() {
if (file instanceof XmlFile) {
XmlTag root = ((XmlFile)file).getRootTag();
if (root != null) {
return root.getName();
}
}
return null;
}
});
if (tagName != null) {
// In multi configuration rendering, clip to screen bounds
RenderPreviewMode currentMode = RenderPreviewMode.getCurrent();
if (currentMode != RenderPreviewMode.NONE) {
return;
}
if (SCROLL_VIEW.equals(tagName)) {
setRenderingMode(RenderingMode.V_SCROLL);
setDecorations(false);
} else if (HORIZONTAL_SCROLL_VIEW.equals(tagName)) {
setRenderingMode(RenderingMode.H_SCROLL);
setDecorations(false);
}
}
}
/**
* Measure the children of the given parent element.
*
* @param parent the parent element whose children should be measured
* @return a list of root view infos
*/
@Nullable
public List<ViewInfo> measure(Element parent) {
ILayoutPullParser modelParser = new DomPullParser(parent);
RenderSession session = measure(modelParser);
if (session != null) {
Result result = session.getResult();
if (result != null && result.isSuccess()) {
assert session.getRootViews().size() == 1;
return session.getRootViews();
}
}
return null;
}
/**
* Measure the children of the given parent tag, applying the given filter to the
* pull parser's attribute values.
*
* @param parent the parent tag to measure children for
* @param filter the filter to apply to the attribute values
* @return a map from the children of the parent to new bounds of the children
*/
@Nullable
public Map<XmlTag, ViewInfo> measureChildren(XmlTag parent, final AttributeFilter filter) {
ILayoutPullParser modelParser = LayoutPsiPullParser.create(filter, parent, myLogger);
Map<XmlTag, ViewInfo> map = Maps.newHashMap();
RenderSession session = measure(modelParser);
if (session != null) {
Result result = session.getResult();
if (result != null && result.isSuccess()) {
assert session.getRootViews().size() == 1;
ViewInfo root = session.getRootViews().get(0);
List<ViewInfo> children = root.getChildren();
for (ViewInfo info : children) {
Object cookie = info.getCookie();
if (cookie instanceof XmlTag) {
map.put((XmlTag)cookie, info);
}
}
}
return map;
}
return null;
}
/**
* Measure the given child in context, applying the given filter to the
* pull parser's attribute values.
*
* @param tag the child to measure
* @param filter the filter to apply to the attribute values
* @return a view info, if found
*/
@Nullable
public ViewInfo measureChild(XmlTag tag, final AttributeFilter filter) {
XmlTag parent = tag.getParentTag();
if (parent != null) {
Map<XmlTag, ViewInfo> map = measureChildren(parent, filter);
if (map != null) {
for (Map.Entry<XmlTag, ViewInfo> entry : map.entrySet()) {
if (entry.getKey() == tag) {
return entry.getValue();
}
}
}
}
return null;
}
@Nullable
private RenderSession measure(ILayoutPullParser parser) {
ResourceResolver resolver = getResourceResolver();
if (resolver == null) {
// Abort the rendering if the resources are not found.
return null;
}
myLayoutlibCallback.reset();
HardwareConfig hardwareConfig = myHardwareConfigHelper.getConfig();
Module module = myRenderService.getModule();
final SessionParams params = new SessionParams(
parser,
RenderingMode.FULL_EXPAND,
module /* projectKey */,
hardwareConfig,
resolver,
myLayoutlibCallback,
myMinSdkVersion.getApiLevel(),
myTargetSdkVersion.getApiLevel(),
myLogger);
params.setLayoutOnly();
params.setForceNoDecor();
params.setExtendedViewInfoMode(true);
params.setLocale(myLocale.toLocaleId());
params.setAssetRepository(myAssetRepository);
params.setFlag(RenderParamsFlags.FLAG_KEY_RECYCLER_VIEW_SUPPORT, true);
ManifestInfo manifestInfo = ManifestInfo.get(module);
try {
params.setRtlSupport(manifestInfo.isRtlSupported());
} catch (Exception e) {
// ignore.
}
try {
myLayoutlibCallback.setLogger(myLogger);
myLayoutlibCallback.setResourceResolver(resolver);
return ApplicationManager.getApplication().runReadAction(new Computable<RenderSession>() {
@Nullable
@Override
public RenderSession compute() {
int retries = 0;
while (retries < 10) {
RenderSession session = myLayoutLib.createSession(params);
Result result = session.getResult();
if (result.getStatus() != Result.Status.ERROR_TIMEOUT) {
// Sometimes happens at startup; treat it as a timeout; typically a retry fixes it
if (!result.isSuccess() && "The main Looper has already been prepared.".equals(result.getErrorMessage())) {
retries++;
continue;
}
return session;
}
retries++;
}
return null;
}
});
}
catch (RuntimeException t) {
// Exceptions from the bridge
myLogger.error(null, t.getLocalizedMessage(), t, null);
throw t;
}
}
/**
* The {@link AttributeFilter} allows a client of {@link #measureChildren} to modify the actual
* XML values of the nodes being rendered, for example to force width and height values to
* wrap_content when measuring preferred size.
*/
public interface AttributeFilter {
/**
* Returns the attribute value for the given node and attribute name. This filter
* allows a client to adjust the attribute values that a node presents to the
* layout library.
* <p/>
* Returns "" to unset an attribute. Returns null to return the unfiltered value.
*
* @param node the node for which the attribute value should be returned
* @param namespace the attribute namespace
* @param localName the attribute local name
* @return an override value, or null to return the unfiltered value
*/
@Nullable
String getAttribute(@NotNull XmlTag node, @Nullable String namespace, @NotNull String localName);
}
}