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