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