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