diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index d0e431acadff9..1d5ac0ca7346b 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -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/, @+id/ and @android:id/ // 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(). diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index f38fb1909c17b..3667f584fc663 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -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 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 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. *

@@ -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); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 194eecc245db1..72ac4c3f90691 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -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 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; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java index 1afd90d39f31c..537fa7744c2a9 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Layout.java @@ -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). diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java index a21de56066cb2..c197e40eb4cff 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java @@ -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(