Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2008 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 
     17 package android.view;
     18 
     19 import com.android.ide.common.rendering.api.LayoutLog;
     20 import com.android.ide.common.rendering.api.LayoutlibCallback;
     21 import com.android.ide.common.rendering.api.MergeCookie;
     22 import com.android.ide.common.rendering.api.ResourceReference;
     23 import com.android.ide.common.rendering.api.ResourceValue;
     24 import com.android.layoutlib.bridge.Bridge;
     25 import com.android.layoutlib.bridge.BridgeConstants;
     26 import com.android.layoutlib.bridge.MockView;
     27 import com.android.layoutlib.bridge.android.BridgeContext;
     28 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
     29 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
     30 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
     31 import com.android.layoutlib.bridge.impl.ParserFactory;
     32 import com.android.layoutlib.bridge.util.ReflectionUtils;
     33 import com.android.resources.ResourceType;
     34 import com.android.util.Pair;
     35 
     36 import org.xmlpull.v1.XmlPullParser;
     37 
     38 import android.annotation.NonNull;
     39 import android.content.Context;
     40 import android.content.res.TypedArray;
     41 import android.graphics.drawable.Animatable;
     42 import android.graphics.drawable.Drawable;
     43 import android.util.AttributeSet;
     44 import android.widget.ImageView;
     45 import android.widget.NumberPicker;
     46 
     47 import java.io.File;
     48 import java.util.Arrays;
     49 import java.util.Collections;
     50 import java.util.HashMap;
     51 import java.util.HashSet;
     52 import java.util.Map;
     53 import java.util.Set;
     54 
     55 import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
     56 import static com.android.SdkConstants.BUTTON;
     57 import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
     58 import static com.android.SdkConstants.CHECK_BOX;
     59 import static com.android.SdkConstants.EDIT_TEXT;
     60 import static com.android.SdkConstants.IMAGE_BUTTON;
     61 import static com.android.SdkConstants.IMAGE_VIEW;
     62 import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
     63 import static com.android.SdkConstants.RADIO_BUTTON;
     64 import static com.android.SdkConstants.SEEK_BAR;
     65 import static com.android.SdkConstants.SPINNER;
     66 import static com.android.SdkConstants.TEXT_VIEW;
     67 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
     68 
     69 /**
     70  * Custom implementation of {@link LayoutInflater} to handle custom views.
     71  */
     72 public final class BridgeInflater extends LayoutInflater {
     73 
     74     private final LayoutlibCallback mLayoutlibCallback;
     75     /**
     76      * If true, the inflater will try to replace the framework widgets with the AppCompat versions.
     77      * Ideally, this should be based on the activity being an AppCompat activity but since that is
     78      * not trivial to check from layoutlib, we currently base the decision on the current theme
     79      * being an AppCompat theme.
     80      */
     81     private boolean mLoadAppCompatViews;
     82     /**
     83      * This set contains the framework views that have an AppCompat version but failed to load.
     84      * This might happen because not all widgets are contained in all versions of the support
     85      * library.
     86      * This will help us to avoid trying to load the AppCompat version multiple times if it
     87      * doesn't exist.
     88      */
     89     private Set<String> mFailedAppCompatViews = new HashSet<>();
     90     private boolean mIsInMerge = false;
     91     private ResourceReference mResourceReference;
     92     private Map<View, String> mOpenDrawerLayouts;
     93 
     94     // Keep in sync with the same value in LayoutInflater.
     95     private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
     96 
     97     private static final String APPCOMPAT_WIDGET_PREFIX = "android.support.v7.widget.AppCompat";
     98     /** List of platform widgets that have an AppCompat version */
     99     private static final Set<String> APPCOMPAT_VIEWS = Collections.unmodifiableSet(
    100             new HashSet<>(
    101                     Arrays.asList(TEXT_VIEW, IMAGE_VIEW, BUTTON, EDIT_TEXT, SPINNER,
    102                             IMAGE_BUTTON, CHECK_BOX, RADIO_BUTTON, CHECKED_TEXT_VIEW,
    103                             AUTO_COMPLETE_TEXT_VIEW, MULTI_AUTO_COMPLETE_TEXT_VIEW, "RatingBar",
    104                             SEEK_BAR)));
    105 
    106     /**
    107      * List of class prefixes which are tried first by default.
    108      * <p/>
    109      * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
    110      */
    111     private static final String[] sClassPrefixList = {
    112         "android.widget.",
    113         "android.webkit.",
    114         "android.app."
    115     };
    116 
    117     public static String[] getClassPrefixList() {
    118         return sClassPrefixList;
    119     }
    120 
    121     private BridgeInflater(LayoutInflater original, Context newContext) {
    122         super(original, newContext);
    123         newContext = getBaseContext(newContext);
    124         if (newContext instanceof BridgeContext) {
    125             mLayoutlibCallback = ((BridgeContext) newContext).getLayoutlibCallback();
    126             mLoadAppCompatViews = ((BridgeContext) newContext).isAppCompatTheme();
    127         } else {
    128             mLayoutlibCallback = null;
    129             mLoadAppCompatViews = false;
    130         }
    131     }
    132 
    133     /**
    134      * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
    135      *
    136      * @param context The Android application context.
    137      * @param layoutlibCallback the {@link LayoutlibCallback} object.
    138      */
    139     public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
    140         super(context);
    141         mLayoutlibCallback = layoutlibCallback;
    142         mConstructorArgs[0] = context;
    143         mLoadAppCompatViews = context.isAppCompatTheme();
    144     }
    145 
    146     @Override
    147     public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    148         View view = null;
    149 
    150         try {
    151             if (mLoadAppCompatViews
    152                     && APPCOMPAT_VIEWS.contains(name)
    153                     && !mFailedAppCompatViews.contains(name)) {
    154                 // We are using an AppCompat theme so try to load the appcompat views
    155                 view = loadCustomView(APPCOMPAT_WIDGET_PREFIX + name, attrs, true);
    156 
    157                 if (view == null) {
    158                     mFailedAppCompatViews.add(name); // Do not try this one anymore
    159                 }
    160             }
    161 
    162             if (view == null) {
    163                 // First try to find a class using the default Android prefixes
    164                 for (String prefix : sClassPrefixList) {
    165                     try {
    166                         view = createView(name, prefix, attrs);
    167                         if (view != null) {
    168                             break;
    169                         }
    170                     } catch (ClassNotFoundException e) {
    171                         // Ignore. We'll try again using the base class below.
    172                     }
    173                 }
    174 
    175                 // Next try using the parent loader. This will most likely only work for
    176                 // fully-qualified class names.
    177                 try {
    178                     if (view == null) {
    179                         view = super.onCreateView(name, attrs);
    180                     }
    181                 } catch (ClassNotFoundException e) {
    182                     // Ignore. We'll try again using the custom view loader below.
    183                 }
    184             }
    185 
    186             // Finally try again using the custom view loader
    187             if (view == null) {
    188                 view = loadCustomView(name, attrs);
    189             }
    190         } catch (InflateException e) {
    191             // Don't catch the InflateException below as that results in hiding the real cause.
    192             throw e;
    193         } catch (Exception e) {
    194             // Wrap the real exception in a ClassNotFoundException, so that the calling method
    195             // can deal with it.
    196             throw new ClassNotFoundException("onCreateView", e);
    197         }
    198 
    199         setupViewInContext(view, attrs);
    200 
    201         return view;
    202     }
    203 
    204     @Override
    205     public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
    206             boolean ignoreThemeAttr) {
    207         View view = null;
    208         if (name.equals("view")) {
    209             // This is usually done by the superclass but this allows us catching the error and
    210             // reporting something useful.
    211             name = attrs.getAttributeValue(null, "class");
    212 
    213             if (name == null) {
    214                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " +
    215                   "class attribute", null);
    216                 // We weren't able to resolve the view so we just pass a mock View to be able to
    217                 // continue rendering.
    218                 view = new MockView(context, attrs);
    219                 ((MockView) view).setText("view");
    220             }
    221         }
    222 
    223         try {
    224             if (view == null) {
    225                 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
    226             }
    227         } catch (InflateException e) {
    228             // Creation of ContextThemeWrapper code is same as in the super method.
    229             // Apply a theme wrapper, if allowed and one is specified.
    230             if (!ignoreThemeAttr) {
    231                 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
    232                 final int themeResId = ta.getResourceId(0, 0);
    233                 if (themeResId != 0) {
    234                     context = new ContextThemeWrapper(context, themeResId);
    235                 }
    236                 ta.recycle();
    237             }
    238             if (!(e.getCause() instanceof ClassNotFoundException)) {
    239                 // There is some unknown inflation exception in inflating a View that was found.
    240                 view = new MockView(context, attrs);
    241                 ((MockView) view).setText(name);
    242                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
    243             } else {
    244                 final Object lastContext = mConstructorArgs[0];
    245                 mConstructorArgs[0] = context;
    246                 // try to load the class from using the custom view loader
    247                 try {
    248                     view = loadCustomView(name, attrs);
    249                 } catch (Exception e2) {
    250                     // Wrap the real exception in an InflateException so that the calling
    251                     // method can deal with it.
    252                     InflateException exception = new InflateException();
    253                     if (!e2.getClass().equals(ClassNotFoundException.class)) {
    254                         exception.initCause(e2);
    255                     } else {
    256                         exception.initCause(e);
    257                     }
    258                     throw exception;
    259                 } finally {
    260                     mConstructorArgs[0] = lastContext;
    261                 }
    262             }
    263         }
    264 
    265         setupViewInContext(view, attrs);
    266 
    267         return view;
    268     }
    269 
    270     @Override
    271     public View inflate(int resource, ViewGroup root) {
    272         Context context = getContext();
    273         context = getBaseContext(context);
    274         if (context instanceof BridgeContext) {
    275             BridgeContext bridgeContext = (BridgeContext)context;
    276 
    277             ResourceValue value = null;
    278 
    279             @SuppressWarnings("deprecation")
    280             Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
    281             if (layoutInfo != null) {
    282                 value = bridgeContext.getRenderResources().getFrameworkResource(
    283                         ResourceType.LAYOUT, layoutInfo.getSecond());
    284             } else {
    285                 layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
    286 
    287                 if (layoutInfo != null) {
    288                     value = bridgeContext.getRenderResources().getProjectResource(
    289                             ResourceType.LAYOUT, layoutInfo.getSecond());
    290                 }
    291             }
    292 
    293             if (value != null) {
    294                 File f = new File(value.getValue());
    295                 if (f.isFile()) {
    296                     try {
    297                         XmlPullParser parser = ParserFactory.create(f, true);
    298 
    299                         BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
    300                                 parser, bridgeContext, value.isFramework());
    301 
    302                         return inflate(bridgeParser, root);
    303                     } catch (Exception e) {
    304                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
    305                                 "Failed to parse file " + f.getAbsolutePath(), e, null);
    306 
    307                         return null;
    308                     }
    309                 }
    310             }
    311         }
    312         return null;
    313     }
    314 
    315     /**
    316      * Instantiates the given view name and returns the instance. If the view doesn't exist, a
    317      * MockView or null might be returned.
    318      * @param name the custom view name
    319      * @param attrs the {@link AttributeSet} to be passed to the view constructor
    320      * @param silent if true, errors while loading the view won't be reported and, if the view
    321      * doesn't exist, null will be returned.
    322      */
    323     private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception {
    324         if (mLayoutlibCallback != null) {
    325             // first get the classname in case it's not the node name
    326             if (name.equals("view")) {
    327                 name = attrs.getAttributeValue(null, "class");
    328                 if (name == null) {
    329                     return null;
    330                 }
    331             }
    332 
    333             mConstructorArgs[1] = attrs;
    334 
    335             Object customView = silent ?
    336                     mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs)
    337                     : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs);
    338 
    339             if (customView instanceof View) {
    340                 return (View)customView;
    341             }
    342         }
    343 
    344         return null;
    345     }
    346 
    347     private View loadCustomView(String name, AttributeSet attrs) throws Exception {
    348         return loadCustomView(name, attrs, false);
    349     }
    350 
    351     private void setupViewInContext(View view, AttributeSet attrs) {
    352         Context context = getContext();
    353         context = getBaseContext(context);
    354         if (context instanceof BridgeContext) {
    355             BridgeContext bc = (BridgeContext) context;
    356             // get the view key
    357             Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
    358             if (viewKey != null) {
    359                 bc.addViewKey(view, viewKey);
    360             }
    361             String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
    362             if (scrollPosX != null && scrollPosX.endsWith("px")) {
    363                 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
    364                 bc.setScrollXPos(view, value);
    365             }
    366             String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
    367             if (scrollPosY != null && scrollPosY.endsWith("px")) {
    368                 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
    369                 bc.setScrollYPos(view, value);
    370             }
    371             if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
    372                 Integer resourceId = null;
    373                 String attrListItemValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
    374                         BridgeConstants.ATTR_LIST_ITEM);
    375                 int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI,
    376                         BridgeConstants.ATTR_ITEM_COUNT, -1);
    377                 if (attrListItemValue != null && !attrListItemValue.isEmpty()) {
    378                     ResourceValue resValue = bc.getRenderResources().findResValue(attrListItemValue, false);
    379                     if (resValue.isFramework()) {
    380                         resourceId = Bridge.getResourceId(resValue.getResourceType(),
    381                                 resValue.getName());
    382                     } else {
    383                         resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(),
    384                                 resValue.getName());
    385                     }
    386                 }
    387                 if (resourceId == null) {
    388                     resourceId = 0;
    389                 }
    390                 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue);
    391             } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
    392                 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
    393                         BridgeConstants.ATTR_OPEN_DRAWER);
    394                 if (attrVal != null) {
    395                     getDrawerLayoutMap().put(view, attrVal);
    396                 }
    397             }
    398             else if (view instanceof NumberPicker) {
    399                 NumberPicker numberPicker = (NumberPicker) view;
    400                 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue");
    401                 if (minValue != null) {
    402                     numberPicker.setMinValue(Integer.parseInt(minValue));
    403                 }
    404                 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue");
    405                 if (maxValue != null) {
    406                     numberPicker.setMaxValue(Integer.parseInt(maxValue));
    407                 }
    408             }
    409             else if (view instanceof ImageView) {
    410                 ImageView img = (ImageView) view;
    411                 Drawable drawable = img.getDrawable();
    412                 if (drawable instanceof Animatable) {
    413                     if (!((Animatable) drawable).isRunning()) {
    414                         ((Animatable) drawable).start();
    415                     }
    416                 }
    417             }
    418 
    419         }
    420     }
    421 
    422     public void setIsInMerge(boolean isInMerge) {
    423         mIsInMerge = isInMerge;
    424     }
    425 
    426     public void setResourceReference(ResourceReference reference) {
    427         mResourceReference = reference;
    428     }
    429 
    430     @Override
    431     public LayoutInflater cloneInContext(Context newContext) {
    432         return new BridgeInflater(this, newContext);
    433     }
    434 
    435     /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
    436             ResourceReference resourceReference, boolean isInMerge) {
    437 
    438         if (!(attrs instanceof BridgeXmlBlockParser)) {
    439             return null;
    440         }
    441         BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
    442 
    443         // get the view key
    444         Object viewKey = parser.getViewCookie();
    445 
    446         if (viewKey == null) {
    447             int currentDepth = parser.getDepth();
    448 
    449             // test whether we are in an included file or in a adapter binding view.
    450             BridgeXmlBlockParser previousParser = bc.getPreviousParser();
    451             if (previousParser != null) {
    452                 // looks like we are inside an embedded layout.
    453                 // only apply the cookie of the calling node (<include>) if we are at the
    454                 // top level of the embedded layout. If there is a merge tag, then
    455                 // skip it and look for the 2nd level
    456                 int testDepth = isInMerge ? 2 : 1;
    457                 if (currentDepth == testDepth) {
    458                     viewKey = previousParser.getViewCookie();
    459                     // if we are in a merge, wrap the cookie in a MergeCookie.
    460                     if (viewKey != null && isInMerge) {
    461                         viewKey = new MergeCookie(viewKey);
    462                     }
    463                 }
    464             } else if (resourceReference != null && currentDepth == 1) {
    465                 // else if there's a resource reference, this means we are in an adapter
    466                 // binding case. Set the resource ref as the view cookie only for the top
    467                 // level view.
    468                 viewKey = resourceReference;
    469             }
    470         }
    471 
    472         return viewKey;
    473     }
    474 
    475     public void postInflateProcess(View view) {
    476         if (mOpenDrawerLayouts != null) {
    477             String gravity = mOpenDrawerLayouts.get(view);
    478             if (gravity != null) {
    479                 DrawerLayoutUtil.openDrawer(view, gravity);
    480             }
    481             mOpenDrawerLayouts.remove(view);
    482         }
    483     }
    484 
    485     @NonNull
    486     private Map<View, String> getDrawerLayoutMap() {
    487         if (mOpenDrawerLayouts == null) {
    488             mOpenDrawerLayouts = new HashMap<View, String>(4);
    489         }
    490         return mOpenDrawerLayouts;
    491     }
    492 
    493     public void onDoneInflation() {
    494         if (mOpenDrawerLayouts != null) {
    495             mOpenDrawerLayouts.clear();
    496         }
    497     }
    498 }
    499