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