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 17 package com.android.layoutlib.bridge.impl; 18 19 import com.android.ide.common.rendering.api.AdapterBinding; 20 import com.android.ide.common.rendering.api.HardwareConfig; 21 import com.android.ide.common.rendering.api.ILayoutPullParser; 22 import com.android.ide.common.rendering.api.LayoutLog; 23 import com.android.ide.common.rendering.api.LayoutlibCallback; 24 import com.android.ide.common.rendering.api.RenderSession; 25 import com.android.ide.common.rendering.api.ResourceReference; 26 import com.android.ide.common.rendering.api.ResourceValue; 27 import com.android.ide.common.rendering.api.Result; 28 import com.android.ide.common.rendering.api.SessionParams; 29 import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 30 import com.android.ide.common.rendering.api.ViewInfo; 31 import com.android.ide.common.rendering.api.ViewType; 32 import com.android.internal.view.menu.ActionMenuItemView; 33 import com.android.internal.view.menu.BridgeMenuItemImpl; 34 import com.android.internal.view.menu.IconMenuItemView; 35 import com.android.internal.view.menu.ListMenuItemView; 36 import com.android.internal.view.menu.MenuItemImpl; 37 import com.android.internal.view.menu.MenuView; 38 import com.android.layoutlib.bridge.Bridge; 39 import com.android.layoutlib.bridge.android.BridgeContext; 40 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 41 import com.android.layoutlib.bridge.android.RenderParamsFlags; 42 import com.android.layoutlib.bridge.android.graphics.NopCanvas; 43 import com.android.layoutlib.bridge.android.support.DesignLibUtil; 44 import com.android.layoutlib.bridge.android.support.FragmentTabHostUtil; 45 import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil; 46 import com.android.layoutlib.bridge.impl.binding.FakeAdapter; 47 import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; 48 import com.android.tools.layoutlib.java.System_Delegate; 49 import com.android.util.Pair; 50 51 import android.annotation.NonNull; 52 import android.annotation.Nullable; 53 import android.app.Fragment_Delegate; 54 import android.graphics.Bitmap; 55 import android.graphics.Bitmap_Delegate; 56 import android.graphics.Canvas; 57 import android.graphics.NinePatch_Delegate; 58 import android.os.Looper; 59 import android.preference.Preference_Delegate; 60 import android.view.AttachInfo_Accessor; 61 import android.view.BridgeInflater; 62 import android.view.Choreographer_Delegate; 63 import android.view.View; 64 import android.view.View.MeasureSpec; 65 import android.view.ViewGroup; 66 import android.view.ViewGroup.LayoutParams; 67 import android.view.ViewGroup.MarginLayoutParams; 68 import android.view.ViewParent; 69 import android.widget.AbsListView; 70 import android.widget.AbsSpinner; 71 import android.widget.ActionMenuView; 72 import android.widget.AdapterView; 73 import android.widget.ExpandableListView; 74 import android.widget.FrameLayout; 75 import android.widget.LinearLayout; 76 import android.widget.ListView; 77 import android.widget.QuickContactBadge; 78 import android.widget.TabHost; 79 import android.widget.TabHost.TabSpec; 80 import android.widget.TabWidget; 81 82 import java.awt.AlphaComposite; 83 import java.awt.Color; 84 import java.awt.Graphics2D; 85 import java.awt.image.BufferedImage; 86 import java.util.ArrayList; 87 import java.util.IdentityHashMap; 88 import java.util.List; 89 import java.util.Map; 90 91 import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; 92 import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; 93 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; 94 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; 95 import static com.android.layoutlib.bridge.util.ReflectionUtils.isInstanceOf; 96 97 /** 98 * Class implementing the render session. 99 * <p/> 100 * A session is a stateful representation of a layout file. It is initialized with data coming 101 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then 102 * be done on the layout. 103 */ 104 public class RenderSessionImpl extends RenderAction<SessionParams> { 105 106 private static final Canvas NOP_CANVAS = new NopCanvas(); 107 108 // scene state 109 private RenderSession mScene; 110 private BridgeXmlBlockParser mBlockParser; 111 private BridgeInflater mInflater; 112 private ViewGroup mViewRoot; 113 private FrameLayout mContentRoot; 114 private Canvas mCanvas; 115 private int mMeasuredScreenWidth = -1; 116 private int mMeasuredScreenHeight = -1; 117 private boolean mIsAlphaChannelImage; 118 /** If >= 0, a frame will be executed */ 119 private long mElapsedFrameTimeNanos = -1; 120 /** True if one frame has been already executed to start the animations */ 121 private boolean mFirstFrameExecuted = false; 122 123 // information being returned through the API 124 private BufferedImage mImage; 125 private List<ViewInfo> mViewInfoList; 126 private List<ViewInfo> mSystemViewInfoList; 127 private Layout.Builder mLayoutBuilder; 128 private boolean mNewRenderSize; 129 130 private static final class PostInflateException extends Exception { 131 private static final long serialVersionUID = 1L; 132 PostInflateException(String message)133 private PostInflateException(String message) { 134 super(message); 135 } 136 } 137 138 /** 139 * Creates a layout scene with all the information coming from the layout bridge API. 140 * <p> 141 * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init(long)}, 142 * which act as a 143 * call to {@link RenderSessionImpl#acquire(long)} 144 * 145 * @see Bridge#createSession(SessionParams) 146 */ RenderSessionImpl(SessionParams params)147 public RenderSessionImpl(SessionParams params) { 148 super(new SessionParams(params)); 149 } 150 151 /** 152 * Initializes and acquires the scene, creating various Android objects such as context, 153 * inflater, and parser. 154 * 155 * @param timeout the time to wait if another rendering is happening. 156 * 157 * @return whether the scene was prepared 158 * 159 * @see #acquire(long) 160 * @see #release() 161 */ 162 @Override init(long timeout)163 public Result init(long timeout) { 164 Result result = super.init(timeout); 165 if (!result.isSuccess()) { 166 return result; 167 } 168 169 SessionParams params = getParams(); 170 BridgeContext context = getContext(); 171 172 // use default of true in case it's not found to use alpha by default 173 mIsAlphaChannelImage = 174 ResourceHelper.getBooleanThemeFrameworkAttrValue(params.getResources(), 175 "windowIsFloating", true); 176 177 mLayoutBuilder = new Layout.Builder(params, context); 178 179 // build the inflater and parser. 180 mInflater = new BridgeInflater(context, params.getLayoutlibCallback()); 181 context.setBridgeInflater(mInflater); 182 183 ILayoutPullParser layoutParser = params.getLayoutDescription(); 184 mBlockParser = new BridgeXmlBlockParser(layoutParser, context, layoutParser.getLayoutNamespace()); 185 186 return SUCCESS.createResult(); 187 } 188 189 /** 190 * Measures the the current layout if needed (see {@link #invalidateRenderingSize}). 191 */ measureLayout(@onNull SessionParams params)192 private void measureLayout(@NonNull SessionParams params) { 193 // only do the screen measure when needed. 194 if (mMeasuredScreenWidth != -1) { 195 return; 196 } 197 198 RenderingMode renderingMode = params.getRenderingMode(); 199 HardwareConfig hardwareConfig = params.getHardwareConfig(); 200 201 mNewRenderSize = true; 202 mMeasuredScreenWidth = hardwareConfig.getScreenWidth(); 203 mMeasuredScreenHeight = hardwareConfig.getScreenHeight(); 204 205 if (renderingMode != RenderingMode.NORMAL) { 206 int widthMeasureSpecMode = renderingMode.isHorizExpand() ? 207 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 208 : MeasureSpec.EXACTLY; 209 int heightMeasureSpecMode = renderingMode.isVertExpand() ? 210 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 211 : MeasureSpec.EXACTLY; 212 213 // We used to compare the measured size of the content to the screen size but 214 // this does not work anymore due to the 2 following issues: 215 // - If the content is in a decor (system bar, title/action bar), the root view 216 // will not resize even with the UNSPECIFIED because of the embedded layout. 217 // - If there is no decor, but a dialog frame, then the dialog padding prevents 218 // comparing the size of the content to the screen frame (as it would not 219 // take into account the dialog padding). 220 221 // The solution is to first get the content size in a normal rendering, inside 222 // the decor or the dialog padding. 223 // Then measure only the content with UNSPECIFIED to see the size difference 224 // and apply this to the screen size. 225 226 View measuredView = mContentRoot.getChildAt(0); 227 228 // first measure the full layout, with EXACTLY to get the size of the 229 // content as it is inside the decor/dialog 230 @SuppressWarnings("deprecation") 231 Pair<Integer, Integer> exactMeasure = measureView( 232 mViewRoot, measuredView, 233 mMeasuredScreenWidth, MeasureSpec.EXACTLY, 234 mMeasuredScreenHeight, MeasureSpec.EXACTLY); 235 236 // now measure the content only using UNSPECIFIED (where applicable, based on 237 // the rendering mode). This will give us the size the content needs. 238 @SuppressWarnings("deprecation") 239 Pair<Integer, Integer> result = measureView( 240 mContentRoot, mContentRoot.getChildAt(0), 241 mMeasuredScreenWidth, widthMeasureSpecMode, 242 mMeasuredScreenHeight, heightMeasureSpecMode); 243 244 // If measuredView is not null, exactMeasure nor result will be null. 245 assert exactMeasure != null; 246 assert result != null; 247 248 // now look at the difference and add what is needed. 249 if (renderingMode.isHorizExpand()) { 250 int measuredWidth = exactMeasure.getFirst(); 251 int neededWidth = result.getFirst(); 252 if (neededWidth > measuredWidth) { 253 mMeasuredScreenWidth += neededWidth - measuredWidth; 254 } 255 if (mMeasuredScreenWidth < measuredWidth) { 256 // If the screen width is less than the exact measured width, 257 // expand to match. 258 mMeasuredScreenWidth = measuredWidth; 259 } 260 } 261 262 if (renderingMode.isVertExpand()) { 263 int measuredHeight = exactMeasure.getSecond(); 264 int neededHeight = result.getSecond(); 265 if (neededHeight > measuredHeight) { 266 mMeasuredScreenHeight += neededHeight - measuredHeight; 267 } 268 if (mMeasuredScreenHeight < measuredHeight) { 269 // If the screen height is less than the exact measured height, 270 // expand to match. 271 mMeasuredScreenHeight = measuredHeight; 272 } 273 } 274 } 275 } 276 277 /** 278 * Inflates the layout. 279 * <p> 280 * {@link #acquire(long)} must have been called before this. 281 * 282 * @throws IllegalStateException if the current context is different than the one owned by 283 * the scene, or if {@link #init(long)} was not called. 284 */ inflate()285 public Result inflate() { 286 checkLock(); 287 288 try { 289 mViewRoot = new Layout(mLayoutBuilder); 290 mLayoutBuilder = null; // Done with the builder. 291 mContentRoot = ((Layout) mViewRoot).getContentRoot(); 292 SessionParams params = getParams(); 293 BridgeContext context = getContext(); 294 295 if (Bridge.isLocaleRtl(params.getLocale())) { 296 if (!params.isRtlSupported()) { 297 Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_ENABLED, 298 "You are using a right-to-left " + 299 "(RTL) locale but RTL is not enabled", null); 300 } else if (params.getSimulatedPlatformVersion() !=0 && 301 params.getSimulatedPlatformVersion() < 17) { 302 // This will render ok because we are using the latest layoutlib but at least 303 // warn the user that this might fail in a real device. 304 Bridge.getLog().warning(LayoutLog.TAG_RTL_NOT_SUPPORTED, "You are using a " + 305 "right-to-left " + 306 "(RTL) locale but RTL is not supported for API level < 17", null); 307 } 308 } 309 310 // Sets the project callback (custom view loader) to the fragment delegate so that 311 // it can instantiate the custom Fragment. 312 Fragment_Delegate.setLayoutlibCallback(params.getLayoutlibCallback()); 313 314 String rootTag = params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG); 315 boolean isPreference = "PreferenceScreen".equals(rootTag) || 316 SupportPreferencesUtil.isSupportRootTag(rootTag); 317 View view; 318 if (isPreference) { 319 // First try to use the support library inflater. If something fails, fallback 320 // to the system preference inflater. 321 view = SupportPreferencesUtil.inflatePreference(getContext(), mBlockParser, 322 mContentRoot); 323 if (view == null) { 324 view = Preference_Delegate.inflatePreference(getContext(), mBlockParser, 325 mContentRoot); 326 } 327 } else { 328 view = mInflater.inflate(mBlockParser, mContentRoot); 329 } 330 331 // done with the parser, pop it. 332 context.popParser(); 333 334 Fragment_Delegate.setLayoutlibCallback(null); 335 336 // set the AttachInfo on the root view. 337 AttachInfo_Accessor.setAttachInfo(mViewRoot); 338 339 // post-inflate process. For now this supports TabHost/TabWidget 340 postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null); 341 mInflater.onDoneInflation(); 342 343 setActiveToolbar(view, context, params); 344 345 measureLayout(params); 346 measureView(mViewRoot, null /*measuredView*/, 347 mMeasuredScreenWidth, MeasureSpec.EXACTLY, 348 mMeasuredScreenHeight, MeasureSpec.EXACTLY); 349 mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 350 mSystemViewInfoList = 351 visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(), 352 false); 353 354 Choreographer_Delegate.clearFrames(); 355 356 return SUCCESS.createResult(); 357 } catch (PostInflateException e) { 358 return ERROR_INFLATION.createResult(e.getMessage(), e); 359 } catch (Throwable e) { 360 // get the real cause of the exception. 361 Throwable t = e; 362 while (t.getCause() != null) { 363 t = t.getCause(); 364 } 365 366 return ERROR_INFLATION.createResult(t.getMessage(), t); 367 } 368 } 369 370 /** 371 * Sets the time for which the next frame will be selected. The time is the elapsed time from 372 * the current system nanos time. You 373 */ setElapsedFrameTimeNanos(long nanos)374 public void setElapsedFrameTimeNanos(long nanos) { 375 mElapsedFrameTimeNanos = nanos; 376 } 377 378 /** 379 * Runs a layout pass for the given view root 380 */ doLayout(@onNull BridgeContext context, @NonNull ViewGroup viewRoot, int width, int height)381 private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot, 382 int width, int height) { 383 // measure again with the size we need 384 // This must always be done before the call to layout 385 measureView(viewRoot, null /*measuredView*/, 386 width, MeasureSpec.EXACTLY, 387 height, MeasureSpec.EXACTLY); 388 389 // now do the layout. 390 viewRoot.layout(0, 0, width, height); 391 handleScrolling(context, viewRoot); 392 } 393 394 /** 395 * Renders the given view hierarchy to the passed canvas and returns the result of the render 396 * operation. 397 * @param canvas an optional canvas to render the views to. If null, only the measure and 398 * layout steps will be executed. 399 */ renderAndBuildResult(@onNull ViewGroup viewRoot, @Nullable Canvas canvas)400 private static Result renderAndBuildResult(@NonNull ViewGroup viewRoot, @Nullable Canvas canvas) { 401 if (canvas == null) { 402 return SUCCESS.createResult(); 403 } 404 405 AttachInfo_Accessor.dispatchOnPreDraw(viewRoot); 406 viewRoot.draw(canvas); 407 408 return SUCCESS.createResult(); 409 } 410 411 /** 412 * Renders the scene. 413 * <p> 414 * {@link #acquire(long)} must have been called before this. 415 * 416 * @param freshRender whether the render is a new one and should erase the existing bitmap (in 417 * the case where bitmaps are reused). This is typically needed when not playing 418 * animations.) 419 * 420 * @throws IllegalStateException if the current context is different than the one owned by 421 * the scene, or if {@link #acquire(long)} was not called. 422 * 423 * @see SessionParams#getRenderingMode() 424 * @see RenderSession#render(long) 425 */ render(boolean freshRender)426 public Result render(boolean freshRender) { 427 return renderAndBuildResult(freshRender, false); 428 } 429 430 /** 431 * Measures the layout 432 * <p> 433 * {@link #acquire(long)} must have been called before this. 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 */ measure()441 public Result measure() { 442 return renderAndBuildResult(false, true); 443 } 444 445 /** 446 * Renders the scene. 447 * <p> 448 * {@link #acquire(long)} must have been called before this. 449 * 450 * @param freshRender whether the render is a new one and should erase the existing bitmap (in 451 * the case where bitmaps are reused). This is typically needed when not playing 452 * animations.) 453 * 454 * @throws IllegalStateException if the current context is different than the one owned by 455 * the scene, or if {@link #acquire(long)} was not called. 456 * 457 * @see SessionParams#getRenderingMode() 458 * @see RenderSession#render(long) 459 */ renderAndBuildResult(boolean freshRender, boolean onlyMeasure)460 private Result renderAndBuildResult(boolean freshRender, boolean onlyMeasure) { 461 checkLock(); 462 463 SessionParams params = getParams(); 464 465 try { 466 if (mViewRoot == null) { 467 return ERROR_NOT_INFLATED.createResult(); 468 } 469 470 measureLayout(params); 471 472 HardwareConfig hardwareConfig = params.getHardwareConfig(); 473 Result renderResult = SUCCESS.createResult(); 474 if (onlyMeasure) { 475 // delete the canvas and image to reset them on the next full rendering 476 mImage = null; 477 mCanvas = null; 478 doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight); 479 } else { 480 // draw the views 481 // create the BufferedImage into which the layout will be rendered. 482 boolean newImage = false; 483 484 // When disableBitmapCaching is true, we do not reuse mImage and 485 // we create a new one in every render. 486 // This is useful when mImage is just a wrapper of Graphics2D so 487 // it doesn't get cached. 488 boolean disableBitmapCaching = Boolean.TRUE.equals(params.getFlag( 489 RenderParamsFlags.FLAG_KEY_DISABLE_BITMAP_CACHING)); 490 491 if (mNewRenderSize || mCanvas == null || disableBitmapCaching) { 492 mNewRenderSize = false; 493 if (params.getImageFactory() != null) { 494 mImage = params.getImageFactory().getImage( 495 mMeasuredScreenWidth, 496 mMeasuredScreenHeight); 497 } else { 498 mImage = new BufferedImage( 499 mMeasuredScreenWidth, 500 mMeasuredScreenHeight, 501 BufferedImage.TYPE_INT_ARGB); 502 newImage = true; 503 } 504 505 if (params.isBgColorOverridden()) { 506 // since we override the content, it's the same as if it was a new image. 507 newImage = true; 508 Graphics2D gc = mImage.createGraphics(); 509 gc.setColor(new Color(params.getOverrideBgColor(), true)); 510 gc.setComposite(AlphaComposite.Src); 511 gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 512 gc.dispose(); 513 } 514 515 // create an Android bitmap around the BufferedImage 516 Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, 517 true /*isMutable*/, hardwareConfig.getDensity()); 518 519 if (mCanvas == null) { 520 // create a Canvas around the Android bitmap 521 mCanvas = new Canvas(bitmap); 522 } else { 523 mCanvas.setBitmap(bitmap); 524 } 525 526 boolean enableImageResizing = 527 mImage.getWidth() != mMeasuredScreenWidth && 528 mImage.getHeight() != mMeasuredScreenHeight && 529 Boolean.TRUE.equals(params.getFlag( 530 RenderParamsFlags.FLAG_KEY_RESULT_IMAGE_AUTO_SCALE)); 531 532 if (enableImageResizing) { 533 float scaleX = (float)mImage.getWidth() / mMeasuredScreenWidth; 534 float scaleY = (float)mImage.getHeight() / mMeasuredScreenHeight; 535 mCanvas.scale(scaleX, scaleY); 536 } 537 538 mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue()); 539 } 540 541 if (freshRender && !newImage) { 542 Graphics2D gc = mImage.createGraphics(); 543 gc.setComposite(AlphaComposite.Src); 544 545 gc.setColor(new Color(0x00000000, true)); 546 gc.fillRect(0, 0, 547 mMeasuredScreenWidth, mMeasuredScreenHeight); 548 549 // done 550 gc.dispose(); 551 } 552 553 doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight); 554 if (mElapsedFrameTimeNanos >= 0) { 555 long initialTime = System_Delegate.nanoTime(); 556 if (!mFirstFrameExecuted) { 557 // We need to run an initial draw call to initialize the animations 558 renderAndBuildResult(mViewRoot, NOP_CANVAS); 559 560 // The first frame will initialize the animations 561 Choreographer_Delegate.doFrame(initialTime); 562 mFirstFrameExecuted = true; 563 } 564 // Second frame will move the animations 565 Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos); 566 } 567 renderResult = renderAndBuildResult(mViewRoot, mCanvas); 568 } 569 570 mSystemViewInfoList = 571 visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(), 572 false); 573 574 // success! 575 return renderResult; 576 } catch (Throwable e) { 577 // get the real cause of the exception. 578 Throwable t = e; 579 while (t.getCause() != null) { 580 t = t.getCause(); 581 } 582 583 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 584 } 585 } 586 587 /** 588 * Executes {@link View#measure(int, int)} on a given view with the given parameters (used 589 * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}. 590 * 591 * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height) 592 * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}). 593 * 594 * @param viewToMeasure the view on which to execute measure(). 595 * @param measuredView if non null, the view to query for its measured width/height. 596 * @param width the width to use in the MeasureSpec. 597 * @param widthMode the MeasureSpec mode to use for the width. 598 * @param height the height to use in the MeasureSpec. 599 * @param heightMode the MeasureSpec mode to use for the height. 600 * @return the measured width/height if measuredView is non-null, null otherwise. 601 */ 602 @SuppressWarnings("deprecation") // For the use of Pair measureView(ViewGroup viewToMeasure, View measuredView, int width, int widthMode, int height, int heightMode)603 private static Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView, 604 int width, int widthMode, int height, int heightMode) { 605 int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode); 606 int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode); 607 viewToMeasure.measure(w_spec, h_spec); 608 609 if (measuredView != null) { 610 return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight()); 611 } 612 613 return null; 614 } 615 616 /** 617 * Post process on a view hierarchy that was just inflated. 618 * <p/> 619 * At the moment this only supports TabHost: If {@link TabHost} is detected, look for the 620 * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically 621 * based on the content of the {@link FrameLayout}. 622 * @param view the root view to process. 623 * @param layoutlibCallback callback to the project. 624 * @param skip the view and it's children are not processed. 625 */ 626 @SuppressWarnings("deprecation") // For the use of Pair postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip)627 private void postInflateProcess(View view, LayoutlibCallback layoutlibCallback, View skip) 628 throws PostInflateException { 629 if (view == skip) { 630 return; 631 } 632 if (view instanceof TabHost) { 633 setupTabHost((TabHost) view, layoutlibCallback); 634 } else if (view instanceof QuickContactBadge) { 635 QuickContactBadge badge = (QuickContactBadge) view; 636 badge.setImageToDefault(); 637 } else if (view instanceof AdapterView<?>) { 638 // get the view ID. 639 int id = view.getId(); 640 641 BridgeContext context = getContext(); 642 643 // get a ResourceReference from the integer ID. 644 ResourceReference listRef = context.resolveId(id); 645 646 if (listRef != null) { 647 SessionParams params = getParams(); 648 AdapterBinding binding = params.getAdapterBindings().get(listRef); 649 650 // if there was no adapter binding, trying to get it from the call back. 651 if (binding == null) { 652 binding = layoutlibCallback.getAdapterBinding( 653 listRef, context.getViewKey(view), view); 654 } 655 656 if (binding != null) { 657 658 if (view instanceof AbsListView) { 659 if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && 660 view instanceof ListView) { 661 ListView list = (ListView) view; 662 663 boolean skipCallbackParser = false; 664 665 int count = binding.getHeaderCount(); 666 for (int i = 0; i < count; i++) { 667 Pair<View, Boolean> pair = context.inflateView( 668 binding.getHeaderAt(i), 669 list, false, skipCallbackParser); 670 if (pair.getFirst() != null) { 671 list.addHeaderView(pair.getFirst()); 672 } 673 674 skipCallbackParser |= pair.getSecond(); 675 } 676 677 count = binding.getFooterCount(); 678 for (int i = 0; i < count; i++) { 679 Pair<View, Boolean> pair = context.inflateView( 680 binding.getFooterAt(i), 681 list, false, skipCallbackParser); 682 if (pair.getFirst() != null) { 683 list.addFooterView(pair.getFirst()); 684 } 685 686 skipCallbackParser |= pair.getSecond(); 687 } 688 } 689 690 if (view instanceof ExpandableListView) { 691 ((ExpandableListView) view).setAdapter( 692 new FakeExpandableAdapter(listRef, binding, layoutlibCallback)); 693 } else { 694 ((AbsListView) view).setAdapter( 695 new FakeAdapter(listRef, binding, layoutlibCallback)); 696 } 697 } else if (view instanceof AbsSpinner) { 698 ((AbsSpinner) view).setAdapter( 699 new FakeAdapter(listRef, binding, layoutlibCallback)); 700 } 701 } 702 } 703 } else if (view instanceof ViewGroup) { 704 mInflater.postInflateProcess(view); 705 ViewGroup group = (ViewGroup) view; 706 final int count = group.getChildCount(); 707 for (int c = 0; c < count; c++) { 708 View child = group.getChildAt(c); 709 postInflateProcess(child, layoutlibCallback, skip); 710 } 711 } 712 } 713 714 /** 715 * If the root layout is a CoordinatorLayout with an AppBar: 716 * Set the title of the AppBar to the title of the activity context. 717 */ setActiveToolbar(View view, BridgeContext context, SessionParams params)718 private void setActiveToolbar(View view, BridgeContext context, SessionParams params) { 719 View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT); 720 if (coordinatorLayout == null) { 721 return; 722 } 723 View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT); 724 if (appBar == null) { 725 return; 726 } 727 ViewGroup collapsingToolbar = 728 (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT); 729 if (collapsingToolbar == null) { 730 return; 731 } 732 if (!hasToolbar(collapsingToolbar)) { 733 return; 734 } 735 String title = params.getAppLabel(); 736 DesignLibUtil.setTitle(collapsingToolbar, title); 737 } 738 findChildView(View view, String[] className)739 private View findChildView(View view, String[] className) { 740 if (!(view instanceof ViewGroup)) { 741 return null; 742 } 743 ViewGroup group = (ViewGroup) view; 744 for (int i = 0; i < group.getChildCount(); i++) { 745 if (isInstanceOf(group.getChildAt(i), className)) { 746 return group.getChildAt(i); 747 } 748 } 749 return null; 750 } 751 hasToolbar(View collapsingToolbar)752 private boolean hasToolbar(View collapsingToolbar) { 753 if (!(collapsingToolbar instanceof ViewGroup)) { 754 return false; 755 } 756 ViewGroup group = (ViewGroup) collapsingToolbar; 757 for (int i = 0; i < group.getChildCount(); i++) { 758 if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) { 759 return true; 760 } 761 } 762 return false; 763 } 764 765 /** 766 * Set the scroll position on all the components with the "scrollX" and "scrollY" attribute. If 767 * the component supports nested scrolling attempt that first, then use the unconsumed scroll 768 * part to scroll the content in the component. 769 */ handleScrolling(BridgeContext context, View view)770 private static void handleScrolling(BridgeContext context, View view) { 771 int scrollPosX = context.getScrollXPos(view); 772 int scrollPosY = context.getScrollYPos(view); 773 if (scrollPosX != 0 || scrollPosY != 0) { 774 if (view.isNestedScrollingEnabled()) { 775 int[] consumed = new int[2]; 776 int axis = scrollPosX != 0 ? View.SCROLL_AXIS_HORIZONTAL : 0; 777 axis |= scrollPosY != 0 ? View.SCROLL_AXIS_VERTICAL : 0; 778 if (view.startNestedScroll(axis)) { 779 view.dispatchNestedPreScroll(scrollPosX, scrollPosY, consumed, null); 780 view.dispatchNestedScroll(consumed[0], consumed[1], scrollPosX, scrollPosY, 781 null); 782 view.stopNestedScroll(); 783 scrollPosX -= consumed[0]; 784 scrollPosY -= consumed[1]; 785 } 786 } 787 if (scrollPosX != 0 || scrollPosY != 0) { 788 view.scrollTo(scrollPosX, scrollPosY); 789 } 790 } 791 792 if (!(view instanceof ViewGroup)) { 793 return; 794 } 795 ViewGroup group = (ViewGroup) view; 796 for (int i = 0; i < group.getChildCount(); i++) { 797 View child = group.getChildAt(i); 798 handleScrolling(context, child); 799 } 800 } 801 802 /** 803 * Sets up a {@link TabHost} object. 804 * @param tabHost the TabHost to setup. 805 * @param layoutlibCallback The project callback object to access the project R class. 806 * @throws PostInflateException if TabHost is missing the required ids for TabHost 807 */ setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback)808 private void setupTabHost(TabHost tabHost, LayoutlibCallback layoutlibCallback) 809 throws PostInflateException { 810 // look for the TabWidget, and the FrameLayout. They have their own specific names 811 View v = tabHost.findViewById(android.R.id.tabs); 812 813 if (v == null) { 814 throw new PostInflateException( 815 "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); 816 } 817 818 if (!(v instanceof TabWidget)) { 819 throw new PostInflateException(String.format( 820 "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + 821 "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); 822 } 823 824 v = tabHost.findViewById(android.R.id.tabcontent); 825 826 if (v == null) { 827 // TODO: see if we can fake tabs even without the FrameLayout (same below when the frameLayout is empty) 828 //noinspection SpellCheckingInspection 829 throw new PostInflateException( 830 "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); 831 } 832 833 if (!(v instanceof FrameLayout)) { 834 //noinspection SpellCheckingInspection 835 throw new PostInflateException(String.format( 836 "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + 837 "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); 838 } 839 840 FrameLayout content = (FrameLayout)v; 841 842 // now process the content of the frameLayout and dynamically create tabs for it. 843 final int count = content.getChildCount(); 844 845 // this must be called before addTab() so that the TabHost searches its TabWidget 846 // and FrameLayout. 847 if (isInstanceOf(tabHost, FragmentTabHostUtil.CN_FRAGMENT_TAB_HOST)) { 848 FragmentTabHostUtil.setup(tabHost, getContext()); 849 } else { 850 tabHost.setup(); 851 } 852 853 if (count == 0) { 854 // Create a placeholder child to get a single tab 855 TabSpec spec = tabHost.newTabSpec("tag") 856 .setIndicator("Tab Label", tabHost.getResources() 857 .getDrawable(android.R.drawable.ic_menu_info_details, null)) 858 .setContent(tag -> new LinearLayout(getContext())); 859 tabHost.addTab(spec); 860 } else { 861 // for each child of the frameLayout, add a new TabSpec 862 for (int i = 0 ; i < count ; i++) { 863 View child = content.getChildAt(i); 864 String tabSpec = String.format("tab_spec%d", i+1); 865 @SuppressWarnings("ConstantConditions") // child cannot be null. 866 int id = child.getId(); 867 @SuppressWarnings("deprecation") 868 ResourceReference resource = layoutlibCallback.resolveResourceId(id); 869 String name; 870 if (resource != null) { 871 name = resource.getName(); 872 } else { 873 name = String.format("Tab %d", i+1); // default name if id is unresolved. 874 } 875 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); 876 } 877 } 878 } 879 880 /** 881 * Visits a {@link View} and its children and generate a {@link ViewInfo} containing the 882 * bounds of all the views. 883 * 884 * @param view the root View 885 * @param hOffset horizontal offset for the view bounds. 886 * @param vOffset vertical offset for the view bounds. 887 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 888 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 889 * content frame. 890 * 891 * @return {@code ViewInfo} containing the bounds of the view and it children otherwise. 892 */ visit(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)893 private ViewInfo visit(View view, int hOffset, int vOffset, boolean setExtendedInfo, 894 boolean isContentFrame) { 895 ViewInfo result = createViewInfo(view, hOffset, vOffset, setExtendedInfo, isContentFrame); 896 897 if (view instanceof ViewGroup) { 898 ViewGroup group = ((ViewGroup) view); 899 result.setChildren(visitAllChildren(group, isContentFrame ? 0 : hOffset, 900 isContentFrame ? 0 : vOffset, 901 setExtendedInfo, isContentFrame)); 902 } 903 return result; 904 } 905 906 /** 907 * Visits all the children of a given ViewGroup and generates a list of {@link ViewInfo} 908 * containing the bounds of all the views. It also initializes the {@link #mViewInfoList} with 909 * the children of the {@code mContentRoot}. 910 * 911 * @param viewGroup the root View 912 * @param hOffset horizontal offset from the top for the content view frame. 913 * @param vOffset vertical offset from the top for the content view frame. 914 * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. 915 * @param isContentFrame {@code true} if the {@code ViewInfo} to be created is part of the 916 * content frame. {@code false} if the {@code ViewInfo} to be created is 917 * part of the system decor. 918 */ visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)919 private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int hOffset, int vOffset, 920 boolean setExtendedInfo, boolean isContentFrame) { 921 if (viewGroup == null) { 922 return null; 923 } 924 925 if (!isContentFrame) { 926 vOffset += viewGroup.getTop(); 927 hOffset += viewGroup.getLeft(); 928 } 929 930 int childCount = viewGroup.getChildCount(); 931 if (viewGroup == mContentRoot) { 932 List<ViewInfo> childrenWithoutOffset = new ArrayList<>(childCount); 933 List<ViewInfo> childrenWithOffset = new ArrayList<>(childCount); 934 for (int i = 0; i < childCount; i++) { 935 ViewInfo[] childViewInfo = 936 visitContentRoot(viewGroup.getChildAt(i), hOffset, vOffset, 937 setExtendedInfo); 938 childrenWithoutOffset.add(childViewInfo[0]); 939 childrenWithOffset.add(childViewInfo[1]); 940 } 941 mViewInfoList = childrenWithOffset; 942 return childrenWithoutOffset; 943 } else { 944 List<ViewInfo> children = new ArrayList<>(childCount); 945 for (int i = 0; i < childCount; i++) { 946 children.add(visit(viewGroup.getChildAt(i), hOffset, vOffset, setExtendedInfo, 947 isContentFrame)); 948 } 949 return children; 950 } 951 } 952 953 /** 954 * Visits the children of {@link #mContentRoot} and generates {@link ViewInfo} containing the 955 * bounds of all the views. It returns two {@code ViewInfo} objects with the same children, 956 * one with the {@code offset} and other without the {@code offset}. The offset is needed to 957 * get the right bounds if the {@code ViewInfo} hierarchy is accessed from 958 * {@code mViewInfoList}. When the hierarchy is accessed via {@code mSystemViewInfoList}, the 959 * offset is not needed. 960 * 961 * @return an array of length two, with ViewInfo at index 0 is without offset and ViewInfo at 962 * index 1 is with the offset. 963 */ 964 @NonNull visitContentRoot(View view, int hOffset, int vOffset, boolean setExtendedInfo)965 private ViewInfo[] visitContentRoot(View view, int hOffset, int vOffset, 966 boolean setExtendedInfo) { 967 ViewInfo[] result = new ViewInfo[2]; 968 if (view == null) { 969 return result; 970 } 971 972 result[0] = createViewInfo(view, 0, 0, setExtendedInfo, true); 973 result[1] = createViewInfo(view, hOffset, vOffset, setExtendedInfo, true); 974 if (view instanceof ViewGroup) { 975 List<ViewInfo> children = 976 visitAllChildren((ViewGroup) view, 0, 0, setExtendedInfo, true); 977 result[0].setChildren(children); 978 result[1].setChildren(children); 979 } 980 return result; 981 } 982 983 /** 984 * Creates a {@link ViewInfo} for the view. The {@code ViewInfo} corresponding to the children 985 * of the {@code view} are not created. Consequently, the children of {@code ViewInfo} is not 986 * set. 987 * @param hOffset horizontal offset for the view bounds. Used only if view is part of the 988 * content frame. 989 * @param vOffset vertial an offset for the view bounds. Used only if view is part of the 990 * content frame. 991 */ createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo, boolean isContentFrame)992 private ViewInfo createViewInfo(View view, int hOffset, int vOffset, boolean setExtendedInfo, 993 boolean isContentFrame) { 994 if (view == null) { 995 return null; 996 } 997 998 ViewParent parent = view.getParent(); 999 ViewInfo result; 1000 if (isContentFrame) { 1001 // Account for parent scroll values when calculating the bounding box 1002 int scrollX = parent != null ? ((View)parent).getScrollX() : 0; 1003 int scrollY = parent != null ? ((View)parent).getScrollY() : 0; 1004 1005 // The view is part of the layout added by the user. Hence, 1006 // the ViewCookie may be obtained only through the Context. 1007 int shiftX = -scrollX + Math.round(view.getTranslationX()) + hOffset; 1008 int shiftY = -scrollY + Math.round(view.getTranslationY()) + vOffset; 1009 result = new ViewInfo(view.getClass().getName(), 1010 getContext().getViewKey(view), 1011 shiftX + view.getLeft(), 1012 shiftY + view.getTop(), 1013 shiftX + view.getRight(), 1014 shiftY + view.getBottom(), 1015 view, view.getLayoutParams()); 1016 } else { 1017 // We are part of the system decor. 1018 SystemViewInfo r = new SystemViewInfo(view.getClass().getName(), 1019 getViewKey(view), 1020 view.getLeft(), view.getTop(), view.getRight(), 1021 view.getBottom(), view, view.getLayoutParams()); 1022 result = r; 1023 // We currently mark three kinds of views: 1024 // 1. Menus in the Action Bar 1025 // 2. Menus in the Overflow popup. 1026 // 3. The overflow popup button. 1027 if (view instanceof ListMenuItemView) { 1028 // Mark 2. 1029 // All menus in the popup are of type ListMenuItemView. 1030 r.setViewType(ViewType.ACTION_BAR_OVERFLOW_MENU); 1031 } else { 1032 // Mark 3. 1033 ViewGroup.LayoutParams lp = view.getLayoutParams(); 1034 if (lp instanceof ActionMenuView.LayoutParams && 1035 ((ActionMenuView.LayoutParams) lp).isOverflowButton) { 1036 r.setViewType(ViewType.ACTION_BAR_OVERFLOW); 1037 } else { 1038 // Mark 1. 1039 // A view is a menu in the Action Bar is it is not the overflow button and of 1040 // its parent is of type ActionMenuView. We can also check if the view is 1041 // instanceof ActionMenuItemView but that will fail for menus using 1042 // actionProviderClass. 1043 while (parent != mViewRoot && parent instanceof ViewGroup) { 1044 if (parent instanceof ActionMenuView) { 1045 r.setViewType(ViewType.ACTION_BAR_MENU); 1046 break; 1047 } 1048 parent = parent.getParent(); 1049 } 1050 } 1051 } 1052 } 1053 1054 if (setExtendedInfo) { 1055 MarginLayoutParams marginParams = null; 1056 LayoutParams params = view.getLayoutParams(); 1057 if (params instanceof MarginLayoutParams) { 1058 marginParams = (MarginLayoutParams) params; 1059 } 1060 result.setExtendedInfo(view.getBaseline(), 1061 marginParams != null ? marginParams.leftMargin : 0, 1062 marginParams != null ? marginParams.topMargin : 0, 1063 marginParams != null ? marginParams.rightMargin : 0, 1064 marginParams != null ? marginParams.bottomMargin : 0); 1065 } 1066 1067 return result; 1068 } 1069 1070 /* (non-Javadoc) 1071 * The cookie for menu items are stored in menu item and not in the map from View stored in 1072 * BridgeContext. 1073 */ 1074 @Nullable getViewKey(View view)1075 private Object getViewKey(View view) { 1076 BridgeContext context = getContext(); 1077 if (!(view instanceof MenuView.ItemView)) { 1078 return context.getViewKey(view); 1079 } 1080 MenuItemImpl menuItem; 1081 if (view instanceof ActionMenuItemView) { 1082 menuItem = ((ActionMenuItemView) view).getItemData(); 1083 } else if (view instanceof ListMenuItemView) { 1084 menuItem = ((ListMenuItemView) view).getItemData(); 1085 } else if (view instanceof IconMenuItemView) { 1086 menuItem = ((IconMenuItemView) view).getItemData(); 1087 } else { 1088 menuItem = null; 1089 } 1090 if (menuItem instanceof BridgeMenuItemImpl) { 1091 return ((BridgeMenuItemImpl) menuItem).getViewCookie(); 1092 } 1093 1094 return null; 1095 } 1096 invalidateRenderingSize()1097 public void invalidateRenderingSize() { 1098 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1099 } 1100 getImage()1101 public BufferedImage getImage() { 1102 return mImage; 1103 } 1104 isAlphaChannelImage()1105 public boolean isAlphaChannelImage() { 1106 return mIsAlphaChannelImage; 1107 } 1108 getViewInfos()1109 public List<ViewInfo> getViewInfos() { 1110 return mViewInfoList; 1111 } 1112 getSystemViewInfos()1113 public List<ViewInfo> getSystemViewInfos() { 1114 return mSystemViewInfoList; 1115 } 1116 getDefaultNamespacedProperties()1117 public Map<Object, Map<ResourceReference, ResourceValue>> getDefaultNamespacedProperties() { 1118 return getContext().getDefaultProperties(); 1119 } 1120 getDefaultStyles()1121 public Map<Object, String> getDefaultStyles() { 1122 Map<Object, String> defaultStyles = new IdentityHashMap<>(); 1123 Map<Object, ResourceReference> namespacedStyles = getDefaultNamespacedStyles(); 1124 for (Object key : namespacedStyles.keySet()) { 1125 ResourceReference style = namespacedStyles.get(key); 1126 defaultStyles.put(key, style.getQualifiedName()); 1127 } 1128 return defaultStyles; 1129 } 1130 getDefaultNamespacedStyles()1131 public Map<Object, ResourceReference> getDefaultNamespacedStyles() { 1132 return getContext().getDefaultNamespacedStyles(); 1133 } 1134 setScene(RenderSession session)1135 public void setScene(RenderSession session) { 1136 mScene = session; 1137 } 1138 getSession()1139 public RenderSession getSession() { 1140 return mScene; 1141 } 1142 dispose()1143 public void dispose() { 1144 boolean createdLooper = false; 1145 if (Looper.myLooper() == null) { 1146 // Detaching the root view from the window will try to stop any running animations. 1147 // The stop method checks that it can run in the looper so, if there is no current 1148 // looper, we create a temporary one to complete the shutdown. 1149 Bridge.prepareThread(); 1150 createdLooper = true; 1151 } 1152 AttachInfo_Accessor.detachFromWindow(mViewRoot); 1153 if (mCanvas != null) { 1154 mCanvas.release(); 1155 mCanvas = null; 1156 } 1157 if (mViewInfoList != null) { 1158 mViewInfoList.clear(); 1159 } 1160 if (mSystemViewInfoList != null) { 1161 mSystemViewInfoList.clear(); 1162 } 1163 mImage = null; 1164 mViewRoot = null; 1165 mContentRoot = null; 1166 NinePatch_Delegate.clearCache(); 1167 1168 if (createdLooper) { 1169 Choreographer_Delegate.dispose(); 1170 Bridge.cleanupThread(); 1171 } 1172 } 1173 } 1174