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