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