/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.layoutlib.bridge.impl; import com.android.ide.common.rendering.api.HardwareConfig; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.StyleResourceValue; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.bars.AppCompatActionBar; import com.android.layoutlib.bridge.bars.BridgeActionBar; import com.android.layoutlib.bridge.bars.Config; import com.android.layoutlib.bridge.bars.FrameworkActionBar; import com.android.layoutlib.bridge.bars.NavigationBar; import com.android.layoutlib.bridge.bars.StatusBar; import com.android.layoutlib.bridge.bars.TitleBar; import com.android.resources.Density; import com.android.resources.ResourceType; import com.android.resources.ScreenOrientation; import android.annotation.NonNull; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.widget.LinearLayout.VERTICAL; import static com.android.layoutlib.bridge.impl.ResourceHelper.getBooleanThemeValue; /** * The Layout used to create the system decor. * * The layout inflated will contain a content frame where the user's layout can be inflated. * <pre> * +-------------------------------------------------+---+ * | Status bar | N | * +-------------------------------------------------+ a | * | Title/Action bar (optional) | v | * +-------------------------------------------------+ | * | Content, vertical extending | b | * | | a | * | | r | * +-------------------------------------------------+---+ * </pre> * or * <pre> * +-------------------------------------+ * | Status bar | * +-------------------------------------+ * | Title/Action bar (optional) | * +-------------------------------------+ * | Content, vertical extending | * | | * | | * +-------------------------------------+ * | Nav bar | * +-------------------------------------+ * </pre> * */ class Layout extends RelativeLayout { // Theme attributes used for configuring appearance of the system decor. private static final String ATTR_WINDOW_FLOATING = "windowIsFloating"; private static final String ATTR_WINDOW_BACKGROUND = "windowBackground"; private static final String ATTR_WINDOW_FULL_SCREEN = "windowFullscreen"; private static final String ATTR_NAV_BAR_HEIGHT = "navigation_bar_height"; private static final String ATTR_NAV_BAR_WIDTH = "navigation_bar_width"; private static final String ATTR_STATUS_BAR_HEIGHT = "status_bar_height"; private static final String ATTR_WINDOW_ACTION_BAR = "windowActionBar"; private static final String ATTR_ACTION_BAR_SIZE = "actionBarSize"; private static final String ATTR_WINDOW_NO_TITLE = "windowNoTitle"; private static final String ATTR_WINDOW_TITLE_SIZE = "windowTitleSize"; private static final String ATTR_WINDOW_TRANSLUCENT_STATUS = StatusBar.ATTR_TRANSLUCENT; private static final String ATTR_WINDOW_TRANSLUCENT_NAV = NavigationBar.ATTR_TRANSLUCENT; private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat"; // Default sizes private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; private static final int DEFAULT_NAV_BAR_SIZE = 48; // Ids assigned to components created. This is so that we can refer to other components in // layout params. private static final String ID_NAV_BAR = "navBar"; private static final String ID_STATUS_BAR = "statusBar"; private static final String ID_TITLE_BAR = "titleBar"; // Prefix used with the above ids in order to make them unique in framework namespace. private static final String ID_PREFIX = "android_layoutlib_"; /** * Temporarily store the builder so that it doesn't have to be passed to all methods used * during inflation. */ private Builder mBuilder; /** * This holds user's layout. */ private FrameLayout mContentRoot; public Layout(@NonNull Builder builder) { super(builder.mContext); mBuilder = builder; if (builder.mWindowBackground != null) { Drawable d = ResourceHelper.getDrawable(builder.mWindowBackground, builder.mContext); setBackground(d); } int simulatedPlatformVersion = getParams().getSimulatedPlatformVersion(); HardwareConfig hwConfig = getParams().getHardwareConfig(); Density density = hwConfig.getDensity(); boolean isRtl = Bridge.isLocaleRtl(getParams().getLocale()); setLayoutDirection(isRtl? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR); NavigationBar navBar = null; if (mBuilder.hasNavBar()) { navBar = createNavBar(getContext(), density, isRtl, getParams().isRtlSupported(), simulatedPlatformVersion); } StatusBar statusBar = null; if (builder.mStatusBarSize > 0) { statusBar = createStatusBar(getContext(), density, isRtl, getParams().isRtlSupported(), simulatedPlatformVersion); } View actionBar = null; TitleBar titleBar = null; if (builder.mActionBarSize > 0) { BridgeActionBar bar = createActionBar(getContext(), getParams()); mContentRoot = bar.getContentRoot(); actionBar = bar.getRootView(); } else if (mBuilder.mTitleBarSize > 0) { titleBar = createTitleBar(getContext(), getParams().getAppLabel(), simulatedPlatformVersion); } addViews(titleBar, mContentRoot == null ? (mContentRoot = createContentFrame()) : actionBar, statusBar, navBar); // Done with the builder. Don't hold a reference to it. mBuilder = null; } @NonNull private FrameLayout createContentFrame() { FrameLayout contentRoot = new FrameLayout(getContext()); LayoutParams params = createLayoutParams(MATCH_PARENT, MATCH_PARENT); int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE; if (mBuilder.hasNavBar() && mBuilder.solidBars()) { params.addRule(rule, getId(ID_NAV_BAR)); } int below = -1; if (mBuilder.mActionBarSize <= 0 && mBuilder.mTitleBarSize > 0) { below = getId(ID_TITLE_BAR); } else if (mBuilder.hasStatusBar() && mBuilder.solidBars()) { below = getId(ID_STATUS_BAR); } if (below != -1) { params.addRule(BELOW, below); } contentRoot.setLayoutParams(params); return contentRoot; } @NonNull private LayoutParams createLayoutParams(int width, int height) { DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); if (width > 0) { width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, width, metrics); } if (height > 0) { height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, metrics); } return new LayoutParams(width, height); } @NonNull public FrameLayout getContentRoot() { return mContentRoot; } @NonNull private SessionParams getParams() { return mBuilder.mParams; } @NonNull @Override public BridgeContext getContext(){ return (BridgeContext) super.getContext(); } /** * @param isRtl whether the current locale is an RTL locale. * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true * in the manifest and targetSdkVersion >= 17. */ @NonNull private StatusBar createStatusBar(BridgeContext context, Density density, boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion) { StatusBar statusBar = new StatusBar(context, density, isRtl, isRtlSupported, simulatedPlatformVersion); LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mStatusBarSize); if (mBuilder.isNavBarVertical()) { params.addRule(START_OF, getId(ID_NAV_BAR)); } statusBar.setLayoutParams(params); statusBar.setId(getId(ID_STATUS_BAR)); return statusBar; } private BridgeActionBar createActionBar(@NonNull BridgeContext context, @NonNull SessionParams params) { BridgeActionBar actionBar; if (mBuilder.isThemeAppCompat()) { actionBar = new AppCompatActionBar(context, params); } else { actionBar = new FrameworkActionBar(context, params); } LayoutParams layoutParams = createLayoutParams(MATCH_PARENT, MATCH_PARENT); int rule = mBuilder.isNavBarVertical() ? START_OF : ABOVE; if (mBuilder.hasNavBar() && mBuilder.solidBars()) { layoutParams.addRule(rule, getId(ID_NAV_BAR)); } if (mBuilder.hasStatusBar() && mBuilder.solidBars()) { layoutParams.addRule(BELOW, getId(ID_STATUS_BAR)); } actionBar.getRootView().setLayoutParams(layoutParams); actionBar.createMenuPopup(); return actionBar; } @NonNull private TitleBar createTitleBar(BridgeContext context, String title, int simulatedPlatformVersion) { TitleBar titleBar = new TitleBar(context, title, simulatedPlatformVersion); LayoutParams params = createLayoutParams(MATCH_PARENT, mBuilder.mTitleBarSize); if (mBuilder.hasStatusBar() && mBuilder.solidBars()) { params.addRule(BELOW, getId(ID_STATUS_BAR)); } if (mBuilder.isNavBarVertical() && mBuilder.solidBars()) { params.addRule(START_OF, getId(ID_NAV_BAR)); } titleBar.setLayoutParams(params); titleBar.setId(getId(ID_TITLE_BAR)); return titleBar; } /** * @param isRtl whether the current locale is an RTL locale. * @param isRtlSupported whether the applications supports RTL (i.e. has supportsRtl=true * in the manifest and targetSdkVersion >= 17. */ @NonNull private NavigationBar createNavBar(BridgeContext context, Density density, boolean isRtl, boolean isRtlSupported, int simulatedPlatformVersion) { int orientation = mBuilder.mNavBarOrientation; int size = mBuilder.mNavBarSize; NavigationBar navBar = new NavigationBar(context, density, orientation, isRtl, isRtlSupported, simulatedPlatformVersion); boolean isVertical = mBuilder.isNavBarVertical(); int w = isVertical ? size : MATCH_PARENT; int h = isVertical ? MATCH_PARENT : size; LayoutParams params = createLayoutParams(w, h); params.addRule(isVertical ? ALIGN_PARENT_END : ALIGN_PARENT_BOTTOM); navBar.setLayoutParams(params); navBar.setId(getId(ID_NAV_BAR)); return navBar; } private void addViews(@NonNull View... views) { for (View view : views) { if (view != null) { addView(view); } } } private int getId(String name) { return Bridge.getResourceId(ResourceType.ID, ID_PREFIX + name); } /** * A helper class to help initialize the Layout. */ static class Builder { @NonNull private final SessionParams mParams; @NonNull private final BridgeContext mContext; private final RenderResources mResources; private final boolean mWindowIsFloating; private ResourceValue mWindowBackground; private int mStatusBarSize; private int mNavBarSize; private int mNavBarOrientation; private int mActionBarSize; private int mTitleBarSize; private boolean mTranslucentStatus; private boolean mTranslucentNav; private Boolean mIsThemeAppCompat; public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) { mParams = params; mContext = context; mResources = mParams.getResources(); mWindowIsFloating = getBooleanThemeValue(mResources, ATTR_WINDOW_FLOATING, true, true); findBackground(); if (!mParams.isForceNoDecor()) { findStatusBar(); findActionBar(); findNavBar(); } } private void findBackground() { if (!mParams.isBgColorOverridden()) { mWindowBackground = mResources.findItemInTheme(ATTR_WINDOW_BACKGROUND, true); mWindowBackground = mResources.resolveResValue(mWindowBackground); } } private void findStatusBar() { boolean windowFullScreen = getBooleanThemeValue(mResources, ATTR_WINDOW_FULL_SCREEN, true, false); if (!windowFullScreen && !mWindowIsFloating) { mStatusBarSize = getDimension(ATTR_STATUS_BAR_HEIGHT, true, DEFAULT_STATUS_BAR_HEIGHT); mTranslucentStatus = getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_STATUS, true, false); } } private void findActionBar() { if (mWindowIsFloating) { return; } // Check if an actionbar is needed boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR, !isThemeAppCompat(), true); if (windowActionBar) { mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT); } else { // Maybe the gingerbread era title bar is needed boolean windowNoTitle = getBooleanThemeValue(mResources, ATTR_WINDOW_NO_TITLE, true, false); if (!windowNoTitle) { mTitleBarSize = getDimension(ATTR_WINDOW_TITLE_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT); } } } private void findNavBar() { if (hasSoftwareButtons() && !mWindowIsFloating) { // get orientation HardwareConfig hwConfig = mParams.getHardwareConfig(); boolean barOnBottom = true; if (hwConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { int shortSize = hwConfig.getScreenHeight(); int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / hwConfig.getDensity().getDpiValue(); // 0-599dp: "phone" UI with bar on the side // 600+dp: "tablet" UI with bar on the bottom barOnBottom = shortSizeDp >= 600; } mNavBarOrientation = barOnBottom ? LinearLayout.HORIZONTAL : VERTICAL; mNavBarSize = getDimension(barOnBottom ? ATTR_NAV_BAR_HEIGHT : ATTR_NAV_BAR_WIDTH, true, DEFAULT_NAV_BAR_SIZE); mTranslucentNav = getBooleanThemeValue(mResources, ATTR_WINDOW_TRANSLUCENT_NAV, true, false); } } private int getDimension(String attr, boolean isFramework, int defaultValue) { ResourceValue value = mResources.findItemInTheme(attr, isFramework); value = mResources.resolveResValue(value); if (value != null) { TypedValue typedValue = ResourceHelper.getValue(attr, value.getValue(), true); if (typedValue != null) { return (int) typedValue.getDimension(mContext.getMetrics()); } } return defaultValue; } private boolean hasSoftwareButtons() { return mParams.getHardwareConfig().hasSoftwareButtons(); } private boolean isThemeAppCompat() { // If a cached value exists, return it. if (mIsThemeAppCompat != null) { return mIsThemeAppCompat; } // Ideally, we should check if the corresponding activity extends // android.support.v7.app.ActionBarActivity, and not care about the theme name at all. StyleResourceValue defaultTheme = mResources.getDefaultTheme(); // We can't simply check for parent using resources.themeIsParentOf() since the // inheritance structure isn't really what one would expect. The first common parent // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21). boolean isThemeAppCompat = false; for (int i = 0; i < 50; i++) { if (defaultTheme == null) { break; } // for loop ensures that we don't run into cyclic theme inheritance. if (defaultTheme.getName().startsWith(PREFIX_THEME_APPCOMPAT)) { isThemeAppCompat = true; break; } defaultTheme = mResources.getParent(defaultTheme); } mIsThemeAppCompat = isThemeAppCompat; return isThemeAppCompat; } /** * Return true if the status bar or nav bar are present, they are not translucent (i.e * content doesn't overlap with them). */ private boolean solidBars() { return !(hasNavBar() && mTranslucentNav) && !(hasStatusBar() && mTranslucentStatus); } private boolean hasNavBar() { return Config.showOnScreenNavBar(mParams.getSimulatedPlatformVersion()) && hasSoftwareButtons() && mNavBarSize > 0; } private boolean hasStatusBar() { return mStatusBarSize > 0; } /** * Return true if the nav bar is present and is vertical. */ private boolean isNavBarVertical() { return hasNavBar() && mNavBarOrientation == VERTICAL; } } }