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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 1058 private boolean hasSoftwareButtons() { 1059 return getParams().getHardwareConfig().hasSoftwareButtons(); 1060 } 1061 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 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 1614 private void invalidateRenderingSize() { 1615 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1616 } 1617 1618 /** 1619 * Creates the status bar with wifi and battery icons. 1620 */ 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 */ 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 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 */ 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 1675 public BufferedImage getImage() { 1676 return mImage; 1677 } 1678 1679 public boolean isAlphaChannelImage() { 1680 return mIsAlphaChannelImage; 1681 } 1682 1683 public List<ViewInfo> getViewInfos() { 1684 return mViewInfoList; 1685 } 1686 1687 public List<ViewInfo> getSystemViewInfos() { 1688 return mSystemViewInfoList; 1689 } 1690 1691 public Map<String, String> getDefaultProperties(Object viewObject) { 1692 return getContext().getDefaultPropMap(viewObject); 1693 } 1694 1695 public void setScene(RenderSession session) { 1696 mScene = session; 1697 } 1698 1699 public RenderSession getSession() { 1700 return mScene; 1701 } 1702} 1703