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