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