1 /*
2  * Copyright (C) 2017 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 package androidx.constraintlayout.widget;
17 
18 import android.content.Context;
19 import android.content.res.TypedArray;
20 import android.graphics.Color;
21 import android.graphics.drawable.ColorDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.util.TypedValue;
26 import android.util.Xml;
27 import android.view.View;
28 
29 import org.xmlpull.v1.XmlPullParser;
30 
31 import java.lang.reflect.InvocationTargetException;
32 import java.lang.reflect.Method;
33 import java.util.HashMap;
34 
35 /**
36  * Defines non standard Attributes
37  *
38  *
39  */
40 public class ConstraintAttribute {
41     private static final String TAG = "TransitionLayout";
42     private static final boolean DEBUG = false;
43     private boolean mMethod = false;
44     String mName;
45     private AttributeType mType;
46     private int mIntegerValue;
47     private float mFloatValue;
48     private String mStringValue;
49     boolean mBooleanValue;
50     private int mColorValue;
51 
52     public enum AttributeType {
53         INT_TYPE,
54         FLOAT_TYPE,
55         COLOR_TYPE,
56         COLOR_DRAWABLE_TYPE,
57         STRING_TYPE,
58         BOOLEAN_TYPE,
59         DIMENSION_TYPE,
60         REFERENCE_TYPE
61     }
62 
getName()63     public String getName() {
64         return mName;
65     }
66 
isMethod()67     public boolean isMethod() {
68         return mMethod;
69     }
70 
getIntegerValue()71     public int getIntegerValue() {
72         return mIntegerValue;
73     }
74 
getFloatValue()75     public float getFloatValue() {
76         return mFloatValue;
77     }
78 
getStringValue()79     public String getStringValue() {
80         return mStringValue;
81     }
82 
isBooleanValue()83     public boolean isBooleanValue() {
84         return mBooleanValue;
85     }
86 
getColorValue()87     public int getColorValue() {
88         return mColorValue;
89     }
90 
getType()91     public AttributeType getType() {
92         return mType;
93     }
94 
95     /**
96      * Continuous types are interpolated they are fired only at
97      * @return
98      */
isContinuous()99     public boolean isContinuous() {
100         switch (mType) {
101             case REFERENCE_TYPE:
102             case BOOLEAN_TYPE:
103             case STRING_TYPE:
104                 return false;
105             default:
106                 return true;
107         }
108     }
109 
setFloatValue(float value)110     public void setFloatValue(float value) {
111         mFloatValue = value;
112     }
113 
setColorValue(int value)114     public void setColorValue(int value) {
115         mColorValue = value;
116     }
117 
setIntValue(int value)118     public void setIntValue(int value) {
119         mIntegerValue = value;
120     }
121 
setStringValue(String value)122     public void setStringValue(String value) {
123         mStringValue = value;
124     }
125 
126     /**
127      * The number of interpolation values that need to be interpolated
128      * Typically 1 but 3 for colors.
129      *
130      * @return Typically 1 but 3 for colors.
131      */
numberOfInterpolatedValues()132     public int numberOfInterpolatedValues() {
133         switch (mType) {
134             case COLOR_TYPE:
135             case COLOR_DRAWABLE_TYPE:
136                 return 4;
137             default:
138                 return 1;
139         }
140     }
141 
142     /**
143      * Transforms value to a float for the purpose of interpolation
144      *
145      * @return interpolation value
146      */
getValueToInterpolate()147     public float getValueToInterpolate() {
148         switch (mType) {
149             case INT_TYPE:
150                 return mIntegerValue;
151             case FLOAT_TYPE:
152             case DIMENSION_TYPE:
153                 return mFloatValue;
154             case COLOR_TYPE:
155             case COLOR_DRAWABLE_TYPE:
156                 throw new RuntimeException("Color does not have a single color to interpolate");
157             case STRING_TYPE:
158                 throw new RuntimeException("Cannot interpolate String");
159             case BOOLEAN_TYPE:
160                 return mBooleanValue ? 1 : 0;
161             case REFERENCE_TYPE:
162                 return Float.NaN;
163         }
164         return Float.NaN;
165     }
166 
167     /**
168      * populate the float array with colors it will fill 4 values
169      * @param ret
170      */
getValuesToInterpolate(float[] ret)171     public void getValuesToInterpolate(float[] ret) {
172         switch (mType) {
173             case INT_TYPE:
174                 ret[0] = mIntegerValue;
175                 break;
176             case FLOAT_TYPE:
177                 ret[0] = mFloatValue;
178                 break;
179             case COLOR_DRAWABLE_TYPE:
180             case COLOR_TYPE:
181                 int a = 0xFF & (mColorValue >> 24);
182                 int r = 0xFF & (mColorValue >> 16);
183                 int g = 0xFF & (mColorValue >> 8);
184                 int b = 0xFF & mColorValue;
185                 float f_r = (float) Math.pow(r / 255.0f, 2.2);
186                 float f_g = (float) Math.pow(g / 255.0f, 2.2);
187                 float f_b = (float) Math.pow(b / 255.0f, 2.2);
188                 ret[0] = f_r;
189                 ret[1] = f_g;
190                 ret[2] = f_b;
191                 ret[3] = a / 255f;
192                 break;
193             case STRING_TYPE:
194                 throw new RuntimeException("Color does not have a single color to interpolate");
195             case BOOLEAN_TYPE:
196                 ret[0] = mBooleanValue ? 1 : 0;
197                 break;
198             case DIMENSION_TYPE:
199                 ret[0] = mFloatValue;
200                 break;
201             default:
202                 if (DEBUG) {
203                     Log.v(TAG, mType.toString());
204                 }
205         }
206     }
207 
208     /**
209      * setValue based on the values in the array
210      * @param value
211      */
setValue(float[] value)212     public void setValue(float[] value) {
213         switch (mType) {
214             case REFERENCE_TYPE:
215             case INT_TYPE:
216                 mIntegerValue = (int) value[0];
217                 break;
218             case FLOAT_TYPE:
219                 mFloatValue = value[0];
220                 break;
221             case COLOR_DRAWABLE_TYPE:
222             case COLOR_TYPE:
223                 mColorValue = Color.HSVToColor(value);
224                 mColorValue = (mColorValue & 0xFFFFFF) | (clamp((int) (0xFF * value[3])) << 24);
225                 break;
226             case STRING_TYPE:
227                 throw new RuntimeException("Color does not have a single color to interpolate");
228             case BOOLEAN_TYPE:
229                 mBooleanValue = value[0] > 0.5;
230                 break;
231             case DIMENSION_TYPE:
232                 mFloatValue = value[0];
233                 break;
234             default:
235                 if (DEBUG) {
236                     Log.v(TAG, mType.toString());
237                 }
238 
239         }
240     }
241 
242     /**
243      * test if the two attributes are different
244      *
245      * @param constraintAttribute
246      * @return
247      */
diff(ConstraintAttribute constraintAttribute)248     public boolean diff(ConstraintAttribute constraintAttribute) {
249         if (constraintAttribute == null || mType != constraintAttribute.mType) {
250             return false;
251         }
252         switch (mType) {
253             case INT_TYPE:
254             case REFERENCE_TYPE:
255                 return mIntegerValue == constraintAttribute.mIntegerValue;
256             case FLOAT_TYPE:
257                 return mFloatValue == constraintAttribute.mFloatValue;
258             case COLOR_TYPE:
259             case COLOR_DRAWABLE_TYPE:
260                 return mColorValue == constraintAttribute.mColorValue;
261             case STRING_TYPE:
262                 return mIntegerValue == constraintAttribute.mIntegerValue;
263             case BOOLEAN_TYPE:
264                 return mBooleanValue == constraintAttribute.mBooleanValue;
265             case DIMENSION_TYPE:
266                 return mFloatValue == constraintAttribute.mFloatValue;
267         }
268         return false;
269     }
270 
ConstraintAttribute(String name, AttributeType attributeType)271     public ConstraintAttribute(String name, AttributeType attributeType) {
272         mName = name;
273         mType = attributeType;
274     }
275 
ConstraintAttribute(String name, AttributeType attributeType, Object value, boolean method)276     public ConstraintAttribute(String name,
277                                AttributeType attributeType,
278                                Object value,
279                                boolean method) {
280         mName = name;
281         mType = attributeType;
282         mMethod = method;
283         setValue(value);
284     }
285 
ConstraintAttribute(ConstraintAttribute source, Object value)286     public ConstraintAttribute(ConstraintAttribute source, Object value) {
287         mName = source.mName;
288         mType = source.mType;
289         setValue(value);
290     }
291 
292     /**
293      * set the value based on casting the object
294      * @param value
295      */
setValue(Object value)296     public void setValue(Object value) {
297         switch (mType) {
298             case REFERENCE_TYPE:
299             case INT_TYPE:
300                 mIntegerValue = (Integer) value;
301                 break;
302             case FLOAT_TYPE:
303                 mFloatValue = (Float) value;
304                 break;
305             case COLOR_TYPE:
306             case COLOR_DRAWABLE_TYPE:
307                 mColorValue = (Integer) value;
308                 break;
309             case STRING_TYPE:
310                 mStringValue = (String) value;
311                 break;
312             case BOOLEAN_TYPE:
313                 mBooleanValue = (Boolean) value;
314                 break;
315             case DIMENSION_TYPE:
316                 mFloatValue = (Float) value;
317                 break;
318         }
319     }
320 
321     /**
322      * extract attributes from the view
323      * @param base
324      * @param view
325      * @return
326      */
extractAttributes( HashMap<String, ConstraintAttribute> base, View view)327     public static HashMap<String, ConstraintAttribute> extractAttributes(
328             HashMap<String, ConstraintAttribute> base, View view) {
329         HashMap<String, ConstraintAttribute> ret = new HashMap<>();
330         Class<? extends View> viewClass = view.getClass();
331         for (String name : base.keySet()) {
332             ConstraintAttribute constraintAttribute = base.get(name);
333 
334             try {
335                 if (name.equals("BackgroundColor")) { // hack for getMap set background color
336                     ColorDrawable viewColor = (ColorDrawable) view.getBackground();
337                     Object val = viewColor.getColor();
338                     ret.put(name, new ConstraintAttribute(constraintAttribute, val));
339                 } else {
340                     Method method = viewClass.getMethod("getMap" + name);
341                     Object val = method.invoke(view);
342                     ret.put(name, new ConstraintAttribute(constraintAttribute, val));
343                 }
344             } catch (NoSuchMethodException e) {
345                 Log.e(TAG, viewClass.getName() + " must have a method " + name, e);
346             } catch (IllegalAccessException e) {
347                 Log.e(TAG, " Custom Attribute \"" + name
348                         + "\" not found on " + viewClass.getName(), e);
349             } catch (InvocationTargetException e) {
350                 Log.e(TAG, " Custom Attribute \"" + name
351                         + "\" not found on " + viewClass.getName(), e);
352             }
353         }
354         return ret;
355     }
356 
357     /**
358      * set attributes from map on to the view
359      * @param view
360      * @param map
361      */
setAttributes(View view, HashMap<String, ConstraintAttribute> map)362     public static void setAttributes(View view, HashMap<String, ConstraintAttribute> map) {
363         Class<? extends View> viewClass = view.getClass();
364         for (String name : map.keySet()) {
365             ConstraintAttribute constraintAttribute = map.get(name);
366             String methodName = name;
367             if (!constraintAttribute.mMethod) {
368                 methodName = "set" + methodName;
369             }
370             try {
371                 Method method;
372                 switch (constraintAttribute.mType) {
373                     case INT_TYPE:
374                         method = viewClass.getMethod(methodName, Integer.TYPE);
375                         method.invoke(view, constraintAttribute.mIntegerValue);
376                         break;
377                     case FLOAT_TYPE:
378                         method = viewClass.getMethod(methodName, Float.TYPE);
379                         method.invoke(view, constraintAttribute.mFloatValue);
380                         break;
381                     case COLOR_DRAWABLE_TYPE:
382                         method = viewClass.getMethod(methodName, Drawable.class);
383                         ColorDrawable drawable = new ColorDrawable(); // TODO cache
384                         drawable.setColor(constraintAttribute.mColorValue);
385                         method.invoke(view, drawable);
386                         break;
387                     case COLOR_TYPE:
388                         method = viewClass.getMethod(methodName, Integer.TYPE);
389                         method.invoke(view, constraintAttribute.mColorValue);
390                         break;
391                     case STRING_TYPE:
392                         method = viewClass.getMethod(methodName, CharSequence.class);
393                         method.invoke(view, constraintAttribute.mStringValue);
394                         break;
395                     case BOOLEAN_TYPE:
396                         method = viewClass.getMethod(methodName, Boolean.TYPE);
397                         method.invoke(view, constraintAttribute.mBooleanValue);
398                         break;
399                     case DIMENSION_TYPE:
400                         method = viewClass.getMethod(methodName, Float.TYPE);
401                         method.invoke(view, constraintAttribute.mFloatValue);
402                         break;
403                     case REFERENCE_TYPE:
404                         method = viewClass.getMethod(methodName, Integer.TYPE);
405                         method.invoke(view, constraintAttribute.mIntegerValue);
406                 }
407             } catch (NoSuchMethodException e) {
408                 Log.e(TAG, viewClass.getName() + " must have a method " + methodName, e);
409             } catch (IllegalAccessException e) {
410                 Log.e(TAG, " Custom Attribute \"" + name
411                         + "\" not found on " + viewClass.getName(), e);
412             } catch (InvocationTargetException e) {
413                 Log.e(TAG, " Custom Attribute \"" + name
414                         + "\" not found on " + viewClass.getName(), e);
415             }
416         }
417     }
418 
419     /**
420      * Apply custom attributes to the view
421      * @param view
422      */
applyCustom(View view)423     public void applyCustom(View view) {
424         Class<? extends View> viewClass = view.getClass();
425         String name = this.mName;
426         String methodName = name;
427         if (!mMethod) {
428             methodName = "set" + methodName;
429         }
430         try {
431             Method method;
432             switch (this.mType) {
433                 case INT_TYPE:
434                 case REFERENCE_TYPE:
435                     method = viewClass.getMethod(methodName, Integer.TYPE);
436                     method.invoke(view, this.mIntegerValue);
437                     break;
438                 case FLOAT_TYPE:
439                     method = viewClass.getMethod(methodName, Float.TYPE);
440                     method.invoke(view, this.mFloatValue);
441                     break;
442                 case COLOR_DRAWABLE_TYPE:
443                     method = viewClass.getMethod(methodName, Drawable.class);
444                     ColorDrawable drawable = new ColorDrawable(); // TODO cache
445                     drawable.setColor(this.mColorValue);
446                     method.invoke(view, drawable);
447                     break;
448                 case COLOR_TYPE:
449                     method = viewClass.getMethod(methodName, Integer.TYPE);
450                     method.invoke(view, this.mColorValue);
451                     break;
452                 case STRING_TYPE:
453                     method = viewClass.getMethod(methodName, CharSequence.class);
454                     method.invoke(view, this.mStringValue);
455                     break;
456                 case BOOLEAN_TYPE:
457                     method = viewClass.getMethod(methodName, Boolean.TYPE);
458                     method.invoke(view, this.mBooleanValue);
459                     break;
460                 case DIMENSION_TYPE:
461                     method = viewClass.getMethod(methodName, Float.TYPE);
462                     method.invoke(view, this.mFloatValue);
463                     break;
464             }
465         } catch (NoSuchMethodException e) {
466             Log.e(TAG, viewClass.getName() + " must have a method " + methodName, e);
467         } catch (IllegalAccessException e) {
468             Log.e(TAG, " Custom Attribute \"" + name
469                     + "\" not found on " + viewClass.getName(), e);
470         } catch (InvocationTargetException e) {
471             Log.e(TAG, " Custom Attribute \"" + name
472                     + "\" not found on " + viewClass.getName(), e);
473         }
474     }
475 
clamp(int c)476     private static int clamp(int c) {
477         int n = 255;
478         c &= ~(c >> 31);
479         c -= n;
480         c &= (c >> 31);
481         c += n;
482         return c;
483     }
484 
485     /**
486      * parse Custom attributes and fill Custom
487      * @param context
488      * @param parser
489      * @param custom
490      */
parse(Context context, XmlPullParser parser, HashMap<String, ConstraintAttribute> custom)491     public static void parse(Context context,
492                              XmlPullParser parser,
493                              HashMap<String, ConstraintAttribute> custom) {
494         AttributeSet attributeSet = Xml.asAttributeSet(parser);
495         TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.CustomAttribute);
496         String name = null;
497         boolean method = false;
498         Object value = null;
499         AttributeType type = null;
500         final int count = a.getIndexCount();
501         for (int i = 0; i < count; i++) {
502             int attr = a.getIndex(i);
503             if (attr == R.styleable.CustomAttribute_attributeName) {
504                 name = a.getString(attr);
505                 if (name != null && name.length() > 0) {
506                     name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
507                 }
508             } else if (attr == R.styleable.CustomAttribute_methodName) {
509                 method = true;
510                 name = a.getString(attr);
511             } else if (attr == R.styleable.CustomAttribute_customBoolean) {
512                 value = a.getBoolean(attr, false);
513                 type = AttributeType.BOOLEAN_TYPE;
514             } else if (attr == R.styleable.CustomAttribute_customColorValue) {
515                 type = AttributeType.COLOR_TYPE;
516                 value = a.getColor(attr, 0);
517             } else if (attr == R.styleable.CustomAttribute_customColorDrawableValue) {
518                 type = AttributeType.COLOR_DRAWABLE_TYPE;
519                 value = a.getColor(attr, 0);
520             } else if (attr == R.styleable.CustomAttribute_customPixelDimension) {
521                 type = AttributeType.DIMENSION_TYPE;
522                 value = TypedValue.applyDimension(
523                         TypedValue.COMPLEX_UNIT_DIP,
524                         a.getDimension(attr, 0),
525                         context.getResources().getDisplayMetrics());
526             } else if (attr == R.styleable.CustomAttribute_customDimension) {
527                 type = AttributeType.DIMENSION_TYPE;
528                 value = a.getDimension(attr, 0);
529             } else if (attr == R.styleable.CustomAttribute_customFloatValue) {
530                 type = AttributeType.FLOAT_TYPE;
531                 value = a.getFloat(attr, Float.NaN);
532             } else if (attr == R.styleable.CustomAttribute_customIntegerValue) {
533                 type = AttributeType.INT_TYPE;
534                 value = a.getInteger(attr, -1);
535             } else if (attr == R.styleable.CustomAttribute_customStringValue) {
536                 type = AttributeType.STRING_TYPE;
537                 value = a.getString(attr);
538             } else if (attr == R.styleable.CustomAttribute_customReference) {
539                 type = AttributeType.REFERENCE_TYPE;
540                 int tmp = a.getResourceId(attr, -1);
541                 if (tmp == -1) {
542                     tmp = a.getInt(attr, -1);
543                 }
544                 value = tmp;
545             }
546         }
547         if (name != null && value != null) {
548             custom.put(name, new ConstraintAttribute(name, type, value, method));
549         }
550         a.recycle();
551     }
552 
553 }
554