Merge "Add support for AppCompat widgets"

This commit is contained in:
Diego Perez
2016-06-27 15:02:38 +00:00
committed by Android (Google) Code Review
5 changed files with 103 additions and 51 deletions

View File

@@ -584,6 +584,7 @@ public final class BridgeTypedArray extends TypedArray {
if (value == null) {
return defValue;
}
value = value.trim();
// if the value is just an integer, return it.
try {
@@ -595,6 +596,11 @@ public final class BridgeTypedArray extends TypedArray {
// pass
}
if (value.startsWith("#")) {
// this looks like a color, do not try to parse it
return defValue;
}
// Handle the @id/<name>, @+id/<name> and @android:id/<name>
// We need to return the exact value that was compiled (from the various R classes),
// as these values can be reused internally with calls to findViewById().

View File

@@ -42,9 +42,24 @@ import android.util.AttributeSet;
import android.widget.NumberPicker;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.IMAGE_BUTTON;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.SEEK_BAR;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
/**
@@ -53,6 +68,13 @@ import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
public final class BridgeInflater extends LayoutInflater {
private final LayoutlibCallback mLayoutlibCallback;
/**
* If true, the inflater will try to replace the framework widgets with the AppCompat versions.
* Ideally, this should be based on the activity being an AppCompat activity but since that is
* not trivial to check from layoutlib, we currently base the decision on the current theme
* being an AppCompat theme.
*/
private boolean mLoadAppCompatViews;
private boolean mIsInMerge = false;
private ResourceReference mResourceReference;
private Map<View, String> mOpenDrawerLayouts;
@@ -60,6 +82,15 @@ public final class BridgeInflater extends LayoutInflater {
// Keep in sync with the same value in LayoutInflater.
private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
private static final String APPCOMPAT_WIDGET_PREFIX = "android.support.v7.widget.AppCompat";
/** List of platform widgets that have an AppCompat version */
private static final Set<String> APPCOMPAT_VIEWS = Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList(TEXT_VIEW, "ImageSwitcher", BUTTON, EDIT_TEXT, SPINNER,
IMAGE_BUTTON, CHECK_BOX, RADIO_BUTTON, CHECKED_TEXT_VIEW,
AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW, "RatingBar",
SEEK_BAR)));
/**
* List of class prefixes which are tried first by default.
* <p/>
@@ -75,13 +106,15 @@ public final class BridgeInflater extends LayoutInflater {
return sClassPrefixList;
}
protected BridgeInflater(LayoutInflater original, Context newContext) {
private BridgeInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
newContext = getBaseContext(newContext);
if (newContext instanceof BridgeContext) {
mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback();
mLoadAppCompatViews = ((BridgeContext) newContext).isAppCompatTheme();
} else {
mLayoutlibCallback = null;
mLoadAppCompatViews = false;
}
}
@@ -91,10 +124,11 @@ public final class BridgeInflater extends LayoutInflater {
* @param context The Android application context.
* @param layoutlibCallback the {@link LayoutlibCallback} object.
*/
public BridgeInflater(Context context, LayoutlibCallback layoutlibCallback) {
public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
super(context);
mLayoutlibCallback = layoutlibCallback;
mConstructorArgs[0] = context;
mLoadAppCompatViews = context.isAppCompatTheme();
}
@Override
@@ -102,28 +136,38 @@ public final class BridgeInflater extends LayoutInflater {
View view = null;
try {
// First try to find a class using the default Android prefixes
for (String prefix : sClassPrefixList) {
if (mLoadAppCompatViews && APPCOMPAT_VIEWS.contains(name)) {
// We are using an AppCompat theme so try to load the appcompat views
view = loadCustomView(APPCOMPAT_WIDGET_PREFIX + name, attrs);
if (view == null) {
mLoadAppCompatViews = false; // Do not try anymore
}
} else {
// First try to find a class using the default Android prefixes
for (String prefix : sClassPrefixList) {
try {
view = createView(name, prefix, attrs);
if (view != null) {
break;
}
} catch (ClassNotFoundException e) {
// Ignore. We'll try again using the base class below.
}
}
// Next try using the parent loader. This will most likely only work for
// fully-qualified class names.
try {
view = createView(name, prefix, attrs);
if (view != null) {
break;
if (view == null) {
view = super.onCreateView(name, attrs);
}
} catch (ClassNotFoundException e) {
// Ignore. We'll try again using the base class below.
// Ignore. We'll try again using the custom view loader below.
}
}
// Next try using the parent loader. This will most likely only work for
// fully-qualified class names.
try {
if (view == null) {
view = super.onCreateView(name, attrs);
}
} catch (ClassNotFoundException e) {
// Ignore. We'll try again using the custom view loader below.
}
// Finally try again using the custom view loader
if (view == null) {
view = loadCustomView(name, attrs);

View File

@@ -110,6 +110,7 @@ import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_AP
*/
@SuppressWarnings("deprecation") // For use of Pair.
public final class BridgeContext extends Context {
private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
/** The map adds cookies to each view so that IDE can link xml tags to views. */
private final HashMap<View, Object> mViewKeyMap = new HashMap<>();
@@ -153,6 +154,7 @@ public final class BridgeContext extends Context {
private ClassLoader mClassLoader;
private IBinder mBinder;
private PackageManager mPackageManager;
private Boolean mIsThemeAppCompat;
/**
* Some applications that target both pre API 17 and post API 17, set the newer attrs to
@@ -479,6 +481,36 @@ public final class BridgeContext extends Context {
return Pair.of(null, Boolean.FALSE);
}
/**
* Returns whether the current selected theme is based on AppCompat
*/
public boolean isAppCompatTheme() {
// 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 = mRenderResources.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 = mRenderResources.getParent(defaultTheme);
}
mIsThemeAppCompat = isThemeAppCompat;
return isThemeAppCompat;
}
@SuppressWarnings("deprecation")
private ILayoutPullParser getParser(ResourceReference resource) {
ILayoutPullParser parser;

View File

@@ -20,7 +20,6 @@ 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.android.RenderParamsFlags;
@@ -94,7 +93,6 @@ class Layout extends RelativeLayout {
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;
@@ -236,7 +234,7 @@ class Layout extends RelativeLayout {
boolean isMenu = "menu".equals(params.getFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG));
BridgeActionBar actionBar;
if (mBuilder.isThemeAppCompat() && !isMenu) {
if (context.isAppCompatTheme() && !isMenu) {
actionBar = new AppCompatActionBar(context, params);
} else {
actionBar = new FrameworkActionBar(context, params);
@@ -324,8 +322,6 @@ class Layout extends RelativeLayout {
private boolean mTranslucentStatus;
private boolean mTranslucentNav;
private Boolean mIsThemeAppCompat;
public Builder(@NonNull SessionParams params, @NonNull BridgeContext context) {
mParams = params;
mContext = context;
@@ -365,7 +361,7 @@ class Layout extends RelativeLayout {
}
// Check if an actionbar is needed
boolean windowActionBar = getBooleanThemeValue(mResources, ATTR_WINDOW_ACTION_BAR,
!isThemeAppCompat(), true);
!mContext.isAppCompatTheme(), true);
if (windowActionBar) {
mActionBarSize = getDimension(ATTR_ACTION_BAR_SIZE, true, DEFAULT_TITLE_BAR_HEIGHT);
} else {
@@ -420,33 +416,6 @@ class Layout extends RelativeLayout {
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).

View File

@@ -77,6 +77,7 @@ public final class ResourceHelper {
*/
public static int getColor(String value) {
if (value != null) {
value = value.trim();
if (!value.startsWith("#")) {
if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) {
throw new NumberFormatException(String.format(