/* * Copyright (C) 2013 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.tools.idea.rendering; import com.android.annotations.Nullable; import com.android.resources.Density; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.android.SdkConstants.*; /** * A custom version of the {@link LayoutPsiPullParser} which * can add padding to a dedicated set of layout nodes, which for example can be used to * ensure that empty view groups have certain minimum size during a palette drop. */ public class PaddingLayoutPsiPullParser extends LayoutPsiPullParser { private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$ private final static int PADDING_VALUE = 10; private boolean myZeroAttributeIsPadding = false; private boolean myIncreaseExistingPadding = false; @NotNull private final Density myDensity; /** * Number of pixels to pad views with in exploded-rendering mode. */ private static final String DEFAULT_PADDING_VALUE = PADDING_VALUE + UNIT_PX; /** * Number of pixels to pad exploded individual views with. (This is HALF the width of the * rectangle since padding is repeated on both sides of the empty content.) */ private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$ /** * Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding * attribute value. Can be null, which is the case when we don't want to perform any * <b>individual</b> node exploding. */ private final Set<XmlTag> myExplodeNodes; /** * Use the {@link LayoutPsiPullParser#create(com.intellij.psi.xml.XmlFile, RenderLogger, java.util.Set, * com.android.resources.Density)} factory instead */ PaddingLayoutPsiPullParser(@NotNull XmlFile file, @NotNull RenderLogger logger, @NotNull Set<XmlTag> explodeNodes, @NotNull Density density) { super(file, logger); myExplodeNodes = explodeNodes; myDensity = density; } @Override protected void push(@NotNull Element node) { super.push(node); myZeroAttributeIsPadding = false; myIncreaseExistingPadding = false; } /* * This does not seem to be called by the layoutlib, but we keep this (and maintain * it) just in case. */ @Override public int getAttributeCount() { int count = super.getAttributeCount(); return count + (myZeroAttributeIsPadding ? 1 : 0); } /* * This does not seem to be called by the layoutlib, but we keep this (and maintain * it) just in case. */ @Nullable @Override public String getAttributeName(int i) { if (myZeroAttributeIsPadding) { if (i == 0) { return ATTR_PADDING; } else { i--; } } return super.getAttributeName(i); } /* * This does not seem to be called by the layoutlib, but we keep this (and maintain * it) just in case. */ @Override public String getAttributeNamespace(int i) { if (myZeroAttributeIsPadding) { if (i == 0) { return ANDROID_URI; } else { i--; } } return super.getAttributeNamespace(i); } /* * This does not seem to be called by the layoutlib, but we keep this (and maintain * it) just in case. */ @Nullable @Override public String getAttributePrefix(int i) { if (myZeroAttributeIsPadding) { if (i == 0) { assert myRoot != null; return myAndroidPrefix; } else { i--; } } return super.getAttributePrefix(i); } /* * This does not seem to be called by the layoutlib, but we keep this (and maintain * it) just in case. */ @Nullable @Override public String getAttributeValue(int i) { if (myZeroAttributeIsPadding) { if (i == 0) { return DEFAULT_PADDING_VALUE; } else { i--; } } Attribute attribute = getAttribute(i); if (attribute != null) { String value = attribute.value; if (value != null && myIncreaseExistingPadding && ATTR_PADDING.equals(attribute.name) && ANDROID_URI.equals(attribute.namespace)) { // add the padding and return the value return addPaddingToValue(value); } return value; } return null; } /* * This is the main method used by the LayoutInflater to query for attributes. */ @Nullable @Override public String getAttributeValue(String namespace, String localName) { boolean isPaddingAttribute = ATTR_PADDING.equals(localName); if (isPaddingAttribute && ANDROID_URI.equals(namespace)) { Element node = getCurrentNode(); if (node != null && myExplodeNodes.contains(node.cookie)) { return FIXED_PADDING_VALUE; } } if (myZeroAttributeIsPadding && isPaddingAttribute && ANDROID_URI.equals(namespace)) { return DEFAULT_PADDING_VALUE; } String value = super.getAttributeValue(namespace, localName); if (value != null) { if (myIncreaseExistingPadding && isPaddingAttribute && ANDROID_URI.equals(namespace)) { // add the padding and return the value return addPaddingToValue(value); } } return value; } // ------- TypedValue stuff // This is adapted from com.android.layoutlib.bridge.ResourceHelper // (but modified to directly take the parsed value and convert it into pixel instead of // storing it into a TypedValue) // this was originally taken from platform/frameworks/base/libs/utils/ResourceTypes.cpp private static final class DimensionEntry { final String name; final int type; DimensionEntry(String name, int unit) { this.name = name; this.type = unit; } } /** * {@link DimensionEntry} complex unit: Value is raw pixels. */ private static final int COMPLEX_UNIT_PX = 0; /** * {@link DimensionEntry} complex unit: Value is Device Independent * Pixels. */ private static final int COMPLEX_UNIT_DIP = 1; /** * {@link DimensionEntry} complex unit: Value is a scaled pixel. */ private static final int COMPLEX_UNIT_SP = 2; /** * {@link DimensionEntry} complex unit: Value is in points. */ private static final int COMPLEX_UNIT_PT = 3; /** * {@link DimensionEntry} complex unit: Value is in inches. */ private static final int COMPLEX_UNIT_IN = 4; /** * {@link DimensionEntry} complex unit: Value is in millimeters. */ private static final int COMPLEX_UNIT_MM = 5; private final static DimensionEntry[] DIMENSIONS = new DimensionEntry[]{ new DimensionEntry(UNIT_PX, COMPLEX_UNIT_PX), new DimensionEntry(UNIT_DIP, COMPLEX_UNIT_DIP), new DimensionEntry(UNIT_DP, COMPLEX_UNIT_DIP), new DimensionEntry(UNIT_SP, COMPLEX_UNIT_SP), new DimensionEntry(UNIT_PT, COMPLEX_UNIT_PT), new DimensionEntry(UNIT_IN, COMPLEX_UNIT_IN), new DimensionEntry(UNIT_MM, COMPLEX_UNIT_MM) }; /** * Adds padding to an existing dimension. * <p/>This will resolve the attribute value (which can be px, dip, dp, sp, pt, in, mm) to * a pixel value, add the padding value ({@link #PADDING_VALUE}), * and then return a string with the new value as a px string ("42px"); * If the conversion fails, only the special padding is returned. */ private String addPaddingToValue(@Nullable String s) { if (s == null) { return DEFAULT_PADDING_VALUE; } int padding = PADDING_VALUE; if (stringToPixel(s)) { padding += myLastPixel; } return padding + UNIT_PX; } /** Out value from {@link #stringToPixel(String)}: the integer pixel value */ private int myLastPixel; /** * Convert the string into a pixel value, and puts it in {@link #myLastPixel} * * @param s the dimension value from an XML attribute * @return true if success. */ private boolean stringToPixel(String s) { // remove the space before and after s = s.trim(); int len = s.length(); if (len <= 0) { return false; } // check that there's no non ASCII characters. char[] buf = s.toCharArray(); for (int i = 0; i < len; i++) { if (buf[i] > 255) { return false; } } // check the first character if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') { return false; } // now look for the string that is after the float... Matcher m = FLOAT_PATTERN.matcher(s); if (m.matches()) { String f_str = m.group(1); String end = m.group(2); float f; try { f = Float.parseFloat(f_str); } catch (NumberFormatException e) { // this shouldn't happen with the regexp above. return false; } if (end.length() > 0 && end.charAt(0) != ' ') { // We only support dimension-type values, so try to parse the unit for dimension DimensionEntry dimension = parseDimension(end); if (dimension != null) { // convert the value into pixel based on the dimension type // This is similar to TypedValue.applyDimension() switch (dimension.type) { case COMPLEX_UNIT_PX: // do nothing, value is already in px break; case COMPLEX_UNIT_DIP: case COMPLEX_UNIT_SP: // intended fall-through since we don't // adjust for font size f *= (float)myDensity.getDpiValue() / Density.DEFAULT_DENSITY; break; case COMPLEX_UNIT_PT: f *= myDensity.getDpiValue() * (1.0f / 72); break; case COMPLEX_UNIT_IN: f *= myDensity.getDpiValue(); break; case COMPLEX_UNIT_MM: f *= myDensity.getDpiValue() * (1.0f / 25.4f); break; } // store result (converted to int) myLastPixel = (int)(f + 0.5); return true; } } } return false; } @Nullable private static DimensionEntry parseDimension(String str) { str = str.trim(); for (DimensionEntry d : DIMENSIONS) { if (d.name.equals(str)) { return d; } } return null; } }