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