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