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