RenderSessionImpl.java revision 14bf0cef7eeed572a67c29a328581afac4decc20
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 17package com.android.layoutlib.bridge.impl; 18 19import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND; 20import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; 21import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; 22import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; 23import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN; 24import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; 25 26import com.android.ide.common.rendering.api.AdapterBinding; 27import com.android.ide.common.rendering.api.HardwareConfig; 28import com.android.ide.common.rendering.api.IAnimationListener; 29import com.android.ide.common.rendering.api.ILayoutPullParser; 30import com.android.ide.common.rendering.api.IProjectCallback; 31import com.android.ide.common.rendering.api.RenderParams; 32import com.android.ide.common.rendering.api.RenderResources; 33import com.android.ide.common.rendering.api.RenderSession; 34import com.android.ide.common.rendering.api.ResourceReference; 35import com.android.ide.common.rendering.api.ResourceValue; 36import com.android.ide.common.rendering.api.Result; 37import com.android.ide.common.rendering.api.Result.Status; 38import com.android.ide.common.rendering.api.SessionParams; 39import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 40import com.android.ide.common.rendering.api.ViewInfo; 41import com.android.internal.util.XmlUtils; 42import com.android.layoutlib.bridge.Bridge; 43import com.android.layoutlib.bridge.android.BridgeContext; 44import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; 45import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 46import com.android.layoutlib.bridge.bars.ActionBarLayout; 47import com.android.layoutlib.bridge.bars.NavigationBar; 48import com.android.layoutlib.bridge.bars.StatusBar; 49import com.android.layoutlib.bridge.bars.TitleBar; 50import com.android.layoutlib.bridge.impl.binding.FakeAdapter; 51import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; 52import com.android.resources.Density; 53import com.android.resources.ResourceType; 54import com.android.resources.ScreenOrientation; 55import com.android.util.Pair; 56 57import org.xmlpull.v1.XmlPullParserException; 58 59import android.animation.AnimationThread; 60import android.animation.Animator; 61import android.animation.AnimatorInflater; 62import android.animation.LayoutTransition; 63import android.animation.LayoutTransition.TransitionListener; 64import android.app.Fragment_Delegate; 65import android.graphics.Bitmap; 66import android.graphics.Bitmap_Delegate; 67import android.graphics.Canvas; 68import android.graphics.drawable.Drawable; 69import android.util.DisplayMetrics; 70import android.util.TypedValue; 71import android.view.AttachInfo_Accessor; 72import android.view.BridgeInflater; 73import android.view.IWindowManager; 74import android.view.IWindowManagerImpl; 75import android.view.Surface; 76import android.view.View; 77import android.view.View.MeasureSpec; 78import android.view.ViewGroup; 79import android.view.ViewGroup.LayoutParams; 80import android.view.ViewGroup.MarginLayoutParams; 81import android.view.WindowManagerGlobal_Delegate; 82import android.widget.AbsListView; 83import android.widget.AbsSpinner; 84import android.widget.AdapterView; 85import android.widget.ExpandableListView; 86import android.widget.FrameLayout; 87import android.widget.LinearLayout; 88import android.widget.ListView; 89import android.widget.QuickContactBadge; 90import android.widget.TabHost; 91import android.widget.TabHost.TabSpec; 92import android.widget.TabWidget; 93 94import java.awt.AlphaComposite; 95import java.awt.Color; 96import java.awt.Graphics2D; 97import java.awt.image.BufferedImage; 98import java.util.ArrayList; 99import java.util.List; 100import java.util.Map; 101 102/** 103 * Class implementing the render session. 104 * 105 * A session is a stateful representation of a layout file. It is initialized with data coming 106 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then 107 * be done on the layout. 108 * 109 */ 110public class RenderSessionImpl extends RenderAction<SessionParams> { 111 112 private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; 113 private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; 114 115 // scene state 116 private RenderSession mScene; 117 private BridgeXmlBlockParser mBlockParser; 118 private BridgeInflater mInflater; 119 private ResourceValue mWindowBackground; 120 private ViewGroup mViewRoot; 121 private FrameLayout mContentRoot; 122 private Canvas mCanvas; 123 private int mMeasuredScreenWidth = -1; 124 private int mMeasuredScreenHeight = -1; 125 private boolean mIsAlphaChannelImage; 126 private boolean mWindowIsFloating; 127 128 private int mStatusBarSize; 129 private int mNavigationBarSize; 130 private int mNavigationBarOrientation = LinearLayout.HORIZONTAL; 131 private int mTitleBarSize; 132 private int mActionBarSize; 133 134 135 // information being returned through the API 136 private BufferedImage mImage; 137 private List<ViewInfo> mViewInfoList; 138 private List<ViewInfo> mSystemViewInfoList; 139 140 private static final class PostInflateException extends Exception { 141 private static final long serialVersionUID = 1L; 142 143 public PostInflateException(String message) { 144 super(message); 145 } 146 } 147 148 /** 149 * Creates a layout scene with all the information coming from the layout bridge API. 150 * <p> 151 * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init()}, which act as a 152 * call to {@link RenderSessionImpl#acquire(long)} 153 * 154 * @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams) 155 */ 156 public RenderSessionImpl(SessionParams params) { 157 super(new SessionParams(params)); 158 } 159 160 /** 161 * Initializes and acquires the scene, creating various Android objects such as context, 162 * inflater, and parser. 163 * 164 * @param timeout the time to wait if another rendering is happening. 165 * 166 * @return whether the scene was prepared 167 * 168 * @see #acquire(long) 169 * @see #release() 170 */ 171 @Override 172 public Result init(long timeout) { 173 Result result = super.init(timeout); 174 if (result.isSuccess() == false) { 175 return result; 176 } 177 178 SessionParams params = getParams(); 179 BridgeContext context = getContext(); 180 181 RenderResources resources = getParams().getResources(); 182 DisplayMetrics metrics = getContext().getMetrics(); 183 184 // use default of true in case it's not found to use alpha by default 185 mIsAlphaChannelImage = getBooleanThemeValue(resources, 186 "windowIsFloating", true /*defaultValue*/); 187 188 mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating", 189 true /*defaultValue*/); 190 191 findBackground(resources); 192 findStatusBar(resources, metrics); 193 findActionBar(resources, metrics); 194 findNavigationBar(resources, metrics); 195 196 // FIXME: find those out, and possibly add them to the render params 197 boolean hasNavigationBar = true; 198 IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(), 199 metrics, Surface.ROTATION_0, 200 hasNavigationBar); 201 WindowManagerGlobal_Delegate.setWindowManagerService(iwm); 202 203 // build the inflater and parser. 204 mInflater = new BridgeInflater(context, params.getProjectCallback()); 205 context.setBridgeInflater(mInflater); 206 207 mBlockParser = new BridgeXmlBlockParser( 208 params.getLayoutDescription(), context, false /* platformResourceFlag */); 209 210 return SUCCESS.createResult(); 211 } 212 213 /** 214 * Inflates the layout. 215 * <p> 216 * {@link #acquire(long)} must have been called before this. 217 * 218 * @throws IllegalStateException if the current context is different than the one owned by 219 * the scene, or if {@link #init(long)} was not called. 220 */ 221 public Result inflate() { 222 checkLock(); 223 224 try { 225 226 SessionParams params = getParams(); 227 HardwareConfig hardwareConfig = params.getHardwareConfig(); 228 BridgeContext context = getContext(); 229 boolean isRtl = Bridge.isLocaleRtl(params.getLocale()); 230 int layoutDirection = isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR; 231 232 // the view group that receives the window background. 233 ViewGroup backgroundView = null; 234 235 if (mWindowIsFloating || params.isForceNoDecor()) { 236 backgroundView = mViewRoot = mContentRoot = new FrameLayout(context); 237 mViewRoot.setLayoutDirection(layoutDirection); 238 } else { 239 if (hasSoftwareButtons() && mNavigationBarOrientation == LinearLayout.VERTICAL) { 240 /* 241 * This is a special case where the navigation bar is on the right. 242 +-------------------------------------------------+---+ 243 | Status bar (always) | | 244 +-------------------------------------------------+ | 245 | (Layout with background drawable) | | 246 | +---------------------------------------------+ | | 247 | | Title/Action bar (optional) | | | 248 | +---------------------------------------------+ | | 249 | | Content, vertical extending | | | 250 | | | | | 251 | +---------------------------------------------+ | | 252 +-------------------------------------------------+---+ 253 254 So we create a horizontal layout, with the nav bar on the right, 255 and the left part is the normal layout below without the nav bar at 256 the bottom 257 */ 258 LinearLayout topLayout = new LinearLayout(context); 259 topLayout.setLayoutDirection(layoutDirection); 260 mViewRoot = topLayout; 261 topLayout.setOrientation(LinearLayout.HORIZONTAL); 262 263 try { 264 NavigationBar navigationBar = createNavigationBar(context, 265 hardwareConfig.getDensity(), isRtl, params.isRtlSupported()); 266 topLayout.addView(navigationBar); 267 } catch (XmlPullParserException e) { 268 269 } 270 } 271 272 /* 273 * we're creating the following layout 274 * 275 +-------------------------------------------------+ 276 | Status bar (always) | 277 +-------------------------------------------------+ 278 | (Layout with background drawable) | 279 | +---------------------------------------------+ | 280 | | Title/Action bar (optional) | | 281 | +---------------------------------------------+ | 282 | | Content, vertical extending | | 283 | | | | 284 | +---------------------------------------------+ | 285 +-------------------------------------------------+ 286 | Navigation bar for soft buttons, maybe see above| 287 +-------------------------------------------------+ 288 289 */ 290 291 LinearLayout topLayout = new LinearLayout(context); 292 topLayout.setOrientation(LinearLayout.VERTICAL); 293 topLayout.setLayoutDirection(layoutDirection); 294 // if we don't already have a view root this is it 295 if (mViewRoot == null) { 296 mViewRoot = topLayout; 297 } else { 298 int topLayoutWidth = 299 params.getHardwareConfig().getScreenWidth() - mNavigationBarSize; 300 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 301 topLayoutWidth, LayoutParams.MATCH_PARENT); 302 topLayout.setLayoutParams(layoutParams); 303 304 // this is the case of soft buttons + vertical bar. 305 // this top layout is the first layout in the horizontal layout. see above) 306 if (isRtl && params.isRtlSupported()) { 307 // If RTL is enabled, layoutlib will mirror the layouts. So, add the 308 // topLayout to the right of Navigation Bar and layoutlib will draw it 309 // to the left. 310 mViewRoot.addView(topLayout); 311 } else { 312 // Add the top layout to the left of the Navigation Bar. 313 mViewRoot.addView(topLayout, 0); 314 } 315 } 316 317 if (mStatusBarSize > 0) { 318 // system bar 319 try { 320 StatusBar statusBar = createStatusBar(context, hardwareConfig.getDensity(), 321 layoutDirection, params.isRtlSupported()); 322 topLayout.addView(statusBar); 323 } catch (XmlPullParserException e) { 324 325 } 326 } 327 328 LinearLayout backgroundLayout = new LinearLayout(context); 329 backgroundView = backgroundLayout; 330 backgroundLayout.setOrientation(LinearLayout.VERTICAL); 331 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 332 LayoutParams.MATCH_PARENT, 0); 333 layoutParams.weight = 1; 334 backgroundLayout.setLayoutParams(layoutParams); 335 topLayout.addView(backgroundLayout); 336 337 338 // if the theme says no title/action bar, then the size will be 0 339 if (mActionBarSize > 0) { 340 try { 341 ActionBarLayout actionBar = createActionBar(context, params); 342 backgroundLayout.addView(actionBar); 343 mContentRoot = (FrameLayout) actionBar.findViewById(android.R.id.content); 344 } catch (XmlPullParserException e) { 345 346 } 347 } else if (mTitleBarSize > 0) { 348 try { 349 TitleBar titleBar = createTitleBar(context, 350 hardwareConfig.getDensity(), params.getAppLabel()); 351 backgroundLayout.addView(titleBar); 352 } catch (XmlPullParserException e) { 353 354 } 355 } 356 357 // content frame 358 if (mContentRoot == null) { 359 mContentRoot = new FrameLayout(context); 360 layoutParams = new LinearLayout.LayoutParams( 361 LayoutParams.MATCH_PARENT, 0); 362 layoutParams.weight = 1; 363 mContentRoot.setLayoutParams(layoutParams); 364 backgroundLayout.addView(mContentRoot); 365 } 366 367 if (mNavigationBarOrientation == LinearLayout.HORIZONTAL && 368 mNavigationBarSize > 0) { 369 // system bar 370 try { 371 NavigationBar navigationBar = createNavigationBar(context, 372 hardwareConfig.getDensity(), isRtl, params.isRtlSupported()); 373 topLayout.addView(navigationBar); 374 } catch (XmlPullParserException e) { 375 376 } 377 } 378 } 379 380 381 // Sets the project callback (custom view loader) to the fragment delegate so that 382 // it can instantiate the custom Fragment. 383 Fragment_Delegate.setProjectCallback(params.getProjectCallback()); 384 385 View view = mInflater.inflate(mBlockParser, mContentRoot); 386 387 // done with the parser, pop it. 388 context.popParser(); 389 390 Fragment_Delegate.setProjectCallback(null); 391 392 // set the AttachInfo on the root view. 393 AttachInfo_Accessor.setAttachInfo(mViewRoot); 394 395 // post-inflate process. For now this supports TabHost/TabWidget 396 postInflateProcess(view, params.getProjectCallback()); 397 398 // get the background drawable 399 if (mWindowBackground != null && backgroundView != null) { 400 Drawable d = ResourceHelper.getDrawable(mWindowBackground, context); 401 backgroundView.setBackground(d); 402 } 403 404 return SUCCESS.createResult(); 405 } catch (PostInflateException e) { 406 return ERROR_INFLATION.createResult(e.getMessage(), e); 407 } catch (Throwable e) { 408 // get the real cause of the exception. 409 Throwable t = e; 410 while (t.getCause() != null) { 411 t = t.getCause(); 412 } 413 414 return ERROR_INFLATION.createResult(t.getMessage(), t); 415 } 416 } 417 418 /** 419 * Renders the scene. 420 * <p> 421 * {@link #acquire(long)} must have been called before this. 422 * 423 * @param freshRender whether the render is a new one and should erase the existing bitmap (in 424 * the case where bitmaps are reused). This is typically needed when not playing 425 * animations.) 426 * 427 * @throws IllegalStateException if the current context is different than the one owned by 428 * the scene, or if {@link #acquire(long)} was not called. 429 * 430 * @see RenderParams#getRenderingMode() 431 * @see RenderSession#render(long) 432 */ 433 public Result render(boolean freshRender) { 434 checkLock(); 435 436 SessionParams params = getParams(); 437 438 try { 439 if (mViewRoot == null) { 440 return ERROR_NOT_INFLATED.createResult(); 441 } 442 443 RenderingMode renderingMode = params.getRenderingMode(); 444 HardwareConfig hardwareConfig = params.getHardwareConfig(); 445 446 // only do the screen measure when needed. 447 boolean newRenderSize = false; 448 if (mMeasuredScreenWidth == -1) { 449 newRenderSize = true; 450 mMeasuredScreenWidth = hardwareConfig.getScreenWidth(); 451 mMeasuredScreenHeight = hardwareConfig.getScreenHeight(); 452 453 if (renderingMode != RenderingMode.NORMAL) { 454 int widthMeasureSpecMode = renderingMode.isHorizExpand() ? 455 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 456 : MeasureSpec.EXACTLY; 457 int heightMeasureSpecMode = renderingMode.isVertExpand() ? 458 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 459 : MeasureSpec.EXACTLY; 460 461 // We used to compare the measured size of the content to the screen size but 462 // this does not work anymore due to the 2 following issues: 463 // - If the content is in a decor (system bar, title/action bar), the root view 464 // will not resize even with the UNSPECIFIED because of the embedded layout. 465 // - If there is no decor, but a dialog frame, then the dialog padding prevents 466 // comparing the size of the content to the screen frame (as it would not 467 // take into account the dialog padding). 468 469 // The solution is to first get the content size in a normal rendering, inside 470 // the decor or the dialog padding. 471 // Then measure only the content with UNSPECIFIED to see the size difference 472 // and apply this to the screen size. 473 474 // first measure the full layout, with EXACTLY to get the size of the 475 // content as it is inside the decor/dialog 476 Pair<Integer, Integer> exactMeasure = measureView( 477 mViewRoot, mContentRoot.getChildAt(0), 478 mMeasuredScreenWidth, MeasureSpec.EXACTLY, 479 mMeasuredScreenHeight, MeasureSpec.EXACTLY); 480 481 // now measure the content only using UNSPECIFIED (where applicable, based on 482 // the rendering mode). This will give us the size the content needs. 483 Pair<Integer, Integer> result = measureView( 484 mContentRoot, mContentRoot.getChildAt(0), 485 mMeasuredScreenWidth, widthMeasureSpecMode, 486 mMeasuredScreenHeight, heightMeasureSpecMode); 487 488 // now look at the difference and add what is needed. 489 if (renderingMode.isHorizExpand()) { 490 int measuredWidth = exactMeasure.getFirst(); 491 int neededWidth = result.getFirst(); 492 if (neededWidth > measuredWidth) { 493 mMeasuredScreenWidth += neededWidth - measuredWidth; 494 } 495 } 496 497 if (renderingMode.isVertExpand()) { 498 int measuredHeight = exactMeasure.getSecond(); 499 int neededHeight = result.getSecond(); 500 if (neededHeight > measuredHeight) { 501 mMeasuredScreenHeight += neededHeight - measuredHeight; 502 } 503 } 504 } 505 } 506 507 // measure again with the size we need 508 // This must always be done before the call to layout 509 measureView(mViewRoot, null /*measuredView*/, 510 mMeasuredScreenWidth, MeasureSpec.EXACTLY, 511 mMeasuredScreenHeight, MeasureSpec.EXACTLY); 512 513 // now do the layout. 514 mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 515 516 if (params.isLayoutOnly()) { 517 // delete the canvas and image to reset them on the next full rendering 518 mImage = null; 519 mCanvas = null; 520 } else { 521 AttachInfo_Accessor.dispatchOnPreDraw(mViewRoot); 522 523 // draw the views 524 // create the BufferedImage into which the layout will be rendered. 525 boolean newImage = false; 526 if (newRenderSize || mCanvas == null) { 527 if (params.getImageFactory() != null) { 528 mImage = params.getImageFactory().getImage( 529 mMeasuredScreenWidth, 530 mMeasuredScreenHeight); 531 } else { 532 mImage = new BufferedImage( 533 mMeasuredScreenWidth, 534 mMeasuredScreenHeight, 535 BufferedImage.TYPE_INT_ARGB); 536 newImage = true; 537 } 538 539 if (params.isBgColorOverridden()) { 540 // since we override the content, it's the same as if it was a new image. 541 newImage = true; 542 Graphics2D gc = mImage.createGraphics(); 543 gc.setColor(new Color(params.getOverrideBgColor(), true)); 544 gc.setComposite(AlphaComposite.Src); 545 gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 546 gc.dispose(); 547 } 548 549 // create an Android bitmap around the BufferedImage 550 Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, 551 true /*isMutable*/, hardwareConfig.getDensity()); 552 553 // create a Canvas around the Android bitmap 554 mCanvas = new Canvas(bitmap); 555 mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue()); 556 } 557 558 if (freshRender && newImage == false) { 559 Graphics2D gc = mImage.createGraphics(); 560 gc.setComposite(AlphaComposite.Src); 561 562 gc.setColor(new Color(0x00000000, true)); 563 gc.fillRect(0, 0, 564 mMeasuredScreenWidth, mMeasuredScreenHeight); 565 566 // done 567 gc.dispose(); 568 } 569 570 mViewRoot.draw(mCanvas); 571 } 572 573 mSystemViewInfoList = visitAllChildren(mViewRoot, 0, params.getExtendedViewInfoMode(), false); 574 575 // success! 576 return SUCCESS.createResult(); 577 } catch (Throwable e) { 578 // get the real cause of the exception. 579 Throwable t = e; 580 while (t.getCause() != null) { 581 t = t.getCause(); 582 } 583 584 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 585 } 586 } 587 588 /** 589 * Executes {@link View#measure(int, int)} on a given view with the given parameters (used 590 * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}. 591 * 592 * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height) 593 * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}). 594 * 595 * @param viewToMeasure the view on which to execute measure(). 596 * @param measuredView if non null, the view to query for its measured width/height. 597 * @param width the width to use in the MeasureSpec. 598 * @param widthMode the MeasureSpec mode to use for the width. 599 * @param height the height to use in the MeasureSpec. 600 * @param heightMode the MeasureSpec mode to use for the height. 601 * @return the measured width/height if measuredView is non-null, null otherwise. 602 */ 603 private Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView, 604 int width, int widthMode, int height, int heightMode) { 605 int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode); 606 int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode); 607 viewToMeasure.measure(w_spec, h_spec); 608 609 if (measuredView != null) { 610 return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight()); 611 } 612 613 return null; 614 } 615 616 /** 617 * Animate an object 618 * <p> 619 * {@link #acquire(long)} must have been called before this. 620 * 621 * @throws IllegalStateException if the current context is different than the one owned by 622 * the scene, or if {@link #acquire(long)} was not called. 623 * 624 * @see RenderSession#animate(Object, String, boolean, IAnimationListener) 625 */ 626 public Result animate(Object targetObject, String animationName, 627 boolean isFrameworkAnimation, IAnimationListener listener) { 628 checkLock(); 629 630 BridgeContext context = getContext(); 631 632 // find the animation file. 633 ResourceValue animationResource = null; 634 int animationId = 0; 635 if (isFrameworkAnimation) { 636 animationResource = context.getRenderResources().getFrameworkResource( 637 ResourceType.ANIMATOR, animationName); 638 if (animationResource != null) { 639 animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName); 640 } 641 } else { 642 animationResource = context.getRenderResources().getProjectResource( 643 ResourceType.ANIMATOR, animationName); 644 if (animationResource != null) { 645 animationId = context.getProjectCallback().getResourceId( 646 ResourceType.ANIMATOR, animationName); 647 } 648 } 649 650 if (animationResource != null) { 651 try { 652 Animator anim = AnimatorInflater.loadAnimator(context, animationId); 653 if (anim != null) { 654 anim.setTarget(targetObject); 655 656 new PlayAnimationThread(anim, this, animationName, listener).start(); 657 658 return SUCCESS.createResult(); 659 } 660 } catch (Exception e) { 661 // get the real cause of the exception. 662 Throwable t = e; 663 while (t.getCause() != null) { 664 t = t.getCause(); 665 } 666 667 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 668 } 669 } 670 671 return ERROR_ANIM_NOT_FOUND.createResult(); 672 } 673 674 /** 675 * Insert a new child into an existing parent. 676 * <p> 677 * {@link #acquire(long)} must have been called before this. 678 * 679 * @throws IllegalStateException if the current context is different than the one owned by 680 * the scene, or if {@link #acquire(long)} was not called. 681 * 682 * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener) 683 */ 684 public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, 685 final int index, IAnimationListener listener) { 686 checkLock(); 687 688 BridgeContext context = getContext(); 689 690 // create a block parser for the XML 691 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 692 childXml, context, false /* platformResourceFlag */); 693 694 // inflate the child without adding it to the root since we want to control where it'll 695 // get added. We do pass the parentView however to ensure that the layoutParams will 696 // be created correctly. 697 final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); 698 blockParser.ensurePopped(); 699 700 invalidateRenderingSize(); 701 702 if (listener != null) { 703 new AnimationThread(this, "insertChild", listener) { 704 705 @Override 706 public Result preAnimation() { 707 parentView.setLayoutTransition(new LayoutTransition()); 708 return addView(parentView, child, index); 709 } 710 711 @Override 712 public void postAnimation() { 713 parentView.setLayoutTransition(null); 714 } 715 }.start(); 716 717 // always return success since the real status will come through the listener. 718 return SUCCESS.createResult(child); 719 } 720 721 // add it to the parentView in the correct location 722 Result result = addView(parentView, child, index); 723 if (result.isSuccess() == false) { 724 return result; 725 } 726 727 result = render(false /*freshRender*/); 728 if (result.isSuccess()) { 729 result = result.getCopyWithData(child); 730 } 731 732 return result; 733 } 734 735 /** 736 * Adds a given view to a given parent at a given index. 737 * 738 * @param parent the parent to receive the view 739 * @param view the view to add to the parent 740 * @param index the index where to do the add. 741 * 742 * @return a Result with {@link Status#SUCCESS} or 743 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 744 * adding views. 745 */ 746 private Result addView(ViewGroup parent, View view, int index) { 747 try { 748 parent.addView(view, index); 749 return SUCCESS.createResult(); 750 } catch (UnsupportedOperationException e) { 751 // looks like this is a view class that doesn't support children manipulation! 752 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 753 } 754 } 755 756 /** 757 * Moves a view to a new parent at a given location 758 * <p> 759 * {@link #acquire(long)} must have been called before this. 760 * 761 * @throws IllegalStateException if the current context is different than the one owned by 762 * the scene, or if {@link #acquire(long)} was not called. 763 * 764 * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener) 765 */ 766 public Result moveChild(final ViewGroup newParentView, final View childView, final int index, 767 Map<String, String> layoutParamsMap, final IAnimationListener listener) { 768 checkLock(); 769 770 invalidateRenderingSize(); 771 772 LayoutParams layoutParams = null; 773 if (layoutParamsMap != null) { 774 // need to create a new LayoutParams object for the new parent. 775 layoutParams = newParentView.generateLayoutParams( 776 new BridgeLayoutParamsMapAttributes(layoutParamsMap)); 777 } 778 779 // get the current parent of the view that needs to be moved. 780 final ViewGroup previousParent = (ViewGroup) childView.getParent(); 781 782 if (listener != null) { 783 final LayoutParams params = layoutParams; 784 785 // there is no support for animating views across layouts, so in case the new and old 786 // parent views are different we fake the animation through a no animation thread. 787 if (previousParent != newParentView) { 788 new Thread("not animated moveChild") { 789 @Override 790 public void run() { 791 Result result = moveView(previousParent, newParentView, childView, index, 792 params); 793 if (result.isSuccess() == false) { 794 listener.done(result); 795 } 796 797 // ready to do the work, acquire the scene. 798 result = acquire(250); 799 if (result.isSuccess() == false) { 800 listener.done(result); 801 return; 802 } 803 804 try { 805 result = render(false /*freshRender*/); 806 if (result.isSuccess()) { 807 listener.onNewFrame(RenderSessionImpl.this.getSession()); 808 } 809 } finally { 810 release(); 811 } 812 813 listener.done(result); 814 } 815 }.start(); 816 } else { 817 new AnimationThread(this, "moveChild", listener) { 818 819 @Override 820 public Result preAnimation() { 821 // set up the transition for the parent. 822 LayoutTransition transition = new LayoutTransition(); 823 previousParent.setLayoutTransition(transition); 824 825 // tweak the animation durations and start delays (to match the duration of 826 // animation playing just before). 827 // Note: Cannot user Animation.setDuration() directly. Have to set it 828 // on the LayoutTransition. 829 transition.setDuration(LayoutTransition.DISAPPEARING, 100); 830 // CHANGE_DISAPPEARING plays after DISAPPEARING 831 transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); 832 833 transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); 834 835 transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); 836 // CHANGE_APPEARING plays after CHANGE_APPEARING 837 transition.setStartDelay(LayoutTransition.APPEARING, 100); 838 839 transition.setDuration(LayoutTransition.APPEARING, 100); 840 841 return moveView(previousParent, newParentView, childView, index, params); 842 } 843 844 @Override 845 public void postAnimation() { 846 previousParent.setLayoutTransition(null); 847 newParentView.setLayoutTransition(null); 848 } 849 }.start(); 850 } 851 852 // always return success since the real status will come through the listener. 853 return SUCCESS.createResult(layoutParams); 854 } 855 856 Result result = moveView(previousParent, newParentView, childView, index, layoutParams); 857 if (result.isSuccess() == false) { 858 return result; 859 } 860 861 result = render(false /*freshRender*/); 862 if (layoutParams != null && result.isSuccess()) { 863 result = result.getCopyWithData(layoutParams); 864 } 865 866 return result; 867 } 868 869 /** 870 * Moves a View from its current parent to a new given parent at a new given location, with 871 * an optional new {@link LayoutParams} instance 872 * 873 * @param previousParent the previous parent, still owning the child at the time of the call. 874 * @param newParent the new parent 875 * @param movedView the view to move 876 * @param index the new location in the new parent 877 * @param params an option (can be null) {@link LayoutParams} instance. 878 * 879 * @return a Result with {@link Status#SUCCESS} or 880 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 881 * adding views. 882 */ 883 private Result moveView(ViewGroup previousParent, final ViewGroup newParent, 884 final View movedView, final int index, final LayoutParams params) { 885 try { 886 // check if there is a transition on the previousParent. 887 LayoutTransition previousTransition = previousParent.getLayoutTransition(); 888 if (previousTransition != null) { 889 // in this case there is an animation. This means we have to wait for the child's 890 // parent reference to be null'ed out so that we can add it to the new parent. 891 // It is technically removed right before the DISAPPEARING animation is done (if 892 // the animation of this type is not null, otherwise it's after which is impossible 893 // to handle). 894 // Because there is no move animation, if the new parent is the same as the old 895 // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before 896 // adding the child or the child will appear in its new location before the 897 // other children have made room for it. 898 899 // add a listener to the transition to be notified of the actual removal. 900 previousTransition.addTransitionListener(new TransitionListener() { 901 private int mChangeDisappearingCount = 0; 902 903 @Override 904 public void startTransition(LayoutTransition transition, ViewGroup container, 905 View view, int transitionType) { 906 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 907 mChangeDisappearingCount++; 908 } 909 } 910 911 @Override 912 public void endTransition(LayoutTransition transition, ViewGroup container, 913 View view, int transitionType) { 914 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 915 mChangeDisappearingCount--; 916 } 917 918 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && 919 mChangeDisappearingCount == 0) { 920 // add it to the parentView in the correct location 921 if (params != null) { 922 newParent.addView(movedView, index, params); 923 } else { 924 newParent.addView(movedView, index); 925 } 926 } 927 } 928 }); 929 930 // remove the view from the current parent. 931 previousParent.removeView(movedView); 932 933 // and return since adding the view to the new parent is done in the listener. 934 return SUCCESS.createResult(); 935 } else { 936 // standard code with no animation. pretty simple. 937 previousParent.removeView(movedView); 938 939 // add it to the parentView in the correct location 940 if (params != null) { 941 newParent.addView(movedView, index, params); 942 } else { 943 newParent.addView(movedView, index); 944 } 945 946 return SUCCESS.createResult(); 947 } 948 } catch (UnsupportedOperationException e) { 949 // looks like this is a view class that doesn't support children manipulation! 950 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 951 } 952 } 953 954 /** 955 * Removes a child from its current parent. 956 * <p> 957 * {@link #acquire(long)} must have been called before this. 958 * 959 * @throws IllegalStateException if the current context is different than the one owned by 960 * the scene, or if {@link #acquire(long)} was not called. 961 * 962 * @see RenderSession#removeChild(Object, IAnimationListener) 963 */ 964 public Result removeChild(final View childView, IAnimationListener listener) { 965 checkLock(); 966 967 invalidateRenderingSize(); 968 969 final ViewGroup parent = (ViewGroup) childView.getParent(); 970 971 if (listener != null) { 972 new AnimationThread(this, "moveChild", listener) { 973 974 @Override 975 public Result preAnimation() { 976 parent.setLayoutTransition(new LayoutTransition()); 977 return removeView(parent, childView); 978 } 979 980 @Override 981 public void postAnimation() { 982 parent.setLayoutTransition(null); 983 } 984 }.start(); 985 986 // always return success since the real status will come through the listener. 987 return SUCCESS.createResult(); 988 } 989 990 Result result = removeView(parent, childView); 991 if (result.isSuccess() == false) { 992 return result; 993 } 994 995 return render(false /*freshRender*/); 996 } 997 998 /** 999 * Removes a given view from its current parent. 1000 * 1001 * @param view the view to remove from its parent 1002 * 1003 * @return a Result with {@link Status#SUCCESS} or 1004 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 1005 * adding views. 1006 */ 1007 private Result removeView(ViewGroup parent, View view) { 1008 try { 1009 parent.removeView(view); 1010 return SUCCESS.createResult(); 1011 } catch (UnsupportedOperationException e) { 1012 // looks like this is a view class that doesn't support children manipulation! 1013 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 1014 } 1015 } 1016 1017 1018 private void findBackground(RenderResources resources) { 1019 if (getParams().isBgColorOverridden() == false) { 1020 mWindowBackground = resources.findItemInTheme("windowBackground", 1021 true /*isFrameworkAttr*/); 1022 if (mWindowBackground != null) { 1023 mWindowBackground = resources.resolveResValue(mWindowBackground); 1024 } 1025 } 1026 } 1027 1028 private boolean hasSoftwareButtons() { 1029 return getParams().getHardwareConfig().hasSoftwareButtons(); 1030 } 1031 1032 private void findStatusBar(RenderResources resources, DisplayMetrics metrics) { 1033 boolean windowFullscreen = getBooleanThemeValue(resources, 1034 "windowFullscreen", false /*defaultValue*/); 1035 1036 if (windowFullscreen == false && mWindowIsFloating == false) { 1037 // default value 1038 mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT; 1039 1040 // get the real value 1041 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1042 "status_bar_height"); 1043 1044 if (value != null) { 1045 TypedValue typedValue = ResourceHelper.getValue("status_bar_height", 1046 value.getValue(), true /*requireUnit*/); 1047 if (typedValue != null) { 1048 // compute the pixel value based on the display metrics 1049 mStatusBarSize = (int)typedValue.getDimension(metrics); 1050 } 1051 } 1052 } 1053 } 1054 1055 private void findActionBar(RenderResources resources, DisplayMetrics metrics) { 1056 if (mWindowIsFloating) { 1057 return; 1058 } 1059 1060 boolean windowActionBar = getBooleanThemeValue(resources, 1061 "windowActionBar", true /*defaultValue*/); 1062 1063 // if there's a value and it's false (default is true) 1064 if (windowActionBar) { 1065 1066 // default size of the window title bar 1067 mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1068 1069 // get value from the theme. 1070 ResourceValue value = resources.findItemInTheme("actionBarSize", 1071 true /*isFrameworkAttr*/); 1072 1073 // resolve it 1074 value = resources.resolveResValue(value); 1075 1076 if (value != null) { 1077 // get the numerical value, if available 1078 TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(), 1079 true /*requireUnit*/); 1080 if (typedValue != null) { 1081 // compute the pixel value based on the display metrics 1082 mActionBarSize = (int)typedValue.getDimension(metrics); 1083 } 1084 } 1085 } else { 1086 // action bar overrides title bar so only look for this one if action bar is hidden 1087 boolean windowNoTitle = getBooleanThemeValue(resources, 1088 "windowNoTitle", false /*defaultValue*/); 1089 1090 if (windowNoTitle == false) { 1091 1092 // default size of the window title bar 1093 mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1094 1095 // get value from the theme. 1096 ResourceValue value = resources.findItemInTheme("windowTitleSize", 1097 true /*isFrameworkAttr*/); 1098 1099 // resolve it 1100 value = resources.resolveResValue(value); 1101 1102 if (value != null) { 1103 // get the numerical value, if available 1104 TypedValue typedValue = ResourceHelper.getValue("windowTitleSize", 1105 value.getValue(), true /*requireUnit*/); 1106 if (typedValue != null) { 1107 // compute the pixel value based on the display metrics 1108 mTitleBarSize = (int)typedValue.getDimension(metrics); 1109 } 1110 } 1111 } 1112 1113 } 1114 } 1115 1116 private void findNavigationBar(RenderResources resources, DisplayMetrics metrics) { 1117 if (hasSoftwareButtons() && mWindowIsFloating == false) { 1118 1119 // default value 1120 mNavigationBarSize = 48; // ?? 1121 1122 HardwareConfig hardwareConfig = getParams().getHardwareConfig(); 1123 1124 boolean barOnBottom = true; 1125 1126 if (hardwareConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { 1127 // compute the dp of the screen. 1128 int shortSize = hardwareConfig.getScreenHeight(); 1129 1130 // compute in dp 1131 int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / hardwareConfig.getDensity().getDpiValue(); 1132 1133 if (shortSizeDp < 600) { 1134 // 0-599dp: "phone" UI with bar on the side 1135 barOnBottom = false; 1136 } else { 1137 // 600+dp: "tablet" UI with bar on the bottom 1138 barOnBottom = true; 1139 } 1140 } 1141 1142 if (barOnBottom) { 1143 mNavigationBarOrientation = LinearLayout.HORIZONTAL; 1144 } else { 1145 mNavigationBarOrientation = LinearLayout.VERTICAL; 1146 } 1147 1148 // get the real value 1149 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1150 barOnBottom ? "navigation_bar_height" : "navigation_bar_width"); 1151 1152 if (value != null) { 1153 TypedValue typedValue = ResourceHelper.getValue("navigation_bar_height", 1154 value.getValue(), true /*requireUnit*/); 1155 if (typedValue != null) { 1156 // compute the pixel value based on the display metrics 1157 mNavigationBarSize = (int)typedValue.getDimension(metrics); 1158 } 1159 } 1160 } 1161 } 1162 1163 /** 1164 * Looks for a attribute in the current theme. The attribute is in the android 1165 * namespace. 1166 * 1167 * @param resources the render resources 1168 * @param name the name of the attribute 1169 * @param defaultValue the default value. 1170 * @return the value of the attribute or the default one if not found. 1171 */ 1172 private boolean getBooleanThemeValue(RenderResources resources, 1173 String name, boolean defaultValue) { 1174 1175 // get the title bar flag from the current theme. 1176 ResourceValue value = resources.findItemInTheme(name, true /*isFrameworkAttr*/); 1177 1178 // because it may reference something else, we resolve it. 1179 value = resources.resolveResValue(value); 1180 1181 // if there's no value, return the default. 1182 if (value == null || value.getValue() == null) { 1183 return defaultValue; 1184 } 1185 1186 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 1187 } 1188 1189 /** 1190 * Post process on a view hierachy that was just inflated. 1191 * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the 1192 * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically 1193 * based on the content of the {@link FrameLayout}. 1194 * @param view the root view to process. 1195 * @param projectCallback callback to the project. 1196 */ 1197 private void postInflateProcess(View view, IProjectCallback projectCallback) 1198 throws PostInflateException { 1199 if (view instanceof TabHost) { 1200 setupTabHost((TabHost)view, projectCallback); 1201 } else if (view instanceof QuickContactBadge) { 1202 QuickContactBadge badge = (QuickContactBadge) view; 1203 badge.setImageToDefault(); 1204 } else if (view instanceof AdapterView<?>) { 1205 // get the view ID. 1206 int id = view.getId(); 1207 1208 BridgeContext context = getContext(); 1209 1210 // get a ResourceReference from the integer ID. 1211 ResourceReference listRef = context.resolveId(id); 1212 1213 if (listRef != null) { 1214 SessionParams params = getParams(); 1215 AdapterBinding binding = params.getAdapterBindings().get(listRef); 1216 1217 // if there was no adapter binding, trying to get it from the call back. 1218 if (binding == null) { 1219 binding = params.getProjectCallback().getAdapterBinding(listRef, 1220 context.getViewKey(view), view); 1221 } 1222 1223 if (binding != null) { 1224 1225 if (view instanceof AbsListView) { 1226 if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && 1227 view instanceof ListView) { 1228 ListView list = (ListView) view; 1229 1230 boolean skipCallbackParser = false; 1231 1232 int count = binding.getHeaderCount(); 1233 for (int i = 0 ; i < count ; i++) { 1234 Pair<View, Boolean> pair = context.inflateView( 1235 binding.getHeaderAt(i), 1236 list, false /*attachToRoot*/, skipCallbackParser); 1237 if (pair.getFirst() != null) { 1238 list.addHeaderView(pair.getFirst()); 1239 } 1240 1241 skipCallbackParser |= pair.getSecond(); 1242 } 1243 1244 count = binding.getFooterCount(); 1245 for (int i = 0 ; i < count ; i++) { 1246 Pair<View, Boolean> pair = context.inflateView( 1247 binding.getFooterAt(i), 1248 list, false /*attachToRoot*/, skipCallbackParser); 1249 if (pair.getFirst() != null) { 1250 list.addFooterView(pair.getFirst()); 1251 } 1252 1253 skipCallbackParser |= pair.getSecond(); 1254 } 1255 } 1256 1257 if (view instanceof ExpandableListView) { 1258 ((ExpandableListView) view).setAdapter( 1259 new FakeExpandableAdapter( 1260 listRef, binding, params.getProjectCallback())); 1261 } else { 1262 ((AbsListView) view).setAdapter( 1263 new FakeAdapter( 1264 listRef, binding, params.getProjectCallback())); 1265 } 1266 } else if (view instanceof AbsSpinner) { 1267 ((AbsSpinner) view).setAdapter( 1268 new FakeAdapter( 1269 listRef, binding, params.getProjectCallback())); 1270 } 1271 } 1272 } 1273 } else if (view instanceof ViewGroup) { 1274 ViewGroup group = (ViewGroup)view; 1275 final int count = group.getChildCount(); 1276 for (int c = 0 ; c < count ; c++) { 1277 View child = group.getChildAt(c); 1278 postInflateProcess(child, projectCallback); 1279 } 1280 } 1281 } 1282 1283 /** 1284 * Sets up a {@link TabHost} object. 1285 * @param tabHost the TabHost to setup. 1286 * @param projectCallback The project callback object to access the project R class. 1287 * @throws PostInflateException 1288 */ 1289 private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback) 1290 throws PostInflateException { 1291 // look for the TabWidget, and the FrameLayout. They have their own specific names 1292 View v = tabHost.findViewById(android.R.id.tabs); 1293 1294 if (v == null) { 1295 throw new PostInflateException( 1296 "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); 1297 } 1298 1299 if ((v instanceof TabWidget) == false) { 1300 throw new PostInflateException(String.format( 1301 "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + 1302 "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); 1303 } 1304 1305 v = tabHost.findViewById(android.R.id.tabcontent); 1306 1307 if (v == null) { 1308 // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty) 1309 throw new PostInflateException( 1310 "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); 1311 } 1312 1313 if ((v instanceof FrameLayout) == false) { 1314 throw new PostInflateException(String.format( 1315 "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + 1316 "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); 1317 } 1318 1319 FrameLayout content = (FrameLayout)v; 1320 1321 // now process the content of the framelayout and dynamically create tabs for it. 1322 final int count = content.getChildCount(); 1323 1324 // this must be called before addTab() so that the TabHost searches its TabWidget 1325 // and FrameLayout. 1326 tabHost.setup(); 1327 1328 if (count == 0) { 1329 // Create a dummy child to get a single tab 1330 TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label", 1331 tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details)) 1332 .setContent(new TabHost.TabContentFactory() { 1333 @Override 1334 public View createTabContent(String tag) { 1335 return new LinearLayout(getContext()); 1336 } 1337 }); 1338 tabHost.addTab(spec); 1339 return; 1340 } else { 1341 // for each child of the framelayout, add a new TabSpec 1342 for (int i = 0 ; i < count ; i++) { 1343 View child = content.getChildAt(i); 1344 String tabSpec = String.format("tab_spec%d", i+1); 1345 int id = child.getId(); 1346 Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id); 1347 String name; 1348 if (resource != null) { 1349 name = resource.getSecond(); 1350 } else { 1351 name = String.format("Tab %d", i+1); // default name if id is unresolved. 1352 } 1353 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); 1354 } 1355 } 1356 } 1357 1358 /** 1359 * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the 1360 * bounds of all the views. 1361 * 1362 * @param view the root View 1363 * @param offset an offset for the view bounds. 1364 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 1365 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 1366 * content frame. 1367 * 1368 * @return {@code ViewInfo} containing the bounds of the view and it children otherwise. 1369 */ 1370 private ViewInfo visit(View view, int offset, boolean setExtendedInfo, 1371 boolean isContentFrame) { 1372 ViewInfo result = createViewInfo(view, offset, setExtendedInfo, isContentFrame); 1373 1374 if (view instanceof ViewGroup) { 1375 ViewGroup group = ((ViewGroup) view); 1376 result.setChildren(visitAllChildren(group, isContentFrame ? 0 : offset, 1377 setExtendedInfo, isContentFrame)); 1378 } 1379 return result; 1380 } 1381 1382 /** 1383 * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo} 1384 * containing the bounds of all the views. It also initializes the {@link mViewInfoList} with 1385 * the children of the {@code mContentRoot}. 1386 * 1387 * @param viewGroup the root View 1388 * @param offset an offset from the top for the content view frame. 1389 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 1390 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 1391 * content frame. {@code false} if the {@code ViewInfo} to be created is 1392 * part of the system decor. 1393 */ 1394 private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset, 1395 boolean setExtendedInfo, boolean isContentFrame) { 1396 if (viewGroup == null) { 1397 return null; 1398 } 1399 1400 if (!isContentFrame) { 1401 offset += viewGroup.getTop(); 1402 } 1403 1404 int childCount = viewGroup.getChildCount(); 1405 if (viewGroup == mContentRoot) { 1406 List<ViewInfo> childrenWithoutOffset = new ArrayList<ViewInfo>(childCount); 1407 List<ViewInfo> childrenWithOffset = new ArrayList<ViewInfo>(childCount); 1408 for (int i = 0; i < childCount; i++) { 1409 ViewInfo[] childViewInfo = visitContentRoot(viewGroup.getChildAt(i), offset, 1410 setExtendedInfo); 1411 childrenWithoutOffset.add(childViewInfo[0]); 1412 childrenWithOffset.add(childViewInfo[1]); 1413 } 1414 mViewInfoList = childrenWithOffset; 1415 return childrenWithoutOffset; 1416 } else { 1417 List<ViewInfo> children = new ArrayList<ViewInfo>(childCount); 1418 for (int i = 0; i < childCount; i++) { 1419 children.add(visit(viewGroup.getChildAt(i), offset, setExtendedInfo, 1420 isContentFrame)); 1421 } 1422 return children; 1423 } 1424 } 1425 1426 /** 1427 * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the 1428 * bounds of all the views. It returns two {@code ViewInfo} objects with the same children, 1429 * one with the {@code offset} and other without the {@code offset}. The offset is needed to 1430 * get the right bounds if the {@code ViewInfo} hierarchy is accessed from 1431 * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the 1432 * offset is not needed. 1433 * 1434 * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at 1435 * index 1 is with the offset. 1436 */ 1437 private ViewInfo[] visitContentRoot(View view, int offset, boolean setExtendedInfo) { 1438 ViewInfo[] result = new ViewInfo[2]; 1439 if (view == null) { 1440 return result; 1441 } 1442 1443 result[0] = createViewInfo(view, 0, setExtendedInfo, true); 1444 result[1] = createViewInfo(view, offset, setExtendedInfo, true); 1445 if (view instanceof ViewGroup) { 1446 List<ViewInfo> children = visitAllChildren((ViewGroup) view, 0, setExtendedInfo, true); 1447 result[0].setChildren(children); 1448 result[1].setChildren(children); 1449 } 1450 return result; 1451 } 1452 1453 /** 1454 * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children 1455 * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not 1456 * set. 1457 * @param offset an offset for the view bounds. Used only if view is part of the content frame. 1458 */ 1459 private ViewInfo createViewInfo(View view, int offset, boolean setExtendedInfo, 1460 boolean isContentFrame) { 1461 if (view == null) { 1462 return null; 1463 } 1464 1465 ViewInfo result; 1466 if (isContentFrame) { 1467 result = new ViewInfo(view.getClass().getName(), 1468 getContext().getViewKey(view), 1469 view.getLeft(), view.getTop() + offset, view.getRight(), 1470 view.getBottom() + offset, view, view.getLayoutParams()); 1471 1472 } else { 1473 result = new SystemViewInfo(view.getClass().getName(), 1474 getContext().getViewKey(view), 1475 view.getLeft(), view.getTop(), view.getRight(), 1476 view.getBottom(), view, view.getLayoutParams()); 1477 } 1478 1479 if (setExtendedInfo) { 1480 MarginLayoutParams marginParams = null; 1481 LayoutParams params = view.getLayoutParams(); 1482 if (params instanceof MarginLayoutParams) { 1483 marginParams = (MarginLayoutParams) params; 1484 } 1485 result.setExtendedInfo(view.getBaseline(), 1486 marginParams != null ? marginParams.leftMargin : 0, 1487 marginParams != null ? marginParams.topMargin : 0, 1488 marginParams != null ? marginParams.rightMargin : 0, 1489 marginParams != null ? marginParams.bottomMargin : 0); 1490 } 1491 1492 return result; 1493 } 1494 1495 private void invalidateRenderingSize() { 1496 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1497 } 1498 1499 /** 1500 * Creates the status bar with wifi and battery icons. 1501 */ 1502 private StatusBar createStatusBar(BridgeContext context, Density density, int direction, 1503 boolean isRtlSupported) throws XmlPullParserException { 1504 StatusBar statusBar = new StatusBar(context, density, 1505 direction, isRtlSupported); 1506 statusBar.setLayoutParams( 1507 new LinearLayout.LayoutParams( 1508 LayoutParams.MATCH_PARENT, mStatusBarSize)); 1509 return statusBar; 1510 } 1511 1512 /** 1513 * Creates the navigation bar with back, home and recent buttons. 1514 * 1515 * @param isRtl true if the current locale is right-to-left 1516 * @param isRtlSupported true is the project manifest declares that the application 1517 * is RTL aware. 1518 */ 1519 private NavigationBar createNavigationBar(BridgeContext context, Density density, 1520 boolean isRtl, boolean isRtlSupported) throws XmlPullParserException { 1521 NavigationBar navigationBar = new NavigationBar(context, 1522 density, mNavigationBarOrientation, isRtl, 1523 isRtlSupported); 1524 if (mNavigationBarOrientation == LinearLayout.VERTICAL) { 1525 navigationBar.setLayoutParams(new LinearLayout.LayoutParams(mNavigationBarSize, 1526 LayoutParams.MATCH_PARENT)); 1527 } else { 1528 navigationBar.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 1529 mNavigationBarSize)); 1530 } 1531 return navigationBar; 1532 } 1533 1534 private TitleBar createTitleBar(BridgeContext context, Density density, String title) 1535 throws XmlPullParserException { 1536 TitleBar titleBar = new TitleBar(context, density, title); 1537 titleBar.setLayoutParams( 1538 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, mTitleBarSize)); 1539 return titleBar; 1540 } 1541 1542 /** 1543 * Creates the action bar. Also queries the project callback for missing information. 1544 */ 1545 private ActionBarLayout createActionBar(BridgeContext context, SessionParams params) 1546 throws XmlPullParserException { 1547 ActionBarLayout actionBar = new ActionBarLayout(context, params); 1548 actionBar.setLayoutParams(new LinearLayout.LayoutParams( 1549 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 1550 return actionBar; 1551 } 1552 1553 public BufferedImage getImage() { 1554 return mImage; 1555 } 1556 1557 public boolean isAlphaChannelImage() { 1558 return mIsAlphaChannelImage; 1559 } 1560 1561 public List<ViewInfo> getViewInfos() { 1562 return mViewInfoList; 1563 } 1564 1565 public List<ViewInfo> getSystemViewInfos() { 1566 return mSystemViewInfoList; 1567 } 1568 1569 public Map<String, String> getDefaultProperties(Object viewObject) { 1570 return getContext().getDefaultPropMap(viewObject); 1571 } 1572 1573 public void setScene(RenderSession session) { 1574 mScene = session; 1575 } 1576 1577 public RenderSession getSession() { 1578 return mScene; 1579 } 1580} 1581