ConstraintAttribute.java

/*
 * Copyright (C) 2017 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 androidx.constraintlayout.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.util.Xml;
import android.view.View;

import org.xmlpull.v1.XmlPullParser;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * Defines non standard Attributes
 *
 *
 */
public class ConstraintAttribute {
    private static final String TAG = "TransitionLayout";
    private static final boolean DEBUG = false;
    private boolean mMethod = false;
    String mName;
    private AttributeType mType;
    private int mIntegerValue;
    private float mFloatValue;
    private String mStringValue;
    boolean mBooleanValue;
    private int mColorValue;

    public enum AttributeType {
        INT_TYPE,
        FLOAT_TYPE,
        COLOR_TYPE,
        COLOR_DRAWABLE_TYPE,
        STRING_TYPE,
        BOOLEAN_TYPE,
        DIMENSION_TYPE,
        REFERENCE_TYPE
    }

    public String getName() {
        return mName;
    }

    public boolean isMethod() {
        return mMethod;
    }

    public int getIntegerValue() {
        return mIntegerValue;
    }

    public float getFloatValue() {
        return mFloatValue;
    }

    public String getStringValue() {
        return mStringValue;
    }

    public boolean isBooleanValue() {
        return mBooleanValue;
    }

    public int getColorValue() {
        return mColorValue;
    }

    public AttributeType getType() {
        return mType;
    }

    /**
     * Continuous types are interpolated they are fired only at
     * @return
     */
    public boolean isContinuous() {
        switch (mType) {
            case REFERENCE_TYPE:
            case BOOLEAN_TYPE:
            case STRING_TYPE:
                return false;
            default:
                return true;
        }
    }

    public void setFloatValue(float value) {
        mFloatValue = value;
    }

    public void setColorValue(int value) {
        mColorValue = value;
    }

    public void setIntValue(int value) {
        mIntegerValue = value;
    }

    public void setStringValue(String value) {
        mStringValue = value;
    }

    /**
     * The number of interpolation values that need to be interpolated
     * Typically 1 but 3 for colors.
     *
     * @return Typically 1 but 3 for colors.
     */
    public int numberOfInterpolatedValues() {
        switch (mType) {
            case COLOR_TYPE:
            case COLOR_DRAWABLE_TYPE:
                return 4;
            default:
                return 1;
        }
    }

    /**
     * Transforms value to a float for the purpose of interpolation
     *
     * @return interpolation value
     */
    public float getValueToInterpolate() {
        switch (mType) {
            case INT_TYPE:
                return mIntegerValue;
            case FLOAT_TYPE:
            case DIMENSION_TYPE:
                return mFloatValue;
            case COLOR_TYPE:
            case COLOR_DRAWABLE_TYPE:
                throw new RuntimeException("Color does not have a single color to interpolate");
            case STRING_TYPE:
                throw new RuntimeException("Cannot interpolate String");
            case BOOLEAN_TYPE:
                return mBooleanValue ? 1 : 0;
            case REFERENCE_TYPE:
                return Float.NaN;
        }
        return Float.NaN;
    }

    /**
     * populate the float array with colors it will fill 4 values
     * @param ret
     */
    public void getValuesToInterpolate(float[] ret) {
        switch (mType) {
            case INT_TYPE:
                ret[0] = mIntegerValue;
                break;
            case FLOAT_TYPE:
                ret[0] = mFloatValue;
                break;
            case COLOR_DRAWABLE_TYPE:
            case COLOR_TYPE:
                int a = 0xFF & (mColorValue >> 24);
                int r = 0xFF & (mColorValue >> 16);
                int g = 0xFF & (mColorValue >> 8);
                int b = 0xFF & mColorValue;
                float f_r = (float) Math.pow(r / 255.0f, 2.2);
                float f_g = (float) Math.pow(g / 255.0f, 2.2);
                float f_b = (float) Math.pow(b / 255.0f, 2.2);
                ret[0] = f_r;
                ret[1] = f_g;
                ret[2] = f_b;
                ret[3] = a / 255f;
                break;
            case STRING_TYPE:
                throw new RuntimeException("Color does not have a single color to interpolate");
            case BOOLEAN_TYPE:
                ret[0] = mBooleanValue ? 1 : 0;
                break;
            case DIMENSION_TYPE:
                ret[0] = mFloatValue;
                break;
            default:
                if (DEBUG) {
                    Log.v(TAG, mType.toString());
                }
        }
    }

    /**
     * setValue based on the values in the array
     * @param value
     */
    public void setValue(float[] value) {
        switch (mType) {
            case REFERENCE_TYPE:
            case INT_TYPE:
                mIntegerValue = (int) value[0];
                break;
            case FLOAT_TYPE:
                mFloatValue = value[0];
                break;
            case COLOR_DRAWABLE_TYPE:
            case COLOR_TYPE:
                mColorValue = Color.HSVToColor(value);
                mColorValue = (mColorValue & 0xFFFFFF) | (clamp((int) (0xFF * value[3])) << 24);
                break;
            case STRING_TYPE:
                throw new RuntimeException("Color does not have a single color to interpolate");
            case BOOLEAN_TYPE:
                mBooleanValue = value[0] > 0.5;
                break;
            case DIMENSION_TYPE:
                mFloatValue = value[0];
                break;
            default:
                if (DEBUG) {
                    Log.v(TAG, mType.toString());
                }

        }
    }

    /**
     * test if the two attributes are different
     *
     * @param constraintAttribute
     * @return
     */
    public boolean diff(ConstraintAttribute constraintAttribute) {
        if (constraintAttribute == null || mType != constraintAttribute.mType) {
            return false;
        }
        switch (mType) {
            case INT_TYPE:
            case REFERENCE_TYPE:
                return mIntegerValue == constraintAttribute.mIntegerValue;
            case FLOAT_TYPE:
                return mFloatValue == constraintAttribute.mFloatValue;
            case COLOR_TYPE:
            case COLOR_DRAWABLE_TYPE:
                return mColorValue == constraintAttribute.mColorValue;
            case STRING_TYPE:
                return mIntegerValue == constraintAttribute.mIntegerValue;
            case BOOLEAN_TYPE:
                return mBooleanValue == constraintAttribute.mBooleanValue;
            case DIMENSION_TYPE:
                return mFloatValue == constraintAttribute.mFloatValue;
        }
        return false;
    }

    public ConstraintAttribute(String name, AttributeType attributeType) {
        mName = name;
        mType = attributeType;
    }

    public ConstraintAttribute(String name,
                               AttributeType attributeType,
                               Object value,
                               boolean method) {
        mName = name;
        mType = attributeType;
        mMethod = method;
        setValue(value);
    }

    public ConstraintAttribute(ConstraintAttribute source, Object value) {
        mName = source.mName;
        mType = source.mType;
        setValue(value);
    }

    /**
     * set the value based on casting the object
     * @param value
     */
    public void setValue(Object value) {
        switch (mType) {
            case REFERENCE_TYPE:
            case INT_TYPE:
                mIntegerValue = (Integer) value;
                break;
            case FLOAT_TYPE:
                mFloatValue = (Float) value;
                break;
            case COLOR_TYPE:
            case COLOR_DRAWABLE_TYPE:
                mColorValue = (Integer) value;
                break;
            case STRING_TYPE:
                mStringValue = (String) value;
                break;
            case BOOLEAN_TYPE:
                mBooleanValue = (Boolean) value;
                break;
            case DIMENSION_TYPE:
                mFloatValue = (Float) value;
                break;
        }
    }

    /**
     * extract attributes from the view
     * @param base
     * @param view
     * @return
     */
    public static HashMap<String, ConstraintAttribute> extractAttributes(
            HashMap<String, ConstraintAttribute> base, View view) {
        HashMap<String, ConstraintAttribute> ret = new HashMap<>();
        Class<? extends View> viewClass = view.getClass();
        for (String name : base.keySet()) {
            ConstraintAttribute constraintAttribute = base.get(name);

            try {
                if (name.equals("BackgroundColor")) { // hack for getMap set background color
                    ColorDrawable viewColor = (ColorDrawable) view.getBackground();
                    Object val = viewColor.getColor();
                    ret.put(name, new ConstraintAttribute(constraintAttribute, val));
                } else {
                    Method method = viewClass.getMethod("getMap" + name);
                    Object val = method.invoke(view);
                    ret.put(name, new ConstraintAttribute(constraintAttribute, val));
                }
            } catch (NoSuchMethodException e) {
                Log.e(TAG, viewClass.getName() + " must have a method " + name, e);
            } catch (IllegalAccessException e) {
                Log.e(TAG, " Custom Attribute \"" + name
                        + "\" not found on " + viewClass.getName(), e);
            } catch (InvocationTargetException e) {
                Log.e(TAG, " Custom Attribute \"" + name
                        + "\" not found on " + viewClass.getName(), e);
            }
        }
        return ret;
    }

    /**
     * set attributes from map on to the view
     * @param view
     * @param map
     */
    public static void setAttributes(View view, HashMap<String, ConstraintAttribute> map) {
        Class<? extends View> viewClass = view.getClass();
        for (String name : map.keySet()) {
            ConstraintAttribute constraintAttribute = map.get(name);
            String methodName = name;
            if (!constraintAttribute.mMethod) {
                methodName = "set" + methodName;
            }
            try {
                Method method;
                switch (constraintAttribute.mType) {
                    case INT_TYPE:
                        method = viewClass.getMethod(methodName, Integer.TYPE);
                        method.invoke(view, constraintAttribute.mIntegerValue);
                        break;
                    case FLOAT_TYPE:
                        method = viewClass.getMethod(methodName, Float.TYPE);
                        method.invoke(view, constraintAttribute.mFloatValue);
                        break;
                    case COLOR_DRAWABLE_TYPE:
                        method = viewClass.getMethod(methodName, Drawable.class);
                        ColorDrawable drawable = new ColorDrawable(); // TODO cache
                        drawable.setColor(constraintAttribute.mColorValue);
                        method.invoke(view, drawable);
                        break;
                    case COLOR_TYPE:
                        method = viewClass.getMethod(methodName, Integer.TYPE);
                        method.invoke(view, constraintAttribute.mColorValue);
                        break;
                    case STRING_TYPE:
                        method = viewClass.getMethod(methodName, CharSequence.class);
                        method.invoke(view, constraintAttribute.mStringValue);
                        break;
                    case BOOLEAN_TYPE:
                        method = viewClass.getMethod(methodName, Boolean.TYPE);
                        method.invoke(view, constraintAttribute.mBooleanValue);
                        break;
                    case DIMENSION_TYPE:
                        method = viewClass.getMethod(methodName, Float.TYPE);
                        method.invoke(view, constraintAttribute.mFloatValue);
                        break;
                    case REFERENCE_TYPE:
                        method = viewClass.getMethod(methodName, Integer.TYPE);
                        method.invoke(view, constraintAttribute.mIntegerValue);
                }
            } catch (NoSuchMethodException e) {
                Log.e(TAG, viewClass.getName() + " must have a method " + methodName, e);
            } catch (IllegalAccessException e) {
                Log.e(TAG, " Custom Attribute \"" + name
                        + "\" not found on " + viewClass.getName(), e);
            } catch (InvocationTargetException e) {
                Log.e(TAG, " Custom Attribute \"" + name
                        + "\" not found on " + viewClass.getName(), e);
            }
        }
    }

    /**
     * Apply custom attributes to the view
     * @param view
     */
    public void applyCustom(View view) {
        Class<? extends View> viewClass = view.getClass();
        String name = this.mName;
        String methodName = name;
        if (!mMethod) {
            methodName = "set" + methodName;
        }
        try {
            Method method;
            switch (this.mType) {
                case INT_TYPE:
                case REFERENCE_TYPE:
                    method = viewClass.getMethod(methodName, Integer.TYPE);
                    method.invoke(view, this.mIntegerValue);
                    break;
                case FLOAT_TYPE:
                    method = viewClass.getMethod(methodName, Float.TYPE);
                    method.invoke(view, this.mFloatValue);
                    break;
                case COLOR_DRAWABLE_TYPE:
                    method = viewClass.getMethod(methodName, Drawable.class);
                    ColorDrawable drawable = new ColorDrawable(); // TODO cache
                    drawable.setColor(this.mColorValue);
                    method.invoke(view, drawable);
                    break;
                case COLOR_TYPE:
                    method = viewClass.getMethod(methodName, Integer.TYPE);
                    method.invoke(view, this.mColorValue);
                    break;
                case STRING_TYPE:
                    method = viewClass.getMethod(methodName, CharSequence.class);
                    method.invoke(view, this.mStringValue);
                    break;
                case BOOLEAN_TYPE:
                    method = viewClass.getMethod(methodName, Boolean.TYPE);
                    method.invoke(view, this.mBooleanValue);
                    break;
                case DIMENSION_TYPE:
                    method = viewClass.getMethod(methodName, Float.TYPE);
                    method.invoke(view, this.mFloatValue);
                    break;
            }
        } catch (NoSuchMethodException e) {
            Log.e(TAG, viewClass.getName() + " must have a method " + methodName, e);
        } catch (IllegalAccessException e) {
            Log.e(TAG, " Custom Attribute \"" + name
                    + "\" not found on " + viewClass.getName(), e);
        } catch (InvocationTargetException e) {
            Log.e(TAG, " Custom Attribute \"" + name
                    + "\" not found on " + viewClass.getName(), e);
        }
    }

    private static int clamp(int c) {
        int n = 255;
        c &= ~(c >> 31);
        c -= n;
        c &= (c >> 31);
        c += n;
        return c;
    }

    /**
     * parse Custom attributes and fill Custom
     * @param context
     * @param parser
     * @param custom
     */
    public static void parse(Context context,
                             XmlPullParser parser,
                             HashMap<String, ConstraintAttribute> custom) {
        AttributeSet attributeSet = Xml.asAttributeSet(parser);
        TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.CustomAttribute);
        String name = null;
        boolean method = false;
        Object value = null;
        AttributeType type = null;
        final int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.CustomAttribute_attributeName) {
                name = a.getString(attr);
                if (name != null && name.length() > 0) {
                    name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
                }
            } else if (attr == R.styleable.CustomAttribute_methodName) {
                method = true;
                name = a.getString(attr);
            } else if (attr == R.styleable.CustomAttribute_customBoolean) {
                value = a.getBoolean(attr, false);
                type = AttributeType.BOOLEAN_TYPE;
            } else if (attr == R.styleable.CustomAttribute_customColorValue) {
                type = AttributeType.COLOR_TYPE;
                value = a.getColor(attr, 0);
            } else if (attr == R.styleable.CustomAttribute_customColorDrawableValue) {
                type = AttributeType.COLOR_DRAWABLE_TYPE;
                value = a.getColor(attr, 0);
            } else if (attr == R.styleable.CustomAttribute_customPixelDimension) {
                type = AttributeType.DIMENSION_TYPE;
                value = TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP,
                        a.getDimension(attr, 0),
                        context.getResources().getDisplayMetrics());
            } else if (attr == R.styleable.CustomAttribute_customDimension) {
                type = AttributeType.DIMENSION_TYPE;
                value = a.getDimension(attr, 0);
            } else if (attr == R.styleable.CustomAttribute_customFloatValue) {
                type = AttributeType.FLOAT_TYPE;
                value = a.getFloat(attr, Float.NaN);
            } else if (attr == R.styleable.CustomAttribute_customIntegerValue) {
                type = AttributeType.INT_TYPE;
                value = a.getInteger(attr, -1);
            } else if (attr == R.styleable.CustomAttribute_customStringValue) {
                type = AttributeType.STRING_TYPE;
                value = a.getString(attr);
            } else if (attr == R.styleable.CustomAttribute_customReference) {
                type = AttributeType.REFERENCE_TYPE;
                int tmp = a.getResourceId(attr, -1);
                if (tmp == -1) {
                    tmp = a.getInt(attr, -1);
                }
                value = tmp;
            }
        }
        if (name != null && value != null) {
            custom.put(name, new ConstraintAttribute(name, type, value, method));
        }
        a.recycle();
    }

}