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