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