RenderSessionImpl.java revision 01cdf8b135be3bf67b9386dc929109c3db82c730
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.StyleResourceValue; 40import com.android.ide.common.rendering.api.ViewInfo; 41import com.android.ide.common.rendering.api.ViewType; 42import com.android.internal.util.XmlUtils; 43import com.android.internal.view.menu.ActionMenuItemView; 44import com.android.internal.view.menu.BridgeMenuItemImpl; 45import com.android.internal.view.menu.IconMenuItemView; 46import com.android.internal.view.menu.ListMenuItemView; 47import com.android.internal.view.menu.MenuItemImpl; 48import com.android.internal.view.menu.MenuView; 49import com.android.layoutlib.bridge.Bridge; 50import com.android.layoutlib.bridge.android.BridgeContext; 51import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; 52import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 53import com.android.layoutlib.bridge.android.SessionParamsFlags; 54import com.android.layoutlib.bridge.bars.BridgeActionBar; 55import com.android.layoutlib.bridge.bars.AppCompatActionBar; 56import com.android.layoutlib.bridge.bars.Config; 57import com.android.layoutlib.bridge.bars.NavigationBar; 58import com.android.layoutlib.bridge.bars.StatusBar; 59import com.android.layoutlib.bridge.bars.TitleBar; 60import com.android.layoutlib.bridge.bars.FrameworkActionBar; 61import com.android.layoutlib.bridge.impl.binding.FakeAdapter; 62import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; 63import com.android.resources.Density; 64import com.android.resources.ResourceType; 65import com.android.resources.ScreenOrientation; 66import com.android.util.Pair; 67 68import org.xmlpull.v1.XmlPullParserException; 69 70import android.animation.AnimationThread; 71import android.animation.Animator; 72import android.animation.AnimatorInflater; 73import android.animation.LayoutTransition; 74import android.animation.LayoutTransition.TransitionListener; 75import android.app.Fragment_Delegate; 76import android.graphics.Bitmap; 77import android.graphics.Bitmap_Delegate; 78import android.graphics.Canvas; 79import android.graphics.drawable.Drawable; 80import android.preference.Preference_Delegate; 81import android.util.DisplayMetrics; 82import android.util.TypedValue; 83import android.view.AttachInfo_Accessor; 84import android.view.BridgeInflater; 85import android.view.IWindowManager; 86import android.view.IWindowManagerImpl; 87import android.view.Surface; 88import android.view.View; 89import android.view.View.MeasureSpec; 90import android.view.ViewGroup; 91import android.view.ViewGroup.LayoutParams; 92import android.view.ViewGroup.MarginLayoutParams; 93import android.view.ViewParent; 94import android.view.WindowManagerGlobal_Delegate; 95import android.widget.AbsListView; 96import android.widget.AbsSpinner; 97import android.widget.ActionMenuView; 98import android.widget.AdapterView; 99import android.widget.ExpandableListView; 100import android.widget.FrameLayout; 101import android.widget.LinearLayout; 102import android.widget.ListView; 103import android.widget.QuickContactBadge; 104import android.widget.TabHost; 105import android.widget.TabHost.TabSpec; 106import android.widget.TabWidget; 107 108import java.awt.AlphaComposite; 109import java.awt.Color; 110import java.awt.Graphics2D; 111import java.awt.image.BufferedImage; 112import java.util.ArrayList; 113import java.util.List; 114import 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 */ 123public 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 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 */ 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 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 */ 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 */ 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 555 // When disableBitmapCaching is true, we do not reuse mImage and 556 // we create a new one in every render. 557 // This is useful when mImage is just a wrapper of Graphics2D so 558 // it doesn't get cached. 559 boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag( 560 SessionParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING)); 561 if (newRenderSize || mCanvas == null || disableBitmapCaching) { 562 if (params.getImageFactory() != null) { 563 mImage = params.getImageFactory().getImage( 564 mMeasuredScreenWidth, 565 mMeasuredScreenHeight); 566 } else { 567 mImage = new BufferedImage( 568 mMeasuredScreenWidth, 569 mMeasuredScreenHeight, 570 BufferedImage.TYPE_INT_ARGB); 571 newImage = true; 572 } 573 574 if (params.isBgColorOverridden()) { 575 // since we override the content, it's the same as if it was a new image. 576 newImage = true; 577 Graphics2D gc = mImage.createGraphics(); 578 gc.setColor(new Color(params.getOverrideBgColor(), true)); 579 gc.setComposite(AlphaComposite.Src); 580 gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 581 gc.dispose(); 582 } 583 584 // create an Android bitmap around the BufferedImage 585 Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, 586 true /*isMutable*/, hardwareConfig.getDensity()); 587 588 if (mCanvas == null) { 589 // create a Canvas around the Android bitmap 590 mCanvas = new Canvas(bitmap); 591 } else { 592 mCanvas.setBitmap(bitmap); 593 } 594 mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue()); 595 } 596 597 if (freshRender && !newImage) { 598 Graphics2D gc = mImage.createGraphics(); 599 gc.setComposite(AlphaComposite.Src); 600 601 gc.setColor(new Color(0x00000000, true)); 602 gc.fillRect(0, 0, 603 mMeasuredScreenWidth, mMeasuredScreenHeight); 604 605 // done 606 gc.dispose(); 607 } 608 609 mViewRoot.draw(mCanvas); 610 } 611 612 mSystemViewInfoList = visitAllChildren(mViewRoot, 0, params.getExtendedViewInfoMode(), 613 false); 614 615 // success! 616 return SUCCESS.createResult(); 617 } catch (Throwable e) { 618 // get the real cause of the exception. 619 Throwable t = e; 620 while (t.getCause() != null) { 621 t = t.getCause(); 622 } 623 624 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 625 } 626 } 627 628 /** 629 * Executes {@link View#measure(int, int)} on a given view with the given parameters (used 630 * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}. 631 * 632 * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height) 633 * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}). 634 * 635 * @param viewToMeasure the view on which to execute measure(). 636 * @param measuredView if non null, the view to query for its measured width/height. 637 * @param width the width to use in the MeasureSpec. 638 * @param widthMode the MeasureSpec mode to use for the width. 639 * @param height the height to use in the MeasureSpec. 640 * @param heightMode the MeasureSpec mode to use for the height. 641 * @return the measured width/height if measuredView is non-null, null otherwise. 642 */ 643 @SuppressWarnings("deprecation") // For the use of Pair 644 private Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView, 645 int width, int widthMode, int height, int heightMode) { 646 int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode); 647 int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode); 648 viewToMeasure.measure(w_spec, h_spec); 649 650 if (measuredView != null) { 651 return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight()); 652 } 653 654 return null; 655 } 656 657 /** 658 * Animate an object 659 * <p> 660 * {@link #acquire(long)} must have been called before this. 661 * 662 * @throws IllegalStateException if the current context is different than the one owned by 663 * the scene, or if {@link #acquire(long)} was not called. 664 * 665 * @see RenderSession#animate(Object, String, boolean, IAnimationListener) 666 */ 667 public Result animate(Object targetObject, String animationName, 668 boolean isFrameworkAnimation, IAnimationListener listener) { 669 checkLock(); 670 671 BridgeContext context = getContext(); 672 673 // find the animation file. 674 ResourceValue animationResource; 675 int animationId = 0; 676 if (isFrameworkAnimation) { 677 animationResource = context.getRenderResources().getFrameworkResource( 678 ResourceType.ANIMATOR, animationName); 679 if (animationResource != null) { 680 animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName); 681 } 682 } else { 683 animationResource = context.getRenderResources().getProjectResource( 684 ResourceType.ANIMATOR, animationName); 685 if (animationResource != null) { 686 animationId = context.getProjectCallback().getResourceId( 687 ResourceType.ANIMATOR, animationName); 688 } 689 } 690 691 if (animationResource != null) { 692 try { 693 Animator anim = AnimatorInflater.loadAnimator(context, animationId); 694 if (anim != null) { 695 anim.setTarget(targetObject); 696 697 new PlayAnimationThread(anim, this, animationName, listener).start(); 698 699 return SUCCESS.createResult(); 700 } 701 } catch (Exception e) { 702 // get the real cause of the exception. 703 Throwable t = e; 704 while (t.getCause() != null) { 705 t = t.getCause(); 706 } 707 708 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 709 } 710 } 711 712 return ERROR_ANIM_NOT_FOUND.createResult(); 713 } 714 715 /** 716 * Insert a new child into an existing parent. 717 * <p> 718 * {@link #acquire(long)} must have been called before this. 719 * 720 * @throws IllegalStateException if the current context is different than the one owned by 721 * the scene, or if {@link #acquire(long)} was not called. 722 * 723 * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener) 724 */ 725 public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, 726 final int index, IAnimationListener listener) { 727 checkLock(); 728 729 BridgeContext context = getContext(); 730 731 // create a block parser for the XML 732 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 733 childXml, context, false /* platformResourceFlag */); 734 735 // inflate the child without adding it to the root since we want to control where it'll 736 // get added. We do pass the parentView however to ensure that the layoutParams will 737 // be created correctly. 738 final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); 739 blockParser.ensurePopped(); 740 741 invalidateRenderingSize(); 742 743 if (listener != null) { 744 new AnimationThread(this, "insertChild", listener) { 745 746 @Override 747 public Result preAnimation() { 748 parentView.setLayoutTransition(new LayoutTransition()); 749 return addView(parentView, child, index); 750 } 751 752 @Override 753 public void postAnimation() { 754 parentView.setLayoutTransition(null); 755 } 756 }.start(); 757 758 // always return success since the real status will come through the listener. 759 return SUCCESS.createResult(child); 760 } 761 762 // add it to the parentView in the correct location 763 Result result = addView(parentView, child, index); 764 if (!result.isSuccess()) { 765 return result; 766 } 767 768 result = render(false /*freshRender*/); 769 if (result.isSuccess()) { 770 result = result.getCopyWithData(child); 771 } 772 773 return result; 774 } 775 776 /** 777 * Adds a given view to a given parent at a given index. 778 * 779 * @param parent the parent to receive the view 780 * @param view the view to add to the parent 781 * @param index the index where to do the add. 782 * 783 * @return a Result with {@link Status#SUCCESS} or 784 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 785 * adding views. 786 */ 787 private Result addView(ViewGroup parent, View view, int index) { 788 try { 789 parent.addView(view, index); 790 return SUCCESS.createResult(); 791 } catch (UnsupportedOperationException e) { 792 // looks like this is a view class that doesn't support children manipulation! 793 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 794 } 795 } 796 797 /** 798 * Moves a view to a new parent at a given location 799 * <p> 800 * {@link #acquire(long)} must have been called before this. 801 * 802 * @throws IllegalStateException if the current context is different than the one owned by 803 * the scene, or if {@link #acquire(long)} was not called. 804 * 805 * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener) 806 */ 807 public Result moveChild(final ViewGroup newParentView, final View childView, final int index, 808 Map<String, String> layoutParamsMap, final IAnimationListener listener) { 809 checkLock(); 810 811 invalidateRenderingSize(); 812 813 LayoutParams layoutParams = null; 814 if (layoutParamsMap != null) { 815 // need to create a new LayoutParams object for the new parent. 816 layoutParams = newParentView.generateLayoutParams( 817 new BridgeLayoutParamsMapAttributes(layoutParamsMap)); 818 } 819 820 // get the current parent of the view that needs to be moved. 821 final ViewGroup previousParent = (ViewGroup) childView.getParent(); 822 823 if (listener != null) { 824 final LayoutParams params = layoutParams; 825 826 // there is no support for animating views across layouts, so in case the new and old 827 // parent views are different we fake the animation through a no animation thread. 828 if (previousParent != newParentView) { 829 new Thread("not animated moveChild") { 830 @Override 831 public void run() { 832 Result result = moveView(previousParent, newParentView, childView, index, 833 params); 834 if (!result.isSuccess()) { 835 listener.done(result); 836 } 837 838 // ready to do the work, acquire the scene. 839 result = acquire(250); 840 if (!result.isSuccess()) { 841 listener.done(result); 842 return; 843 } 844 845 try { 846 result = render(false /*freshRender*/); 847 if (result.isSuccess()) { 848 listener.onNewFrame(RenderSessionImpl.this.getSession()); 849 } 850 } finally { 851 release(); 852 } 853 854 listener.done(result); 855 } 856 }.start(); 857 } else { 858 new AnimationThread(this, "moveChild", listener) { 859 860 @Override 861 public Result preAnimation() { 862 // set up the transition for the parent. 863 LayoutTransition transition = new LayoutTransition(); 864 previousParent.setLayoutTransition(transition); 865 866 // tweak the animation durations and start delays (to match the duration of 867 // animation playing just before). 868 // Note: Cannot user Animation.setDuration() directly. Have to set it 869 // on the LayoutTransition. 870 transition.setDuration(LayoutTransition.DISAPPEARING, 100); 871 // CHANGE_DISAPPEARING plays after DISAPPEARING 872 transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); 873 874 transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); 875 876 transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); 877 // CHANGE_APPEARING plays after CHANGE_APPEARING 878 transition.setStartDelay(LayoutTransition.APPEARING, 100); 879 880 transition.setDuration(LayoutTransition.APPEARING, 100); 881 882 return moveView(previousParent, newParentView, childView, index, params); 883 } 884 885 @Override 886 public void postAnimation() { 887 previousParent.setLayoutTransition(null); 888 newParentView.setLayoutTransition(null); 889 } 890 }.start(); 891 } 892 893 // always return success since the real status will come through the listener. 894 return SUCCESS.createResult(layoutParams); 895 } 896 897 Result result = moveView(previousParent, newParentView, childView, index, layoutParams); 898 if (!result.isSuccess()) { 899 return result; 900 } 901 902 result = render(false /*freshRender*/); 903 if (layoutParams != null && result.isSuccess()) { 904 result = result.getCopyWithData(layoutParams); 905 } 906 907 return result; 908 } 909 910 /** 911 * Moves a View from its current parent to a new given parent at a new given location, with 912 * an optional new {@link LayoutParams} instance 913 * 914 * @param previousParent the previous parent, still owning the child at the time of the call. 915 * @param newParent the new parent 916 * @param movedView the view to move 917 * @param index the new location in the new parent 918 * @param params an option (can be null) {@link LayoutParams} instance. 919 * 920 * @return a Result with {@link Status#SUCCESS} or 921 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 922 * adding views. 923 */ 924 private Result moveView(ViewGroup previousParent, final ViewGroup newParent, 925 final View movedView, final int index, final LayoutParams params) { 926 try { 927 // check if there is a transition on the previousParent. 928 LayoutTransition previousTransition = previousParent.getLayoutTransition(); 929 if (previousTransition != null) { 930 // in this case there is an animation. This means we have to wait for the child's 931 // parent reference to be null'ed out so that we can add it to the new parent. 932 // It is technically removed right before the DISAPPEARING animation is done (if 933 // the animation of this type is not null, otherwise it's after which is impossible 934 // to handle). 935 // Because there is no move animation, if the new parent is the same as the old 936 // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before 937 // adding the child or the child will appear in its new location before the 938 // other children have made room for it. 939 940 // add a listener to the transition to be notified of the actual removal. 941 previousTransition.addTransitionListener(new TransitionListener() { 942 private int mChangeDisappearingCount = 0; 943 944 @Override 945 public void startTransition(LayoutTransition transition, ViewGroup container, 946 View view, int transitionType) { 947 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 948 mChangeDisappearingCount++; 949 } 950 } 951 952 @Override 953 public void endTransition(LayoutTransition transition, ViewGroup container, 954 View view, int transitionType) { 955 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 956 mChangeDisappearingCount--; 957 } 958 959 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && 960 mChangeDisappearingCount == 0) { 961 // add it to the parentView in the correct location 962 if (params != null) { 963 newParent.addView(movedView, index, params); 964 } else { 965 newParent.addView(movedView, index); 966 } 967 } 968 } 969 }); 970 971 // remove the view from the current parent. 972 previousParent.removeView(movedView); 973 974 // and return since adding the view to the new parent is done in the listener. 975 return SUCCESS.createResult(); 976 } else { 977 // standard code with no animation. pretty simple. 978 previousParent.removeView(movedView); 979 980 // add it to the parentView in the correct location 981 if (params != null) { 982 newParent.addView(movedView, index, params); 983 } else { 984 newParent.addView(movedView, index); 985 } 986 987 return SUCCESS.createResult(); 988 } 989 } catch (UnsupportedOperationException e) { 990 // looks like this is a view class that doesn't support children manipulation! 991 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 992 } 993 } 994 995 /** 996 * Removes a child from its current parent. 997 * <p> 998 * {@link #acquire(long)} must have been called before this. 999 * 1000 * @throws IllegalStateException if the current context is different than the one owned by 1001 * the scene, or if {@link #acquire(long)} was not called. 1002 * 1003 * @see RenderSession#removeChild(Object, IAnimationListener) 1004 */ 1005 public Result removeChild(final View childView, IAnimationListener listener) { 1006 checkLock(); 1007 1008 invalidateRenderingSize(); 1009 1010 final ViewGroup parent = (ViewGroup) childView.getParent(); 1011 1012 if (listener != null) { 1013 new AnimationThread(this, "moveChild", listener) { 1014 1015 @Override 1016 public Result preAnimation() { 1017 parent.setLayoutTransition(new LayoutTransition()); 1018 return removeView(parent, childView); 1019 } 1020 1021 @Override 1022 public void postAnimation() { 1023 parent.setLayoutTransition(null); 1024 } 1025 }.start(); 1026 1027 // always return success since the real status will come through the listener. 1028 return SUCCESS.createResult(); 1029 } 1030 1031 Result result = removeView(parent, childView); 1032 if (!result.isSuccess()) { 1033 return result; 1034 } 1035 1036 return render(false /*freshRender*/); 1037 } 1038 1039 /** 1040 * Removes a given view from its current parent. 1041 * 1042 * @param view the view to remove from its parent 1043 * 1044 * @return a Result with {@link Status#SUCCESS} or 1045 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 1046 * adding views. 1047 */ 1048 private Result removeView(ViewGroup parent, View view) { 1049 try { 1050 parent.removeView(view); 1051 return SUCCESS.createResult(); 1052 } catch (UnsupportedOperationException e) { 1053 // looks like this is a view class that doesn't support children manipulation! 1054 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 1055 } 1056 } 1057 1058 1059 private void findBackground(RenderResources resources) { 1060 if (!getParams().isBgColorOverridden()) { 1061 mWindowBackground = resources.findItemInTheme("windowBackground", 1062 true /*isFrameworkAttr*/); 1063 if (mWindowBackground != null) { 1064 mWindowBackground = resources.resolveResValue(mWindowBackground); 1065 } 1066 } 1067 } 1068 1069 private boolean hasSoftwareButtons() { 1070 return getParams().getHardwareConfig().hasSoftwareButtons(); 1071 } 1072 1073 private void findStatusBar(RenderResources resources, DisplayMetrics metrics) { 1074 boolean windowFullscreen = getBooleanThemeValue(resources, 1075 "windowFullscreen", false, !isThemeAppCompat(resources)); 1076 1077 if (!windowFullscreen && !mWindowIsFloating) { 1078 // default value 1079 mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT; 1080 1081 // get the real value 1082 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1083 "status_bar_height"); 1084 1085 if (value != null) { 1086 TypedValue typedValue = ResourceHelper.getValue("status_bar_height", 1087 value.getValue(), true /*requireUnit*/); 1088 if (typedValue != null) { 1089 // compute the pixel value based on the display metrics 1090 mStatusBarSize = (int)typedValue.getDimension(metrics); 1091 } 1092 } 1093 } 1094 } 1095 1096 private void findActionBar(RenderResources resources, DisplayMetrics metrics) { 1097 if (mWindowIsFloating) { 1098 return; 1099 } 1100 1101 boolean windowActionBar = getBooleanThemeValue(resources, 1102 "windowActionBar", true, !isThemeAppCompat(resources)); 1103 1104 // if there's a value and it's false (default is true) 1105 if (windowActionBar) { 1106 1107 // default size of the window title bar 1108 mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1109 1110 // get value from the theme. 1111 ResourceValue value = resources.findItemInTheme("actionBarSize", 1112 true /*isFrameworkAttr*/); 1113 1114 // resolve it 1115 value = resources.resolveResValue(value); 1116 1117 if (value != null) { 1118 // get the numerical value, if available 1119 TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(), 1120 true /*requireUnit*/); 1121 if (typedValue != null) { 1122 // compute the pixel value based on the display metrics 1123 mActionBarSize = (int)typedValue.getDimension(metrics); 1124 } 1125 } 1126 } else { 1127 // action bar overrides title bar so only look for this one if action bar is hidden 1128 boolean windowNoTitle = getBooleanThemeValue(resources, 1129 "windowNoTitle", false, !isThemeAppCompat(resources)); 1130 1131 if (!windowNoTitle) { 1132 1133 // default size of the window title bar 1134 mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1135 1136 // get value from the theme. 1137 ResourceValue value = resources.findItemInTheme("windowTitleSize", 1138 true /*isFrameworkAttr*/); 1139 1140 // resolve it 1141 value = resources.resolveResValue(value); 1142 1143 if (value != null) { 1144 // get the numerical value, if available 1145 TypedValue typedValue = ResourceHelper.getValue("windowTitleSize", 1146 value.getValue(), true /*requireUnit*/); 1147 if (typedValue != null) { 1148 // compute the pixel value based on the display metrics 1149 mTitleBarSize = (int)typedValue.getDimension(metrics); 1150 } 1151 } 1152 } 1153 1154 } 1155 } 1156 1157 private void findNavigationBar(RenderResources resources, DisplayMetrics metrics) { 1158 if (hasSoftwareButtons() && !mWindowIsFloating) { 1159 1160 // default value 1161 mNavigationBarSize = 48; // ?? 1162 1163 HardwareConfig hardwareConfig = getParams().getHardwareConfig(); 1164 1165 boolean barOnBottom = true; 1166 1167 if (hardwareConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { 1168 // compute the dp of the screen. 1169 int shortSize = hardwareConfig.getScreenHeight(); 1170 1171 // compute in dp 1172 int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / 1173 hardwareConfig.getDensity().getDpiValue(); 1174 1175 // 0-599dp: "phone" UI with bar on the side 1176 // 600+dp: "tablet" UI with bar on the bottom 1177 barOnBottom = shortSizeDp >= 600; 1178 } 1179 1180 if (barOnBottom) { 1181 mNavigationBarOrientation = LinearLayout.HORIZONTAL; 1182 } else { 1183 mNavigationBarOrientation = LinearLayout.VERTICAL; 1184 } 1185 1186 // get the real value 1187 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1188 barOnBottom ? "navigation_bar_height" : "navigation_bar_width"); 1189 1190 if (value != null) { 1191 TypedValue typedValue = ResourceHelper.getValue("navigation_bar_height", 1192 value.getValue(), true /*requireUnit*/); 1193 if (typedValue != null) { 1194 // compute the pixel value based on the display metrics 1195 mNavigationBarSize = (int)typedValue.getDimension(metrics); 1196 } 1197 } 1198 } 1199 } 1200 1201 private boolean isThemeAppCompat(RenderResources resources) { 1202 // Ideally, we should check if the corresponding activity extends 1203 // android.support.v7.app.ActionBarActivity, and not care about the theme name at all. 1204 if (mIsThemeAppCompat == null) { 1205 StyleResourceValue defaultTheme = resources.getDefaultTheme(); 1206 // We can't simply check for parent using resources.themeIsParentOf() since the 1207 // inheritance structure isn't really what one would expect. The first common parent 1208 // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21). 1209 boolean isThemeAppCompat = false; 1210 for (int i = 0; i < 50; i++) { 1211 // for loop ensures that we don't run into cyclic theme inheritance. 1212 if (defaultTheme.getName().startsWith("Theme.AppCompat")) { 1213 isThemeAppCompat = true; 1214 break; 1215 } 1216 defaultTheme = resources.getParent(defaultTheme); 1217 if (defaultTheme == null) { 1218 break; 1219 } 1220 } 1221 mIsThemeAppCompat = isThemeAppCompat; 1222 } 1223 return mIsThemeAppCompat; 1224 } 1225 1226 /** 1227 * Looks for an attribute in the current theme. 1228 * 1229 * @param resources the render resources 1230 * @param name the name of the attribute 1231 * @param defaultValue the default value. 1232 * @param isFrameworkAttr if the attribute is in android namespace 1233 * @return the value of the attribute or the default one if not found. 1234 */ 1235 private boolean getBooleanThemeValue(RenderResources resources, 1236 String name, boolean defaultValue, boolean isFrameworkAttr) { 1237 1238 ResourceValue value = resources.findItemInTheme(name, isFrameworkAttr); 1239 1240 // because it may reference something else, we resolve it. 1241 value = resources.resolveResValue(value); 1242 1243 // if there's no value, return the default. 1244 if (value == null || value.getValue() == null) { 1245 return defaultValue; 1246 } 1247 1248 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 1249 } 1250 1251 /** 1252 * Post process on a view hierarchy that was just inflated. 1253 * <p/> 1254 * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the 1255 * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically 1256 * based on the content of the {@link FrameLayout}. 1257 * @param view the root view to process. 1258 * @param projectCallback callback to the project. 1259 * @param skip the view and it's children are not processed. 1260 */ 1261 @SuppressWarnings("deprecation") // For the use of Pair 1262 private void postInflateProcess(View view, IProjectCallback projectCallback, View skip) 1263 throws PostInflateException { 1264 if (view == skip) { 1265 return; 1266 } 1267 if (view instanceof TabHost) { 1268 setupTabHost((TabHost) view, projectCallback); 1269 } else if (view instanceof QuickContactBadge) { 1270 QuickContactBadge badge = (QuickContactBadge) view; 1271 badge.setImageToDefault(); 1272 } else if (view instanceof AdapterView<?>) { 1273 // get the view ID. 1274 int id = view.getId(); 1275 1276 BridgeContext context = getContext(); 1277 1278 // get a ResourceReference from the integer ID. 1279 ResourceReference listRef = context.resolveId(id); 1280 1281 if (listRef != null) { 1282 SessionParams params = getParams(); 1283 AdapterBinding binding = params.getAdapterBindings().get(listRef); 1284 1285 // if there was no adapter binding, trying to get it from the call back. 1286 if (binding == null) { 1287 binding = params.getProjectCallback().getAdapterBinding(listRef, 1288 context.getViewKey(view), view); 1289 } 1290 1291 if (binding != null) { 1292 1293 if (view instanceof AbsListView) { 1294 if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && 1295 view instanceof ListView) { 1296 ListView list = (ListView) view; 1297 1298 boolean skipCallbackParser = false; 1299 1300 int count = binding.getHeaderCount(); 1301 for (int i = 0; i < count; i++) { 1302 Pair<View, Boolean> pair = context.inflateView( 1303 binding.getHeaderAt(i), 1304 list, false /*attachToRoot*/, skipCallbackParser); 1305 if (pair.getFirst() != null) { 1306 list.addHeaderView(pair.getFirst()); 1307 } 1308 1309 skipCallbackParser |= pair.getSecond(); 1310 } 1311 1312 count = binding.getFooterCount(); 1313 for (int i = 0; i < count; i++) { 1314 Pair<View, Boolean> pair = context.inflateView( 1315 binding.getFooterAt(i), 1316 list, false /*attachToRoot*/, skipCallbackParser); 1317 if (pair.getFirst() != null) { 1318 list.addFooterView(pair.getFirst()); 1319 } 1320 1321 skipCallbackParser |= pair.getSecond(); 1322 } 1323 } 1324 1325 if (view instanceof ExpandableListView) { 1326 ((ExpandableListView) view).setAdapter( 1327 new FakeExpandableAdapter( 1328 listRef, binding, params.getProjectCallback())); 1329 } else { 1330 ((AbsListView) view).setAdapter( 1331 new FakeAdapter( 1332 listRef, binding, params.getProjectCallback())); 1333 } 1334 } else if (view instanceof AbsSpinner) { 1335 ((AbsSpinner) view).setAdapter( 1336 new FakeAdapter( 1337 listRef, binding, params.getProjectCallback())); 1338 } 1339 } 1340 } 1341 } else if (view instanceof ViewGroup) { 1342 ViewGroup group = (ViewGroup) view; 1343 final int count = group.getChildCount(); 1344 for (int c = 0; c < count; c++) { 1345 View child = group.getChildAt(c); 1346 postInflateProcess(child, projectCallback, skip); 1347 } 1348 } 1349 } 1350 1351 /** 1352 * Sets up a {@link TabHost} object. 1353 * @param tabHost the TabHost to setup. 1354 * @param projectCallback The project callback object to access the project R class. 1355 * @throws PostInflateException 1356 */ 1357 private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback) 1358 throws PostInflateException { 1359 // look for the TabWidget, and the FrameLayout. They have their own specific names 1360 View v = tabHost.findViewById(android.R.id.tabs); 1361 1362 if (v == null) { 1363 throw new PostInflateException( 1364 "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); 1365 } 1366 1367 if (!(v instanceof TabWidget)) { 1368 throw new PostInflateException(String.format( 1369 "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + 1370 "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); 1371 } 1372 1373 v = tabHost.findViewById(android.R.id.tabcontent); 1374 1375 if (v == null) { 1376 // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty) 1377 //noinspection SpellCheckingInspection 1378 throw new PostInflateException( 1379 "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); 1380 } 1381 1382 if (!(v instanceof FrameLayout)) { 1383 //noinspection SpellCheckingInspection 1384 throw new PostInflateException(String.format( 1385 "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + 1386 "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); 1387 } 1388 1389 FrameLayout content = (FrameLayout)v; 1390 1391 // now process the content of the frameLayout and dynamically create tabs for it. 1392 final int count = content.getChildCount(); 1393 1394 // this must be called before addTab() so that the TabHost searches its TabWidget 1395 // and FrameLayout. 1396 tabHost.setup(); 1397 1398 if (count == 0) { 1399 // Create a dummy child to get a single tab 1400 TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label", 1401 tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details)) 1402 .setContent(new TabHost.TabContentFactory() { 1403 @Override 1404 public View createTabContent(String tag) { 1405 return new LinearLayout(getContext()); 1406 } 1407 }); 1408 tabHost.addTab(spec); 1409 } else { 1410 // for each child of the frameLayout, add a new TabSpec 1411 for (int i = 0 ; i < count ; i++) { 1412 View child = content.getChildAt(i); 1413 String tabSpec = String.format("tab_spec%d", i+1); 1414 @SuppressWarnings("ConstantConditions") // child cannot be null. 1415 int id = child.getId(); 1416 @SuppressWarnings("deprecation") 1417 Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id); 1418 String name; 1419 if (resource != null) { 1420 name = resource.getSecond(); 1421 } else { 1422 name = String.format("Tab %d", i+1); // default name if id is unresolved. 1423 } 1424 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); 1425 } 1426 } 1427 } 1428 1429 /** 1430 * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the 1431 * bounds of all the views. 1432 * 1433 * @param view the root View 1434 * @param offset an offset for the view bounds. 1435 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 1436 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 1437 * content frame. 1438 * 1439 * @return {@code ViewInfo} containing the bounds of the view and it children otherwise. 1440 */ 1441 private ViewInfo visit(View view, int offset, boolean setExtendedInfo, 1442 boolean isContentFrame) { 1443 ViewInfo result = createViewInfo(view, offset, setExtendedInfo, isContentFrame); 1444 1445 if (view instanceof ViewGroup) { 1446 ViewGroup group = ((ViewGroup) view); 1447 result.setChildren(visitAllChildren(group, isContentFrame ? 0 : offset, 1448 setExtendedInfo, isContentFrame)); 1449 } 1450 return result; 1451 } 1452 1453 /** 1454 * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo} 1455 * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with 1456 * the children of the {@code mContentRoot}. 1457 * 1458 * @param viewGroup the root View 1459 * @param offset an offset from the top for the content view frame. 1460 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 1461 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 1462 * content frame. {@code false} if the {@code ViewInfo} to be created is 1463 * part of the system decor. 1464 */ 1465 private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset, 1466 boolean setExtendedInfo, boolean isContentFrame) { 1467 if (viewGroup == null) { 1468 return null; 1469 } 1470 1471 if (!isContentFrame) { 1472 offset += viewGroup.getTop(); 1473 } 1474 1475 int childCount = viewGroup.getChildCount(); 1476 if (viewGroup == mContentRoot) { 1477 List<ViewInfo> childrenWithoutOffset = new ArrayList<ViewInfo>(childCount); 1478 List<ViewInfo> childrenWithOffset = new ArrayList<ViewInfo>(childCount); 1479 for (int i = 0; i < childCount; i++) { 1480 ViewInfo[] childViewInfo = visitContentRoot(viewGroup.getChildAt(i), offset, 1481 setExtendedInfo); 1482 childrenWithoutOffset.add(childViewInfo[0]); 1483 childrenWithOffset.add(childViewInfo[1]); 1484 } 1485 mViewInfoList = childrenWithOffset; 1486 return childrenWithoutOffset; 1487 } else { 1488 List<ViewInfo> children = new ArrayList<ViewInfo>(childCount); 1489 for (int i = 0; i < childCount; i++) { 1490 children.add(visit(viewGroup.getChildAt(i), offset, setExtendedInfo, 1491 isContentFrame)); 1492 } 1493 return children; 1494 } 1495 } 1496 1497 /** 1498 * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the 1499 * bounds of all the views. It returns two {@code ViewInfo} objects with the same children, 1500 * one with the {@code offset} and other without the {@code offset}. The offset is needed to 1501 * get the right bounds if the {@code ViewInfo} hierarchy is accessed from 1502 * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the 1503 * offset is not needed. 1504 * 1505 * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at 1506 * index 1 is with the offset. 1507 */ 1508 private ViewInfo[] visitContentRoot(View view, int offset, boolean setExtendedInfo) { 1509 ViewInfo[] result = new ViewInfo[2]; 1510 if (view == null) { 1511 return result; 1512 } 1513 1514 result[0] = createViewInfo(view, 0, setExtendedInfo, true); 1515 result[1] = createViewInfo(view, offset, setExtendedInfo, true); 1516 if (view instanceof ViewGroup) { 1517 List<ViewInfo> children = visitAllChildren((ViewGroup) view, 0, setExtendedInfo, true); 1518 result[0].setChildren(children); 1519 result[1].setChildren(children); 1520 } 1521 return result; 1522 } 1523 1524 /** 1525 * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children 1526 * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not 1527 * set. 1528 * @param offset an offset for the view bounds. Used only if view is part of the content frame. 1529 */ 1530 private ViewInfo createViewInfo(View view, int offset, boolean setExtendedInfo, 1531 boolean isContentFrame) { 1532 if (view == null) { 1533 return null; 1534 } 1535 1536 ViewInfo result; 1537 if (isContentFrame) { 1538 // The view is part of the layout added by the user. Hence, 1539 // the ViewCookie may be obtained only through the Context. 1540 result = new ViewInfo(view.getClass().getName(), 1541 getContext().getViewKey(view), 1542 view.getLeft(), view.getTop() + offset, view.getRight(), 1543 view.getBottom() + offset, view, view.getLayoutParams()); 1544 } else { 1545 // We are part of the system decor. 1546 SystemViewInfo r = new SystemViewInfo(view.getClass().getName(), 1547 getViewKey(view), 1548 view.getLeft(), view.getTop(), view.getRight(), 1549 view.getBottom(), view, view.getLayoutParams()); 1550 result = r; 1551 // We currently mark three kinds of views: 1552 // 1. Menus in the Action Bar 1553 // 2. Menus in the Overflow popup. 1554 // 3. The overflow popup button. 1555 if (view instanceof ListMenuItemView) { 1556 // Mark 2. 1557 // All menus in the popup are of type ListMenuItemView. 1558 r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU); 1559 } else { 1560 // Mark 3. 1561 ViewGroup.LayoutParams lp = view.getLayoutParams(); 1562 if (lp instanceof ActionMenuView.LayoutParams && 1563 ((ActionMenuView.LayoutParams) lp).isOverflowButton) { 1564 r.setViewType(ViewType.ACTION_BAR_OVERFLOW); 1565 } else { 1566 // Mark 1. 1567 // A view is a menu in the Action Bar is it is not the overflow button and of 1568 // its parent is of type ActionMenuView. We can also check if the view is 1569 // instanceof ActionMenuItemView but that will fail for menus using 1570 // actionProviderClass. 1571 ViewParent parent = view.getParent(); 1572 while (parent != mViewRoot && parent instanceof ViewGroup) { 1573 if (parent instanceof ActionMenuView) { 1574 r.setViewType(ViewType.ACTION_BAR_MENU); 1575 break; 1576 } 1577 parent = parent.getParent(); 1578 } 1579 } 1580 } 1581 } 1582 1583 if (setExtendedInfo) { 1584 MarginLayoutParams marginParams = null; 1585 LayoutParams params = view.getLayoutParams(); 1586 if (params instanceof MarginLayoutParams) { 1587 marginParams = (MarginLayoutParams) params; 1588 } 1589 result.setExtendedInfo(view.getBaseline(), 1590 marginParams != null ? marginParams.leftMargin : 0, 1591 marginParams != null ? marginParams.topMargin : 0, 1592 marginParams != null ? marginParams.rightMargin : 0, 1593 marginParams != null ? marginParams.bottomMargin : 0); 1594 } 1595 1596 return result; 1597 } 1598 1599 /* (non-Javadoc) 1600 * The cookie for menu items are stored in menu item and not in the map from View stored in 1601 * BridgeContext. 1602 */ 1603 private Object getViewKey(View view) { 1604 BridgeContext context = getContext(); 1605 if (!(view instanceof MenuView.ItemView)) { 1606 return context.getViewKey(view); 1607 } 1608 MenuItemImpl menuItem; 1609 if (view instanceof ActionMenuItemView) { 1610 menuItem = ((ActionMenuItemView) view).getItemData(); 1611 } else if (view instanceof ListMenuItemView) { 1612 menuItem = ((ListMenuItemView) view).getItemData(); 1613 } else if (view instanceof IconMenuItemView) { 1614 menuItem = ((IconMenuItemView) view).getItemData(); 1615 } else { 1616 menuItem = null; 1617 } 1618 if (menuItem instanceof BridgeMenuItemImpl) { 1619 return ((BridgeMenuItemImpl) menuItem).getViewCookie(); 1620 } 1621 1622 return null; 1623 } 1624 1625 public void invalidateRenderingSize() { 1626 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1627 } 1628 1629 /** 1630 * Creates the status bar with wifi and battery icons. 1631 */ 1632 private StatusBar createStatusBar(BridgeContext context, Density density, int direction, 1633 boolean isRtlSupported, int platformVersion) throws XmlPullParserException { 1634 StatusBar statusBar = new StatusBar(context, density, 1635 direction, isRtlSupported, platformVersion); 1636 statusBar.setLayoutParams( 1637 new LinearLayout.LayoutParams( 1638 LayoutParams.MATCH_PARENT, mStatusBarSize)); 1639 return statusBar; 1640 } 1641 1642 /** 1643 * Creates the navigation bar with back, home and recent buttons. 1644 * 1645 * @param isRtl true if the current locale is right-to-left 1646 * @param isRtlSupported true is the project manifest declares that the application 1647 * is RTL aware. 1648 */ 1649 private NavigationBar createNavigationBar(BridgeContext context, Density density, 1650 boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion) 1651 throws XmlPullParserException { 1652 NavigationBar navigationBar = new NavigationBar(context, 1653 density, mNavigationBarOrientation, isRtl, 1654 isRtlSupported, simulatedPlatformVersion); 1655 if (mNavigationBarOrientation == LinearLayout.VERTICAL) { 1656 navigationBar.setLayoutParams(new LinearLayout.LayoutParams(mNavigationBarSize, 1657 LayoutParams.MATCH_PARENT)); 1658 } else { 1659 navigationBar.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 1660 mNavigationBarSize)); 1661 } 1662 return navigationBar; 1663 } 1664 1665 private TitleBar createTitleBar(BridgeContext context, String title, 1666 int simulatedPlatformVersion) 1667 throws XmlPullParserException { 1668 TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion); 1669 titleBar.setLayoutParams( 1670 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, mTitleBarSize)); 1671 return titleBar; 1672 } 1673 1674 /** 1675 * Creates the action bar. Also queries the project callback for missing information. 1676 */ 1677 private BridgeActionBar createActionBar(BridgeContext context, SessionParams params, 1678 ViewGroup parentView) { 1679 if (mIsThemeAppCompat == Boolean.TRUE) { 1680 return new AppCompatActionBar(context, params, parentView); 1681 } else { 1682 return new FrameworkActionBar(context, params, parentView); 1683 } 1684 } 1685 1686 public BufferedImage getImage() { 1687 return mImage; 1688 } 1689 1690 public boolean isAlphaChannelImage() { 1691 return mIsAlphaChannelImage; 1692 } 1693 1694 public List<ViewInfo> getViewInfos() { 1695 return mViewInfoList; 1696 } 1697 1698 public List<ViewInfo> getSystemViewInfos() { 1699 return mSystemViewInfoList; 1700 } 1701 1702 public Map<String, String> getDefaultProperties(Object viewObject) { 1703 return getContext().getDefaultPropMap(viewObject); 1704 } 1705 1706 public void setScene(RenderSession session) { 1707 mScene = session; 1708 } 1709 1710 public RenderSession getSession() { 1711 return mScene; 1712 } 1713} 1714