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