1 /*
2  * Copyright (C) 2018 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 androidx.constraintlayout.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.content.res.XmlResourceParser;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.util.SparseArray;
25 import android.util.Xml;
26 
27 import org.xmlpull.v1.XmlPullParser;
28 import org.xmlpull.v1.XmlPullParserException;
29 
30 import java.io.IOException;
31 import java.util.ArrayList;
32 
33 /**
34  *
35  */
36 
37 public class StateSet {
38     public static final String TAG = "ConstraintLayoutStates";
39     private static final boolean DEBUG = false;
40     int mDefaultState = -1;
41 
42     int mCurrentStateId = -1; // default
43     int mCurrentConstraintNumber = -1; // default
44     private SparseArray<State> mStateList = new SparseArray<>();
45     @SuppressWarnings("unused")
46     private ConstraintsChangedListener mConstraintsChangedListener = null;
47 
48     /**
49      * Parse a StateSet
50      * @param context
51      * @param parser
52      */
StateSet(Context context, XmlPullParser parser)53     public StateSet(Context context, XmlPullParser parser) {
54         load(context, parser);
55     }
56 
57     /**
58      * Load a constraint set from a constraintSet.xml file
59      *
60      * @param context    the context for the inflation
61      * @param parser  mId of xml file in res/xml/
62      */
load(Context context, XmlPullParser parser)63     private void load(Context context, XmlPullParser parser) {
64         if (DEBUG) {
65             Log.v(TAG, "#########load stateSet###### ");
66         }
67         // Parse the stateSet attributes
68         AttributeSet attrs = Xml.asAttributeSet(parser);
69         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StateSet);
70         final int count = a.getIndexCount();
71 
72         for (int i = 0; i < count; i++) {
73             int attr = a.getIndex(i);
74             if (attr == R.styleable.StateSet_defaultState) {
75                 mDefaultState = a.getResourceId(attr, mDefaultState);
76             }
77         }
78         a.recycle();
79 
80         try {
81             Variant match;
82             State state = null;
83             for (int eventType = parser.getEventType();
84                     eventType != XmlResourceParser.END_DOCUMENT;
85                     eventType = parser.next()) {
86 
87                 switch (eventType) {
88                     case XmlResourceParser.START_DOCUMENT:
89                     case XmlResourceParser.TEXT:
90                         break;
91                     case XmlResourceParser.START_TAG:
92                         String tagName = parser.getName();
93                         switch(tagName) {
94                             case "LayoutDescription":
95                                 break;
96                             case "StateSet":
97                                 break;
98                             case "State":
99                                 state = new State(context, parser);
100                                 mStateList.put(state.mId, state);
101                                 break;
102                             case "Variant":
103                                 match = new Variant(context, parser);
104                                 if (state != null) {
105                                     state.add(match);
106                                 }
107                                 break;
108 
109                             default:
110                                 if (DEBUG) {
111                                     Log.v(TAG, "unknown tag " + tagName);
112                                 }
113                         }
114 
115                         break;
116                     case XmlResourceParser.END_TAG:
117                         if ("StateSet".equals(parser.getName())) {
118                             if (DEBUG) {
119                                 Log.v(TAG, "############ finished parsing state set");
120                             }
121                             return;
122                         }
123                         break;
124                 }
125             }
126 
127         } catch (XmlPullParserException e) {
128             Log.e(TAG, "Error parsing XML resource", e);
129         } catch (IOException e) {
130             Log.e(TAG, "Error parsing XML resource", e);
131         }
132     }
133 
134     /**
135      * will the layout need to change
136      * @param id
137      * @param width
138      * @param height
139      * @return
140      */
needsToChange(int id, float width, float height)141     public boolean needsToChange(int id, float width, float height) {
142         if (mCurrentStateId != id) {
143             return true;
144         }
145 
146         State state = (id == -1) ? mStateList.valueAt(0) : mStateList.get(mCurrentStateId);
147 
148         if (mCurrentConstraintNumber != -1) {
149             if (state.mVariants.get(mCurrentConstraintNumber).match(width, height)) {
150                 return false;
151             }
152         }
153 
154         if (mCurrentConstraintNumber == state.findMatch(width, height)) {
155             return false;
156         }
157         return true;
158     }
159 
160     /**
161      * listen for changes in constraintSet
162      * @param constraintsChangedListener
163      */
setOnConstraintsChanged(ConstraintsChangedListener constraintsChangedListener)164     public void setOnConstraintsChanged(ConstraintsChangedListener constraintsChangedListener) {
165         this.mConstraintsChangedListener = constraintsChangedListener;
166     }
167 
168     /**
169      * Get the constraint id for a state
170      * @param id
171      * @param width
172      * @param height
173      * @return
174      */
stateGetConstraintID(int id, int width, int height)175     public int stateGetConstraintID(int id, int width, int height) {
176         return updateConstraints(-1, id, width, height);
177     }
178 
179     /**
180      * converts a state to a constraintSet
181      *
182      * @param currentConstrainSettId
183      * @param stateId
184      * @param width
185      * @param height
186      * @return
187      */
convertToConstraintSet(int currentConstrainSettId, int stateId, float width, float height)188     public int convertToConstraintSet(int currentConstrainSettId,
189                                       int stateId,
190                                       float width,
191                                       float height) {
192         State state = mStateList.get(stateId);
193         if (state == null) {
194             return stateId;
195         }
196         if (width == -1 || height == -1) {            // for the case without width/height matching
197             if (state.mConstraintID == currentConstrainSettId) {
198                 return currentConstrainSettId;
199             }
200             for (Variant mVariant : state.mVariants) {
201                 if (currentConstrainSettId == mVariant.mConstraintID) {
202                     return currentConstrainSettId;
203                 }
204             }
205             return state.mConstraintID;
206         } else {
207             Variant match = null;
208             for (Variant mVariant : state.mVariants) {
209                 if (mVariant.match(width, height)) {
210                     if (currentConstrainSettId == mVariant.mConstraintID) {
211                         return currentConstrainSettId;
212                     }
213                     match = mVariant;
214                 }
215             }
216             if (match != null) {
217                 return match.mConstraintID;
218             }
219 
220             return state.mConstraintID;
221         }
222     }
223 
224     /**
225      * Update the Constraints
226      * @param currentId
227      * @param id
228      * @param width
229      * @param height
230      * @return
231      */
updateConstraints(int currentId, int id, float width, float height)232     public int updateConstraints(int currentId, int id, float width, float height) {
233         if (currentId == id) {
234             State state;
235             if (id == -1) {
236                 state = mStateList.valueAt(0); // id not being used take the first
237             } else {
238                 state = mStateList.get(mCurrentStateId);
239 
240             }
241             if (state == null) {
242                 return -1;
243             }
244             if (mCurrentConstraintNumber != -1) {
245                 if (state.mVariants.get(currentId).match(width, height)) {
246                     return currentId;
247                 }
248             }
249             int match = state.findMatch(width, height);
250             if (currentId == match) {
251                 return currentId;
252             }
253 
254             return (match == -1) ? state.mConstraintID : state.mVariants.get(match).mConstraintID;
255 
256         } else  {
257             State state = mStateList.get(id);
258             if (state == null) {
259                 return  -1;
260             }
261             int match = state.findMatch(width, height);
262             return (match == -1) ? state.mConstraintID :  state.mVariants.get(match).mConstraintID;
263         }
264 
265     }
266 
267     /////////////////////////////////////////////////////////////////////////
268     //      This represents one state
269     /////////////////////////////////////////////////////////////////////////
270     static class State {
271         int mId;
272         ArrayList<Variant> mVariants = new ArrayList<>();
273         int mConstraintID = -1;
274         boolean mIsLayout = false;
275 
State(Context context, XmlPullParser parser)276         State(Context context, XmlPullParser parser) {
277             AttributeSet attrs = Xml.asAttributeSet(parser);
278             TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.State);
279             final int count = a.getIndexCount();
280             for (int i = 0; i < count; i++) {
281                 int attr = a.getIndex(i);
282                 if (attr == R.styleable.State_android_id) {
283                     mId = a.getResourceId(attr, mId);
284                 } else if (attr == R.styleable.State_constraints) {
285                     mConstraintID = a.getResourceId(attr, mConstraintID);
286                     String type = context.getResources().getResourceTypeName(mConstraintID);
287                     @SuppressWarnings("unused")
288                     String name = context.getResources().getResourceName(mConstraintID);
289 
290                     if ("layout".equals(type)) {
291                         mIsLayout = true;
292                     }
293                 }
294             }
295             a.recycle();
296         }
297 
add(Variant size)298         void add(Variant size) {
299             mVariants.add(size);
300         }
301 
findMatch(float width, float height)302         public int findMatch(float width, float height) {
303             for (int i = 0; i < mVariants.size(); i++) {
304                 if (mVariants.get(i).match(width, height)) {
305                     return i;
306                 }
307             }
308             return -1;
309         }
310     }
311 
312     static class Variant {
313         int mId;
314         float mMinWidth = Float.NaN;
315         float mMinHeight = Float.NaN;
316         float mMaxWidth = Float.NaN;
317         float mMaxHeight = Float.NaN;
318         int mConstraintID = -1;
319         boolean mIsLayout = false;
320 
Variant(Context context, XmlPullParser parser)321         Variant(Context context, XmlPullParser parser) {
322             AttributeSet attrs = Xml.asAttributeSet(parser);
323             TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Variant);
324             final int count = a.getIndexCount();
325             if (DEBUG) {
326                 Log.v(TAG, "############### Variant");
327             }
328 
329             for (int i = 0; i < count; i++) {
330                 int attr = a.getIndex(i);
331                 if (attr == R.styleable.Variant_constraints) {
332                     mConstraintID = a.getResourceId(attr, mConstraintID);
333                     String type = context.getResources().getResourceTypeName(mConstraintID);
334                     @SuppressWarnings("unused")
335                     String name = context.getResources().getResourceName(mConstraintID);
336 
337                     if ("layout".equals(type)) {
338                         mIsLayout = true;
339                     }
340                 } else if (attr == R.styleable.Variant_region_heightLessThan) {
341                     mMaxHeight = a.getDimension(attr, mMaxHeight);
342                 } else if (attr == R.styleable.Variant_region_heightMoreThan) {
343                     mMinHeight = a.getDimension(attr, mMinHeight);
344                 } else if (attr == R.styleable.Variant_region_widthLessThan) {
345                     mMaxWidth = a.getDimension(attr, mMaxWidth);
346                 } else if (attr == R.styleable.Variant_region_widthMoreThan) {
347                     mMinWidth = a.getDimension(attr, mMinWidth);
348                 } else {
349                     Log.v(TAG, "Unknown tag");
350                 }
351             }
352             a.recycle();
353             if (DEBUG) {
354                 Log.v(TAG, "############### Variant");
355                 if (!Float.isNaN(mMinWidth)) {
356                     Log.v(TAG, "############### Variant mMinWidth " + mMinWidth);
357                 }
358                 if (!Float.isNaN(mMinHeight)) {
359                     Log.v(TAG, "############### Variant mMinHeight " + mMinHeight);
360                 }
361                 if (!Float.isNaN(mMaxWidth)) {
362                     Log.v(TAG, "############### Variant mMaxWidth " + mMaxWidth);
363                 }
364                 if (!Float.isNaN(mMaxHeight)) {
365                     Log.v(TAG, "############### Variant mMinWidth " + mMaxHeight);
366                 }
367             }
368         }
369 
match(float widthDp, float heightDp)370         boolean match(float widthDp, float heightDp) {
371             if (DEBUG) {
372                 Log.v(TAG, "width = " + (int) widthDp
373                         + " < " + mMinWidth + " && " + (int) widthDp + " > " + mMaxWidth
374                         + " height = " + (int) heightDp
375                         + " < " + mMinHeight + " && " + (int) heightDp + " > " + mMaxHeight);
376             }
377             if (!Float.isNaN(mMinWidth)) {
378                 if (widthDp < mMinWidth) return false;
379             }
380             if (!Float.isNaN(mMinHeight)) {
381                 if (heightDp < mMinHeight) return false;
382             }
383             if (!Float.isNaN(mMaxWidth)) {
384                 if (widthDp > mMaxWidth) return false;
385             }
386             if (!Float.isNaN(mMaxHeight)) {
387                 if (heightDp > mMaxHeight) return false;
388             }
389             return true;
390         }
391     }
392 
393 }
394