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