/*
* 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;
}
}
}