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