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