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