Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
     17 
     18 import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX;
     19 
     20 import com.android.ide.common.api.IClientRulesEngine;
     21 import com.android.ide.common.api.INode;
     22 import com.android.ide.common.api.Rect;
     23 import com.android.ide.common.rendering.LayoutLibrary;
     24 import com.android.ide.common.rendering.api.DrawableParams;
     25 import com.android.ide.common.rendering.api.IImageFactory;
     26 import com.android.ide.common.rendering.api.ILayoutPullParser;
     27 import com.android.ide.common.rendering.api.LayoutLog;
     28 import com.android.ide.common.rendering.api.RenderSession;
     29 import com.android.ide.common.rendering.api.ResourceValue;
     30 import com.android.ide.common.rendering.api.Result;
     31 import com.android.ide.common.rendering.api.SessionParams;
     32 import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
     33 import com.android.ide.common.rendering.api.ViewInfo;
     34 import com.android.ide.common.resources.ResourceResolver;
     35 import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
     36 import com.android.ide.eclipse.adt.AdtPlugin;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
     40 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
     41 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
     42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
     43 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
     44 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
     45 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     46 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     47 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     48 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     49 import com.android.resources.Density;
     50 
     51 import org.eclipse.core.resources.IProject;
     52 import org.xmlpull.v1.XmlPullParser;
     53 import org.xmlpull.v1.XmlPullParserException;
     54 
     55 import java.awt.image.BufferedImage;
     56 import java.io.File;
     57 import java.io.FileInputStream;
     58 import java.io.FileNotFoundException;
     59 import java.util.Collections;
     60 import java.util.HashMap;
     61 import java.util.List;
     62 import java.util.Map;
     63 import java.util.Set;
     64 
     65 /**
     66  * The {@link RenderService} provides rendering and layout information for
     67  * Android layouts. This is a wrapper around the layout library.
     68  */
     69 public class RenderService {
     70     /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
     71     private final GraphicalEditorPart mEditor;
     72 
     73     // The following fields are inferred from the editor and not customizable by the
     74     // client of the render service:
     75 
     76     private final IProject mProject;
     77     private final ProjectCallback mProjectCallback;
     78     private final ResourceResolver mResourceResolver;
     79     private final int mMinSdkVersion;
     80     private final int mTargetSdkVersion;
     81     private final LayoutLibrary mLayoutLib;
     82     private final IImageFactory mImageFactory;
     83     private final Density mDensity;
     84     private final float mXdpi;
     85     private final float mYdpi;
     86     private final ScreenSizeQualifier mScreenSize;
     87 
     88     // The following fields are optional or configurable using the various chained
     89     // setters:
     90 
     91     private UiDocumentNode mModel;
     92     private int mWidth = -1;
     93     private int mHeight = -1;
     94     private boolean mUseExplodeMode;
     95     private Reference mIncludedWithin;
     96     private RenderingMode mRenderingMode = RenderingMode.NORMAL;
     97     private LayoutLog mLogger;
     98     private Integer mOverrideBgColor;
     99     private boolean mShowDecorations = true;
    100     private Set<UiElementNode> mExpandNodes = Collections.<UiElementNode>emptySet();
    101 
    102     /** Use the {@link #create} factory instead */
    103     private RenderService(GraphicalEditorPart editor) {
    104         mEditor = editor;
    105 
    106         mProject = editor.getProject();
    107         LayoutCanvas canvas = editor.getCanvasControl();
    108         mImageFactory = canvas.getImageOverlay();
    109         ConfigurationComposite config = editor.getConfigurationComposite();
    110         mDensity = config.getDensity();
    111         mXdpi = config.getXDpi();
    112         mYdpi = config.getYDpi();
    113         mScreenSize = config.getCurrentConfig().getScreenSizeQualifier();
    114         mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
    115         mResourceResolver = editor.getResourceResolver();
    116         mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
    117         mMinSdkVersion = editor.getMinSdkVersion();
    118         mTargetSdkVersion = editor.getTargetSdkVersion();
    119     }
    120 
    121     /**
    122      * Creates a new {@link RenderService} associated with the given editor.
    123      *
    124      * @param editor the editor to provide configuration data such as the render target
    125      * @return a {@link RenderService} which can perform rendering services
    126      */
    127     public static RenderService create(GraphicalEditorPart editor) {
    128         RenderService renderService = new RenderService(editor);
    129 
    130         return renderService;
    131     }
    132 
    133     /**
    134      * Renders the given model, using this editor's theme and screen settings, and returns
    135      * the result as a {@link RenderSession}.
    136      *
    137      * @param model the model to be rendered, which can be different than the editor's own
    138      *            {@link #getModel()}.
    139      * @param width the width to use for the layout, or -1 to use the width of the screen
    140      *            associated with this editor
    141      * @param height the height to use for the layout, or -1 to use the height of the screen
    142      *            associated with this editor
    143      * @param explodeNodes a set of nodes to explode, or null for none
    144      * @param overrideBgColor If non-null, use the given color as a background to render over
    145      *        rather than the normal background requested by the theme
    146      * @param noDecor If true, don't draw window decorations like the system bar
    147      * @param logger a logger where rendering errors are reported
    148      * @param renderingMode the {@link RenderingMode} to use for rendering
    149      * @return the resulting rendered image wrapped in an {@link RenderSession}
    150      */
    151 
    152     /**
    153      * Sets the {@link LayoutLog} to be used during rendering. If none is specified, a
    154      * silent logger will be used.
    155      *
    156      * @param logger the log to be used
    157      * @return this (such that chains of setters can be stringed together)
    158      */
    159     public RenderService setLog(LayoutLog logger) {
    160         mLogger = logger;
    161         return this;
    162     }
    163 
    164     /**
    165      * Sets the model to be rendered, which can be different than the editor's own
    166      * {@link GraphicalEditorPart#getModel()}.
    167      *
    168      * @param model the model to be rendered
    169      * @return this (such that chains of setters can be stringed together)
    170      */
    171     public RenderService setModel(UiDocumentNode model) {
    172         mModel = model;
    173         return this;
    174     }
    175 
    176     /**
    177      * Sets the width and height to be used during rendering (which might be adjusted if
    178      * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
    179      *
    180      * @param width the width in pixels of the layout to be rendered
    181      * @param height the height in pixels of the layout to be rendered
    182      * @return this (such that chains of setters can be stringed together)
    183      */
    184     public RenderService setSize(int width, int height) {
    185         mWidth = width;
    186         mHeight = height;
    187         return this;
    188     }
    189 
    190     /**
    191      * Sets the {@link RenderingMode} to be used during rendering. If none is specified,
    192      * the default is {@link RenderingMode#NORMAL}.
    193      *
    194      * @param renderingMode the rendering mode to be used
    195      * @return this (such that chains of setters can be stringed together)
    196      */
    197     public RenderService setRenderingMode(RenderingMode renderingMode) {
    198         mRenderingMode = renderingMode;
    199         return this;
    200     }
    201 
    202     /**
    203      * Sets the overriding background color to be used, if any. The color should be a
    204      * bitmask of AARRGGBB. The default is null.
    205      *
    206      * @param overrideBgColor the overriding background color to be used in the rendering,
    207      *            in the form of a AARRGGBB bitmask, or null to use no custom background.
    208      * @return this (such that chains of setters can be stringed together)
    209      */
    210     public RenderService setOverrideBgColor(Integer overrideBgColor) {
    211         mOverrideBgColor = overrideBgColor;
    212         return this;
    213     }
    214 
    215     /**
    216      * Sets whether the rendering should include decorations such as a system bar, an
    217      * application bar etc depending on the SDK target and theme. The default is true.
    218      *
    219      * @param showDecorations true if the rendering should include system bars etc.
    220      * @return this (such that chains of setters can be stringed together)
    221      */
    222     public RenderService setDecorations(boolean showDecorations) {
    223         mShowDecorations = showDecorations;
    224         return this;
    225     }
    226 
    227     /**
    228      * Sets the nodes to expand during rendering. These will be padded with approximately
    229      * 20 pixels and also highlighted by the {@link EmptyViewsOverlay}. The default is an
    230      * empty collection.
    231      *
    232      * @param nodesToExpand the nodes to be expanded
    233      * @return this (such that chains of setters can be stringed together)
    234      */
    235     public RenderService setNodesToExpand(Set<UiElementNode> nodesToExpand) {
    236         mExpandNodes = nodesToExpand;
    237         return this;
    238     }
    239 
    240     /**
    241      * Sets the {@link Reference} to an outer layout that this layout should be rendered
    242      * within. The outer layout <b>must</b> contain an include tag which points to this
    243      * layout. The default is null.
    244      *
    245      * @param includedWithin a reference to an outer layout to render this layout within
    246      * @return this (such that chains of setters can be stringed together)
    247      */
    248     public RenderService setIncludedWithin(Reference includedWithin) {
    249         mIncludedWithin = includedWithin;
    250         return this;
    251     }
    252 
    253     /** Initializes any remaining optional fields after all setters have been called */
    254     private void finishConfiguration() {
    255         if (mLogger == null) {
    256             // Silent logging
    257             mLogger = new LayoutLog();
    258         }
    259     }
    260 
    261     /**
    262      * Renders the model and returns the result as a {@link RenderSession}.
    263      * @return the {@link RenderSession} resulting from rendering the current model
    264      */
    265     public RenderSession createRenderSession() {
    266         assert mModel != null && mWidth != -1 && mHeight != -1 : "Incomplete service config";
    267         finishConfiguration();
    268 
    269         if (mResourceResolver == null) {
    270             // Abort the rendering if the resources are not found.
    271             return null;
    272         }
    273 
    274         int width = mWidth;
    275         int height = mHeight;
    276         if (mUseExplodeMode) {
    277             // compute how many padding in x and y will bump the screen size
    278             List<UiElementNode> children = mModel.getUiChildren();
    279             if (children.size() == 1) {
    280                 ExplodedRenderingHelper helper = new ExplodedRenderingHelper(
    281                         children.get(0).getXmlNode(), mProject);
    282 
    283                 // there are 2 paddings for each view
    284                 // left and right, or top and bottom.
    285                 int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2;
    286 
    287                 width += helper.getWidthPadding() * paddingValue;
    288                 height += helper.getHeightPadding() * paddingValue;
    289             }
    290         }
    291 
    292         UiElementPullParser modelParser = new UiElementPullParser(mModel,
    293                 mUseExplodeMode, mExpandNodes, mDensity, mXdpi, mProject);
    294         ILayoutPullParser topParser = modelParser;
    295 
    296         // Code to support editing included layout
    297         // first reset the layout parser just in case.
    298         mProjectCallback.setLayoutParser(null, null);
    299 
    300         if (mIncludedWithin != null) {
    301             // Outer layout name:
    302             String contextLayoutName = mIncludedWithin.getName();
    303 
    304             // Find the layout file.
    305             ResourceValue contextLayout = mResourceResolver.findResValue(
    306                     LAYOUT_PREFIX + contextLayoutName, false  /* forceFrameworkOnly*/);
    307             if (contextLayout != null) {
    308                 File layoutFile = new File(contextLayout.getValue());
    309                 if (layoutFile.isFile()) {
    310                     try {
    311                         // Get the name of the layout actually being edited, without the extension
    312                         // as it's what IXmlPullParser.getParser(String) will receive.
    313                         String queryLayoutName = mEditor.getLayoutResourceName();
    314                         mProjectCallback.setLayoutParser(queryLayoutName, modelParser);
    315                         topParser = new ContextPullParser(mProjectCallback, layoutFile);
    316                         topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
    317                         topParser.setInput(new FileInputStream(layoutFile), "UTF-8"); //$NON-NLS-1$
    318                     } catch (XmlPullParserException e) {
    319                         AdtPlugin.log(e, ""); //$NON-NLS-1$
    320                     } catch (FileNotFoundException e) {
    321                         // this will not happen since we check above.
    322                     }
    323                 }
    324             }
    325         }
    326 
    327         SessionParams params = new SessionParams(
    328                 topParser,
    329                 mRenderingMode,
    330                 mProject /* projectKey */,
    331                 width, height,
    332                 mDensity, mXdpi, mYdpi,
    333                 mResourceResolver,
    334                 mProjectCallback,
    335                 mMinSdkVersion,
    336                 mTargetSdkVersion,
    337                 mLogger);
    338 
    339         // Request margin and baseline information.
    340         // TODO: Be smarter about setting this; start without it, and on the first request
    341         // for an extended view info, re-render in the same session, and then set a flag
    342         // which will cause this to create extended view info each time from then on in the
    343         // same session
    344         params.setExtendedViewInfoMode(true);
    345 
    346         if (!mShowDecorations) {
    347             params.setForceNoDecor();
    348         } else {
    349             ManifestInfo manifestInfo = ManifestInfo.get(mProject);
    350             try {
    351                 params.setAppLabel(manifestInfo.getApplicationLabel());
    352                 params.setAppIcon(manifestInfo.getApplicationIcon());
    353             } catch (Exception e) {
    354                 // ignore.
    355             }
    356         }
    357 
    358         if (mScreenSize != null) {
    359             params.setConfigScreenSize(mScreenSize.getValue());
    360         }
    361 
    362         if (mOverrideBgColor != null) {
    363             params.setOverrideBgColor(mOverrideBgColor.intValue());
    364         }
    365 
    366         // set the Image Overlay as the image factory.
    367         params.setImageFactory(mImageFactory);
    368 
    369         try {
    370             mProjectCallback.setLogger(mLogger);
    371             mProjectCallback.setResourceResolver(mResourceResolver);
    372             return mLayoutLib.createSession(params);
    373         } catch (RuntimeException t) {
    374             // Exceptions from the bridge
    375             mLogger.error(null, t.getLocalizedMessage(), t, null);
    376             throw t;
    377         } finally {
    378             mProjectCallback.setLogger(null);
    379             mProjectCallback.setResourceResolver(null);
    380         }
    381     }
    382 
    383     /**
    384      * Renders the given resource value (which should refer to a drawable) and returns it
    385      * as an image
    386      *
    387      * @param drawableResourceValue the drawable resource value to be rendered, or null
    388      * @return the image, or null if something went wrong
    389      */
    390     public BufferedImage renderDrawable(ResourceValue drawableResourceValue) {
    391         if (drawableResourceValue == null) {
    392             return null;
    393         }
    394 
    395         finishConfiguration();
    396 
    397         DrawableParams params = new DrawableParams(drawableResourceValue, mProject, mWidth, mHeight,
    398                 mDensity, mXdpi, mYdpi, mResourceResolver, mProjectCallback, mMinSdkVersion,
    399                 mTargetSdkVersion, mLogger);
    400         params.setForceNoDecor();
    401         Result result = mLayoutLib.renderDrawable(params);
    402         if (result != null && result.isSuccess()) {
    403             Object data = result.getData();
    404             if (data instanceof BufferedImage) {
    405                 return (BufferedImage) data;
    406             }
    407         }
    408 
    409         return null;
    410     }
    411 
    412     /**
    413      * Measure the children of the given parent node, applying the given filter to the
    414      * pull parser's attribute values.
    415      *
    416      * @param parent the parent node to measure children for
    417      * @param filter the filter to apply to the attribute values
    418      * @return a map from node children of the parent to new bounds of the nodes
    419      */
    420     public Map<INode, Rect> measureChildren(INode parent,
    421             final IClientRulesEngine.AttributeFilter filter) {
    422         finishConfiguration();
    423 
    424         int width = parent.getBounds().w;
    425         int height = parent.getBounds().h;
    426 
    427         final NodeFactory mNodeFactory = mEditor.getCanvasControl().getNodeFactory();
    428         UiElementNode parentNode = ((NodeProxy) parent).getNode();
    429         UiElementPullParser topParser = new UiElementPullParser(parentNode,
    430                 false, Collections.<UiElementNode>emptySet(), mDensity, mXdpi, mProject) {
    431             @Override
    432             public String getAttributeValue(String namespace, String localName) {
    433                 if (filter != null) {
    434                     Object cookie = getViewCookie();
    435                     if (cookie instanceof UiViewElementNode) {
    436                         NodeProxy node = mNodeFactory.create((UiViewElementNode) cookie);
    437                         if (node != null) {
    438                             String value = filter.getAttribute(node, namespace, localName);
    439                             if (value != null) {
    440                                 return value;
    441                             }
    442                             // null means no preference, not "unset".
    443                         }
    444                     }
    445                 }
    446 
    447                 return super.getAttributeValue(namespace, localName);
    448             }
    449 
    450             /**
    451              * The parser usually assumes that the top level node is a document node that
    452              * should be skipped, and that's not the case when we render in the middle of
    453              * the tree, so override {@link UiElementPullParser#onNextFromStartDocument}
    454              * to change this behavior
    455              */
    456             @Override
    457             public void onNextFromStartDocument() {
    458                 mParsingState = START_TAG;
    459             }
    460         };
    461 
    462         SessionParams params = new SessionParams(
    463                 topParser,
    464                 RenderingMode.FULL_EXPAND,
    465                 mProject /* projectKey */,
    466                 width, height,
    467                 mDensity, mXdpi, mYdpi,
    468                 mResourceResolver,
    469                 mProjectCallback,
    470                 mMinSdkVersion,
    471                 mTargetSdkVersion,
    472                 mLogger);
    473         params.setLayoutOnly();
    474         params.setForceNoDecor();
    475 
    476         RenderSession session = null;
    477         try {
    478             mProjectCallback.setLogger(mLogger);
    479             mProjectCallback.setResourceResolver(mResourceResolver);
    480             session = mLayoutLib.createSession(params);
    481             if (session.getResult().isSuccess()) {
    482                 assert session.getRootViews().size() == 1;
    483                 ViewInfo root = session.getRootViews().get(0);
    484                 List<ViewInfo> children = root.getChildren();
    485                 Map<INode, Rect> map = new HashMap<INode, Rect>(children.size());
    486                 for (ViewInfo info : children) {
    487                     if (info.getCookie() instanceof UiViewElementNode) {
    488                         UiViewElementNode uiNode = (UiViewElementNode) info.getCookie();
    489                         NodeProxy node = mNodeFactory.create(uiNode);
    490                         map.put(node, new Rect(info.getLeft(), info.getTop(),
    491                                 info.getRight() - info.getLeft(),
    492                                 info.getBottom() - info.getTop()));
    493                     }
    494                 }
    495 
    496                 return map;
    497             }
    498         } catch (RuntimeException t) {
    499             // Exceptions from the bridge
    500             mLogger.error(null, t.getLocalizedMessage(), t, null);
    501             throw t;
    502         } finally {
    503             mProjectCallback.setLogger(null);
    504             mProjectCallback.setResourceResolver(null);
    505             if (session != null) {
    506                 session.dispose();
    507             }
    508         }
    509 
    510         return null;
    511     }
    512 }
    513