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