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