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