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