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