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