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