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