1/* 2 * Copyright (C) 2015 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.HardwareConfig; 20import com.android.ide.common.rendering.api.RenderResources; 21import com.android.ide.common.rendering.api.ResourceValue; 22import com.android.ide.common.rendering.api.SessionParams; 23import com.android.layoutlib.bridge.Bridge; 24import com.android.layoutlib.bridge.android.BridgeContext; 25import com.android.layoutlib.bridge.android.RenderParamsFlags; 26import com.android.layoutlib.bridge.bars.AppCompatActionBar; 27import com.android.layoutlib.bridge.bars.BridgeActionBar; 28import com.android.layoutlib.bridge.bars.Config; 29import com.android.layoutlib.bridge.bars.FrameworkActionBar; 30import com.android.layoutlib.bridge.bars.NavigationBar; 31import com.android.layoutlib.bridge.bars.StatusBar; 32import com.android.layoutlib.bridge.bars.TitleBar; 33import com.android.resources.Density; 34import com.android.resources.ResourceType; 35import com.android.resources.ScreenOrientation; 36 37import android.annotation.NonNull; 38import android.graphics.drawable.Drawable; 39import android.util.DisplayMetrics; 40import android.util.TypedValue; 41import android.view.AttachInfo_Accessor; 42import android.view.View; 43import android.view.ViewRootImpl; 44import android.view.ViewRootImpl_Accessor; 45import android.widget.FrameLayout; 46import android.widget.LinearLayout; 47import android.widget.RelativeLayout; 48 49import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 50import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 51import static android.widget.LinearLayout.VERTICAL; 52import static com.android.layoutlib.bridge.impl.ResourceHelper.getBooleanThemeValue; 53 54/** 55 * The Layout used to create the system decor. 56 * <p> 57 * The layout inflated will contain a content frame where the user's layout can be inflated. 58 * <pre> 59 * +-------------------------------------------------+---+ 60 * | Status bar | N | 61 * +-------------------------------------------------+ a | 62 * | Title/Framework Action bar (optional) | v | 63 * +-------------------------------------------------+ | 64 * | AppCompat Action bar (optional) | | 65 * +-------------------------------------------------+ | 66 * | Content, vertical extending | b | 67 * | | a | 68 * | | r | 69 * +-------------------------------------------------+---+ 70 * </pre> 71 * or 72 * <pre> 73 * +--------------------------------------+ 74 * | Status bar | 75 * +--------------------------------------+ 76 * | Title/Framework Action bar (optional)| 77 * +--------------------------------------+ 78 * | AppCompat Action bar (optional) | 79 * +--------------------------------------+ 80 * | Content, vertical extending | 81 * | | 82 * | | 83 * +--------------------------------------+ 84 * | Nav bar | 85 * +--------------------------------------+ 86 * </pre> 87 */ 88class Layout extends RelativeLayout { 89 90 // Theme attributes used for configuring appearance of the system decor. 91 private static final String ATTR_WINDOW_FLOATING = "windowIsFloating"; 92 private static final String ATTR_WINDOW_BACKGROUND = "windowBackground"; 93 private static final String ATTR_WINDOW_FULL_SCREEN = "windowFullscreen"; 94 private static final String ATTR_NAV_BAR_HEIGHT = "navigation_bar_height"; 95 private static final String ATTR_NAV_BAR_WIDTH = "navigation_bar_width"; 96 private static final String ATTR_STATUS_BAR_HEIGHT = "status_bar_height"; 97 private static final String ATTR_WINDOW_ACTION_BAR = "windowActionBar"; 98 private static final String ATTR_ACTION_BAR_SIZE = "actionBarSize"; 99 private static final String ATTR_WINDOW_NO_TITLE = "windowNoTitle"; 100 private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize"; 101 private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT; 102 private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT; 103 104 // Default sizes 105 private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; 106 private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; 107 private static final int DEFAULT_NAV_BAR_SIZE = 48; 108 109 // Ids assigned to components created. This is so that we can refer to other components in 110 // layout params. 111 private static final String ID_NAV_BAR = "navBar"; 112 private static final String ID_STATUS_BAR = "statusBar"; 113 private static final String ID_APP_COMPAT_ACTION_BAR = "appCompatActionBar"; 114 private static final String ID_FRAMEWORK_BAR = "frameworkBar"; 115 // Prefix used with the above ids in order to make them unique in framework namespace. 116 private static final String ID_PREFIX = "android_layoutlib_"; 117 118 /** 119 * Temporarily store the builder so that it doesn't have to be passed to all methods used 120 * during inflation. 121 */ 122 private Builder mBuilder; 123 124 /** 125 * This holds user's layout. 126 */ 127 private FrameLayout mContentRoot; 128 129 public Layout(@NonNull Builder builder) { 130 super(builder.mContext); 131 132 mBuilder = builder; 133 View frameworkActionBar = null; 134 View appCompatActionBar = null; 135 TitleBar titleBar = null; 136 StatusBar statusBar = null; 137 NavigationBar navBar = null; 138 139 if (builder.mWindowBackground != null) { 140 Drawable d = ResourceHelper.getDrawable(builder.mWindowBackground, builder.mContext); 141 setBackground(d); 142 } 143 144 int simulatedPlatformVersion = getParams().getSimulatedPlatformVersion(); 145 HardwareConfig hwConfig = getParams().getHardwareConfig(); 146 Density density = hwConfig.getDensity(); 147 boolean isRtl = Bridge.isLocaleRtl(getParams().getLocale()); 148 setLayoutDirection(isRtl ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR); 149 150 if (mBuilder.hasNavBar()) { 151 navBar = createNavBar(getContext(), density, isRtl, getParams().isRtlSupported(), 152 simulatedPlatformVersion); 153 } 154 155 if (builder.hasStatusBar()) { 156 statusBar = createStatusBar(getContext(), density, isRtl, getParams().isRtlSupported(), 157 simulatedPlatformVersion); 158 } 159 160 if (mBuilder.hasAppCompatActionBar()) { 161 BridgeActionBar bar = createActionBar(getContext(), getParams(), true); 162 mContentRoot = bar.getContentRoot(); 163 appCompatActionBar = bar.getRootView(); 164 } 165 166 // Title bar must appear on top of the Action bar 167 if (mBuilder.hasTitleBar()) { 168 titleBar = createTitleBar(getContext(), getParams().getAppLabel(), 169 simulatedPlatformVersion); 170 } else if (mBuilder.hasFrameworkActionBar()) { 171 BridgeActionBar bar = createActionBar(getContext(), getParams(), false); 172 if(mContentRoot == null) { 173 // We only set the content root if the AppCompat action bar did not already 174 // provide it 175 mContentRoot = bar.getContentRoot(); 176 } 177 frameworkActionBar = bar.getRootView(); 178 } 179 180 addViews(titleBar, mContentRoot == null ? (mContentRoot = createContentFrame()) : frameworkActionBar, 181 statusBar, navBar, appCompatActionBar); 182 // Done with the builder. Don't hold a reference to it. 183 mBuilder = null; 184 } 185 186 @NonNull 187 private FrameLayout createContentFrame() { 188 FrameLayout contentRoot = new FrameLayout(getContext()); 189 LayoutParams params = createLayoutParams(MATCH_PARENT, MATCH_PARENT); 190 int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE; 191 if (mBuilder.hasSolidNavBar()) { 192 params.addRule(rule, getId(ID_NAV_BAR)); 193 } 194 int below = -1; 195 if (mBuilder.mAppCompatActionBarSize > 0) { 196 below = getId(ID_APP_COMPAT_ACTION_BAR); 197 } else if (mBuilder.hasFrameworkActionBar() || mBuilder.hasTitleBar()) { 198 below = getId(ID_FRAMEWORK_BAR); 199 } else if (mBuilder.hasSolidStatusBar()) { 200 below = getId(ID_STATUS_BAR); 201 } 202 if (below != -1) { 203 params.addRule(BELOW, below); 204 } 205 contentRoot.setLayoutParams(params); 206 return contentRoot; 207 } 208 209 @NonNull 210 private LayoutParams createLayoutParams(int width, int height) { 211 DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); 212 if (width > 0) { 213 width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width, metrics); 214 } 215 if (height > 0) { 216 height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, metrics); 217 } 218 return new LayoutParams(width, height); 219 } 220 221 @NonNull 222 public FrameLayout getContentRoot() { 223 return mContentRoot; 224 } 225 226 @NonNull 227 private SessionParams getParams() { 228 return mBuilder.mParams; 229 } 230 231 @NonNull 232 @Override 233 public BridgeContext getContext() { 234 return (BridgeContext) super.getContext(); 235 } 236 237 /** 238 * @param isRtl whether the current locale is an RTL locale. 239 * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true in the 240 * manifest and targetSdkVersion >= 17. 241 */ 242 @NonNull 243 private StatusBar createStatusBar(BridgeContext context, Density density, boolean isRtl, 244 boolean isRtlSupported, int simulatedPlatformVersion) { 245 StatusBar statusBar = 246 new StatusBar(context, density, isRtl, isRtlSupported, simulatedPlatformVersion); 247 LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mStatusBarSize); 248 if (mBuilder.isNavBarVertical()) { 249 params.addRule(START_OF, getId(ID_NAV_BAR)); 250 } 251 statusBar.setLayoutParams(params); 252 statusBar.setId(getId(ID_STATUS_BAR)); 253 return statusBar; 254 } 255 256 private BridgeActionBar createActionBar(@NonNull BridgeContext context, 257 @NonNull SessionParams params, boolean appCompatActionBar) { 258 boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG)); 259 String id; 260 261 // For the framework action bar, we set the height to MATCH_PARENT only if there is no 262 // AppCompat ActionBar below it 263 int heightRule = appCompatActionBar || !mBuilder.hasAppCompatActionBar() ? MATCH_PARENT : 264 WRAP_CONTENT; 265 LayoutParams layoutParams = createLayoutParams(MATCH_PARENT, heightRule); 266 int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE; 267 if (mBuilder.hasSolidNavBar()) { 268 // If there 269 if(rule == START_OF || appCompatActionBar || !mBuilder.hasAppCompatActionBar()) { 270 layoutParams.addRule(rule, getId(ID_NAV_BAR)); 271 } 272 } 273 274 275 BridgeActionBar actionBar; 276 if (appCompatActionBar && !isMenu) { 277 actionBar = new AppCompatActionBar(context, params); 278 id = ID_APP_COMPAT_ACTION_BAR; 279 280 if (mBuilder.hasTitleBar() || mBuilder.hasFrameworkActionBar()) { 281 layoutParams.addRule(BELOW, getId(ID_FRAMEWORK_BAR)); 282 } else if (mBuilder.hasSolidStatusBar()) { 283 layoutParams.addRule(BELOW, getId(ID_STATUS_BAR)); 284 } 285 } else { 286 actionBar = new FrameworkActionBar(context, params); 287 id = ID_FRAMEWORK_BAR; 288 if (mBuilder.hasSolidStatusBar()) { 289 layoutParams.addRule(BELOW, getId(ID_STATUS_BAR)); 290 } 291 } 292 293 actionBar.getRootView().setLayoutParams(layoutParams); 294 actionBar.getRootView().setId(getId(id)); 295 actionBar.createMenuPopup(); 296 return actionBar; 297 } 298 299 @NonNull 300 private TitleBar createTitleBar(BridgeContext context, String title, 301 int simulatedPlatformVersion) { 302 TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion); 303 LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mTitleBarSize); 304 if (mBuilder.hasSolidStatusBar()) { 305 params.addRule(BELOW, getId(ID_STATUS_BAR)); 306 } 307 if (mBuilder.isNavBarVertical() && mBuilder.hasSolidNavBar()) { 308 params.addRule(START_OF, getId(ID_NAV_BAR)); 309 } 310 titleBar.setLayoutParams(params); 311 titleBar.setId(getId(ID_FRAMEWORK_BAR)); 312 return titleBar; 313 } 314 315 /** 316 * @param isRtl whether the current locale is an RTL locale. 317 * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true in the 318 * manifest and targetSdkVersion >= 17. 319 */ 320 @NonNull 321 private NavigationBar createNavBar(BridgeContext context, Density density, boolean isRtl, 322 boolean isRtlSupported, int simulatedPlatformVersion) { 323 int orientation = mBuilder.mNavBarOrientation; 324 int size = mBuilder.mNavBarSize; 325 NavigationBar navBar = 326 new NavigationBar(context, density, orientation, isRtl, isRtlSupported, 327 simulatedPlatformVersion); 328 boolean isVertical = mBuilder.isNavBarVertical(); 329 int w = isVertical ? size : MATCH_PARENT; 330 int h = isVertical ? MATCH_PARENT : size; 331 LayoutParams params = createLayoutParams(w, h); 332 params.addRule(isVertical ? ALIGN_PARENT_END : ALIGN_PARENT_BOTTOM); 333 navBar.setLayoutParams(params); 334 navBar.setId(getId(ID_NAV_BAR)); 335 return navBar; 336 } 337 338 private void addViews(@NonNull View... views) { 339 for (View view : views) { 340 if (view != null) { 341 addView(view); 342 } 343 } 344 } 345 346 private int getId(String name) { 347 return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name); 348 } 349 350 @SuppressWarnings("deprecation") 351 @Override 352 public void requestFitSystemWindows() { 353 // The framework call would usually bubble up to ViewRootImpl but, in layoutlib, Layout will 354 // act as view root for most purposes. That way, we can also save going through the Handler 355 // to dispatch the new applied insets. 356 ViewRootImpl root = AttachInfo_Accessor.getRootView(this); 357 if (root != null) { 358 ViewRootImpl_Accessor.dispatchApplyInsets(root, this); 359 } 360 } 361 362 /** 363 * A helper class to help initialize the Layout. 364 */ 365 static class Builder { 366 @NonNull 367 private final SessionParams mParams; 368 @NonNull 369 private final BridgeContext mContext; 370 private final RenderResources mResources; 371 372 private final boolean mWindowIsFloating; 373 private ResourceValue mWindowBackground; 374 private int mStatusBarSize; 375 private int mNavBarSize; 376 private int mNavBarOrientation; 377 private int mAppCompatActionBarSize; 378 private int mFrameworkActionBarSize; 379 private int mTitleBarSize; 380 private boolean mTranslucentStatus; 381 private boolean mTranslucentNav; 382 383 public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) { 384 mParams = params; 385 mContext = context; 386 mResources = mParams.getResources(); 387 mWindowIsFloating = getBooleanThemeValue(mResources, ATTR_WINDOW_FLOATING, true, true); 388 389 findBackground(); 390 391 if (!mParams.isForceNoDecor()) { 392 findStatusBar(); 393 findFrameworkBar(); 394 findAppCompatActionBar(); 395 findNavBar(); 396 } 397 } 398 399 private void findBackground() { 400 if (!mParams.isBgColorOverridden()) { 401 mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true); 402 mWindowBackground = mResources.resolveResValue(mWindowBackground); 403 } 404 } 405 406 private void findStatusBar() { 407 boolean windowFullScreen = 408 getBooleanThemeValue(mResources, ATTR_WINDOW_FULL_SCREEN, true, false); 409 if (!windowFullScreen && !mWindowIsFloating) { 410 mStatusBarSize = 411 getDimension(ATTR_STATUS_BAR_HEIGHT, true, DEFAULT_STATUS_BAR_HEIGHT); 412 mTranslucentStatus = 413 getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_STATUS, true, 414 false); 415 } 416 } 417 418 /** 419 * The behavior is different wether the App is using AppCompat or not. 420 * <h1>With App compat :</h1> 421 * <li> framework ("android:") attributes have to effect 422 * <li> windowNoTile=true hides the AppCompatActionBar 423 * <li> windowActionBar=false throws an exception 424 */ 425 private void findAppCompatActionBar() { 426 if (mWindowIsFloating || !mContext.isAppCompatTheme()) { 427 return; 428 } 429 430 boolean windowNoTitle = 431 getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, false, false); 432 433 boolean windowActionBar = 434 getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR, false, true); 435 436 if (!windowNoTitle && windowActionBar) { 437 mAppCompatActionBarSize = 438 getDimension(ATTR_ACTION_BAR_SIZE, false, DEFAULT_TITLE_BAR_HEIGHT); 439 } 440 } 441 442 /** 443 * Find if we should show either the titleBar or the framework ActionBar 444 * <p> 445 * <h1> Without App compat :</h1> 446 * <li> windowNoTitle has no effect 447 * <li> android:windowNoTile=true hides the <b>ActionBar</b> 448 * <li> android:windowActionBar=true/false toggles between ActionBar/TitleBar 449 * </ul> 450 * <pre> 451 * +------------------------------------------------------------+ 452 * | | android:windowNoTitle | 453 * |android: | TRUE | FALSE | 454 * |windowActionBar|---------------------+----------------------+ 455 * | TRUE | Nothing | ActionBar (Default) | 456 * | FALSE | Nothing | TitleBar | 457 * +---------------+--------------------------------------------+ 458 * </pre> 459 * 460 * @see #findAppCompatActionBar() 461 */ 462 private void findFrameworkBar() { 463 if (mWindowIsFloating) { 464 return; 465 } 466 boolean frameworkWindowNoTitle = 467 getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, true, false); 468 469 // Check if an actionbar is needed 470 boolean isMenu = "menu".equals(mParams.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG)); 471 472 473 boolean windowActionBar = 474 getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR, true, true); 475 476 if (!frameworkWindowNoTitle || isMenu) { 477 if (isMenu || windowActionBar) { 478 mFrameworkActionBarSize = 479 getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT); 480 } else { 481 mTitleBarSize = 482 getDimension(ATTR_WINDOW_TITLE_SIZE, false, DEFAULT_TITLE_BAR_HEIGHT); 483 } 484 } 485 } 486 487 private void findNavBar() { 488 if (hasSoftwareButtons() && !mWindowIsFloating) { 489 490 // get orientation 491 HardwareConfig hwConfig = mParams.getHardwareConfig(); 492 boolean barOnBottom = true; 493 494 if (hwConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { 495 int shortSize = hwConfig.getScreenHeight(); 496 int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / 497 hwConfig.getDensity().getDpiValue(); 498 499 // 0-599dp: "phone" UI with bar on the side 500 // 600+dp: "tablet" UI with bar on the bottom 501 barOnBottom = shortSizeDp >= 600; 502 } 503 504 mNavBarOrientation = barOnBottom ? LinearLayout.HORIZONTAL : VERTICAL; 505 mNavBarSize = 506 getDimension(barOnBottom ? ATTR_NAV_BAR_HEIGHT : ATTR_NAV_BAR_WIDTH, true, 507 DEFAULT_NAV_BAR_SIZE); 508 mTranslucentNav = 509 getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_NAV, true, false); 510 } 511 } 512 513 @SuppressWarnings("SameParameterValue") 514 private int getDimension(String attr, boolean isFramework, int defaultValue) { 515 ResourceValue value = mResources.findItemInTheme(attr, isFramework); 516 value = mResources.resolveResValue(value); 517 if (value != null) { 518 TypedValue typedValue = ResourceHelper.getValue(attr, value.getValue(), true); 519 if (typedValue != null) { 520 return (int) typedValue.getDimension(mContext.getMetrics()); 521 } 522 } 523 return defaultValue; 524 } 525 526 private boolean hasSoftwareButtons() { 527 return mParams.getHardwareConfig().hasSoftwareButtons(); 528 } 529 530 /** 531 * Return true if the nav bar is present and not translucent 532 */ 533 private boolean hasSolidNavBar() { 534 return hasNavBar() && !mTranslucentNav; 535 } 536 537 /** 538 * Return true if the status bar is present and not translucent 539 */ 540 private boolean hasSolidStatusBar() { 541 return hasStatusBar() && !mTranslucentStatus; 542 } 543 544 private boolean hasNavBar() { 545 return Config.showOnScreenNavBar(mParams.getSimulatedPlatformVersion()) && 546 hasSoftwareButtons() && mNavBarSize > 0; 547 } 548 549 private boolean hasTitleBar() { 550 return mTitleBarSize > 0; 551 } 552 553 private boolean hasStatusBar() { 554 return mStatusBarSize > 0; 555 } 556 557 private boolean hasAppCompatActionBar() { 558 return mAppCompatActionBarSize > 0; 559 } 560 561 /** 562 * Return true if the nav bar is present and is vertical. 563 */ 564 private boolean isNavBarVertical() { 565 return hasNavBar() && mNavBarOrientation == VERTICAL; 566 } 567 568 private boolean hasFrameworkActionBar() { 569 return mFrameworkActionBarSize > 0; 570 } 571 } 572} 573