1 /*
2  * Copyright (C) 2021 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.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.util.SparseArray;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.ViewParent;
29 
30 import androidx.constraintlayout.core.widgets.ConstraintWidget;
31 import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
32 import androidx.constraintlayout.core.widgets.Helper;
33 import androidx.constraintlayout.core.widgets.HelperWidget;
34 
35 import org.jspecify.annotations.NonNull;
36 
37 import java.lang.reflect.Field;
38 import java.util.Arrays;
39 import java.util.HashMap;
40 
41 /**
42  *
43  * <b>Added in 1.1</b>
44  * <p>
45  *     This class manages a set of referenced widgets. HelperWidget objects can be
46  *     created to act upon the set
47  *     of referenced widgets. The difference between {@code ConstraintHelper} and
48  *     {@code ViewGroup} is that
49  *     multiple {@code ConstraintHelper} can reference the same widgets.
50  * <p>
51  *     Widgets are referenced by being added to a comma separated list of ids, e.g.:
52  *     <pre>
53  *     {@code
54  *         <androidx.constraintlayout.widget.Barrier
55  *              android:id="@+id/barrier"
56  *              android:layout_width="wrap_content"
57  *              android:layout_height="wrap_content"
58  *              app:barrierDirection="start"
59  *              app:constraint_referenced_ids="button1,button2" />
60  *     }
61  *     </pre>
62  * </p>
63  */
64 public abstract class ConstraintHelper extends View {
65 
66     /**
67      *
68      */
69     protected int[] mIds = new int[32];
70     /**
71      *
72      */
73     protected int mCount;
74 
75     /**
76      *
77      */
78     protected Context myContext;
79     /**
80      *
81      */
82     protected Helper mHelperWidget;
83     /**
84      *
85      */
86     protected boolean mUseViewMeasure = false;
87     /**
88      *
89      */
90     protected String mReferenceIds;
91     /**
92      *
93      */
94     protected String mReferenceTags;
95 
96     /**
97      *
98      */
99     private View[] mViews = null;
100 
101     /**
102      *
103      */
104     protected final static String CHILD_TAG = "CONSTRAINT_LAYOUT_HELPER_CHILD";
105 
106     protected HashMap<Integer, String> mMap = new HashMap<>();
107 
ConstraintHelper(Context context)108     public ConstraintHelper(Context context) {
109         super(context);
110         myContext = context;
111         init(null);
112     }
113 
ConstraintHelper(Context context, AttributeSet attrs)114     public ConstraintHelper(Context context, AttributeSet attrs) {
115         super(context, attrs);
116         myContext = context;
117         init(attrs);
118     }
119 
ConstraintHelper(Context context, AttributeSet attrs, int defStyleAttr)120     public ConstraintHelper(Context context, AttributeSet attrs, int defStyleAttr) {
121         super(context, attrs, defStyleAttr);
122         myContext = context;
123         init(attrs);
124     }
125 
126     /**
127      *
128      */
init(AttributeSet attrs)129     protected void init(AttributeSet attrs) {
130         if (attrs != null) {
131             TypedArray a = getContext().obtainStyledAttributes(attrs,
132                     R.styleable.ConstraintLayout_Layout);
133             final int count = a.getIndexCount();
134             for (int i = 0; i < count; i++) {
135                 int attr = a.getIndex(i);
136                 if (attr == R.styleable.ConstraintLayout_Layout_constraint_referenced_ids) {
137                     mReferenceIds = a.getString(attr);
138                     setIds(mReferenceIds);
139                 } else if (attr == R.styleable.ConstraintLayout_Layout_constraint_referenced_tags) {
140                     mReferenceTags = a.getString(attr);
141                     setReferenceTags(mReferenceTags);
142                 }
143             }
144             a.recycle();
145         }
146     }
147 
148     @Override
onAttachedToWindow()149     protected void onAttachedToWindow() {
150         super.onAttachedToWindow();
151         if (mReferenceIds != null) {
152             setIds(mReferenceIds);
153         }
154         if (mReferenceTags != null) {
155             setReferenceTags(mReferenceTags);
156         }
157     }
158 
159     /**
160      * Add a view to the helper. The referenced view need to be a child of the helper's parent.
161      * The view also need to have its id set in order to be added.
162      *
163      * @param view
164      */
addView(View view)165     public void addView(View view) {
166         if (view == this) {
167             return;
168         }
169         if (view.getId() == -1) {
170             Log.e("ConstraintHelper", "Views added to a ConstraintHelper need to have an id");
171             return;
172         }
173         if (view.getParent() == null) {
174             Log.e("ConstraintHelper", "Views added to a ConstraintHelper need to have a parent");
175             return;
176         }
177         mReferenceIds = null;
178         addRscID(view.getId());
179         requestLayout();
180     }
181 
182     /**
183      * Remove a given view from the helper.
184      *
185      * @param view
186      * @return index of view removed
187      */
removeView(View view)188     public int removeView(View view) {
189         int index = -1;
190         int id = view.getId();
191         if (id == -1) {
192             return index;
193         }
194         mReferenceIds = null;
195         for (int i = 0; i < mCount; i++) {
196             if (mIds[i] == id) {
197                 index = i;
198                 for (int j = i; j < mCount - 1; j++) {
199                     mIds[j] = mIds[j + 1];
200                 }
201                 mIds[mCount - 1] = 0;
202                 mCount--;
203                 break;
204             }
205         }
206         requestLayout();
207         return index;
208     }
209 
210     /**
211      * Helpers typically reference a collection of ids
212      * @return ids referenced
213      */
getReferencedIds()214     public int[] getReferencedIds() {
215         return Arrays.copyOf(mIds, mCount);
216     }
217 
218     /**
219      * Helpers typically reference a collection of ids
220      */
setReferencedIds(int[] ids)221     public void setReferencedIds(int[] ids) {
222         mReferenceIds = null;
223         mCount = 0;
224         for (int i = 0; i < ids.length; i++) {
225             addRscID(ids[i]);
226         }
227     }
228 
229     /**
230      *
231      */
addRscID(int id)232     private void addRscID(int id) {
233         if (id == getId()) {
234             return;
235         }
236         if (mCount + 1 > mIds.length) {
237             mIds = Arrays.copyOf(mIds, mIds.length * 2);
238         }
239         mIds[mCount] = id;
240         mCount++;
241     }
242 
243     /**
244      *
245      */
246     @Override
onDraw(@onNull Canvas canvas)247     public void onDraw(@NonNull Canvas canvas) {
248         // Nothing
249     }
250 
251     /**
252      *
253      */
254     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)255     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
256         if (mUseViewMeasure) {
257             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
258         } else {
259             setMeasuredDimension(0, 0);
260         }
261     }
262 
263     /**
264      *
265      * Allows a helper to replace the default ConstraintWidget in LayoutParams by its own subclass
266      */
validateParams()267     public void validateParams() {
268         if (mHelperWidget == null) {
269             return;
270         }
271         ViewGroup.LayoutParams params = getLayoutParams();
272         if (params instanceof ConstraintLayout.LayoutParams) {
273             ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) params;
274             layoutParams.mWidget = (ConstraintWidget) mHelperWidget;
275         }
276     }
277 
278     /**
279      *
280      */
addID(String idString)281     private void addID(String idString) {
282         if (idString == null || idString.length() == 0) {
283             return;
284         }
285         if (myContext == null) {
286             return;
287         }
288 
289         idString = idString.trim();
290 
291         int rscId = findId(idString);
292         if (rscId != 0) {
293             mMap.put(rscId, idString); // let's remember the idString used,
294             // as we may need it for dynamic modules
295             addRscID(rscId);
296         } else {
297             Log.w("ConstraintHelper", "Could not find id of \"" + idString + "\"");
298         }
299     }
300 
301     /**
302      *
303      */
addTag(String tagString)304     private void addTag(String tagString) {
305         if (tagString == null || tagString.length() == 0) {
306             return;
307         }
308         if (myContext == null) {
309             return;
310         }
311 
312         tagString = tagString.trim();
313 
314         ConstraintLayout parent = null;
315         if (getParent() instanceof ConstraintLayout) {
316             parent = (ConstraintLayout) getParent();
317         }
318         if (parent == null) {
319             Log.w("ConstraintHelper", "Parent not a ConstraintLayout");
320             return;
321         }
322         int count = parent.getChildCount();
323         for (int i = 0; i < count; i++) {
324             View v = parent.getChildAt(i);
325             ViewGroup.LayoutParams params = v.getLayoutParams();
326             if (params instanceof ConstraintLayout.LayoutParams) {
327                 ConstraintLayout.LayoutParams lp = (ConstraintLayout.LayoutParams) params;
328                 if (tagString.equals(lp.constraintTag)) {
329                     if (v.getId() == View.NO_ID) {
330                         Log.w("ConstraintHelper", "to use ConstraintTag view "
331                                 + v.getClass().getSimpleName() + " must have an ID");
332                     } else {
333                         addRscID(v.getId());
334                     }
335                 }
336             }
337 
338         }
339     }
340 
341     /**
342      * Attempt to find the id given a reference string
343      * @param referenceId
344      * @return
345      */
findId(String referenceId)346     private int findId(String referenceId) {
347         ConstraintLayout parent = null;
348         if (getParent() instanceof ConstraintLayout) {
349             parent = (ConstraintLayout) getParent();
350         }
351         int rscId = 0;
352 
353         // First, if we are in design mode let's get the cached information
354         if (isInEditMode() && parent != null) {
355             Object value = parent.getDesignInformation(0, referenceId);
356             if (value instanceof Integer) {
357                 rscId = (Integer) value;
358             }
359         }
360 
361         // ... if not, let's check our siblings
362         if (rscId == 0 && parent != null) {
363             // TODO: cache this in ConstraintLayout
364             rscId = findId(parent, referenceId);
365         }
366 
367         if (rscId == 0) {
368             try {
369                 Class res = R.id.class;
370                 Field field = res.getField(referenceId);
371                 rscId = field.getInt(null);
372             } catch (Exception e) {
373                 // Do nothing
374             }
375         }
376 
377         if (rscId == 0) {
378             // this will first try to parse the string id as a number (!) in ResourcesImpl, so
379             // let's try that last...
380             rscId = myContext.getResources().getIdentifier(referenceId, "id",
381                     myContext.getPackageName());
382         }
383 
384         return rscId;
385     }
386 
387     /**
388      * Iterate through the container's children to find a matching id.
389      * Slow path, seems necessary to handle dynamic modules resolution...
390      *
391      * @param container
392      * @param idString
393      * @return
394      */
findId(ConstraintLayout container, String idString)395     private int findId(ConstraintLayout container, String idString) {
396         if (idString == null || container == null) {
397             return 0;
398         }
399         Resources resources = myContext.getResources();
400         if (resources == null) {
401             return 0;
402         }
403         final int count = container.getChildCount();
404         for (int j = 0; j < count; j++) {
405             View child = container.getChildAt(j);
406             if (child.getId() != -1) {
407                 String res = null;
408                 try {
409                     res = resources.getResourceEntryName(child.getId());
410                 } catch (android.content.res.Resources.NotFoundException e) {
411                     // nothing
412                 }
413                 if (idString.equals(res)) {
414                     return child.getId();
415                 }
416             }
417         }
418         return 0;
419     }
420 
421     /**
422      *
423      */
setIds(String idList)424     protected void setIds(String idList) {
425         mReferenceIds = idList;
426         if (idList == null) {
427             return;
428         }
429         int begin = 0;
430         mCount = 0;
431         while (true) {
432             int end = idList.indexOf(',', begin);
433             if (end == -1) {
434                 addID(idList.substring(begin));
435                 break;
436             }
437             addID(idList.substring(begin, end));
438             begin = end + 1;
439         }
440     }
441 
442     /**
443      *
444      */
setReferenceTags(String tagList)445     protected void setReferenceTags(String tagList) {
446         mReferenceTags = tagList;
447         if (tagList == null) {
448             return;
449         }
450         int begin = 0;
451         mCount = 0;
452         while (true) {
453             int end = tagList.indexOf(',', begin);
454             if (end == -1) {
455                 addTag(tagList.substring(begin));
456                 break;
457             }
458             addTag(tagList.substring(begin, end));
459             begin = end + 1;
460         }
461     }
462 
463     /**
464      *
465      * @param container
466      */
applyLayoutFeatures(ConstraintLayout container)467     protected void applyLayoutFeatures(ConstraintLayout container) {
468         int visibility = getVisibility();
469         float elevation = 0;
470         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
471             elevation = getElevation();
472         }
473         for (int i = 0; i < mCount; i++) {
474             int id = mIds[i];
475             View view = container.getViewById(id);
476             if (view != null) {
477                 view.setVisibility(visibility);
478                 if (elevation > 0
479                         && android.os.Build.VERSION.SDK_INT
480                         >= android.os.Build.VERSION_CODES.LOLLIPOP) {
481                     view.setTranslationZ(view.getTranslationZ() + elevation);
482                 }
483             }
484         }
485     }
486 
487     /**
488      *
489      */
applyLayoutFeatures()490     protected void applyLayoutFeatures() {
491         ViewParent parent = getParent();
492         if (parent != null && parent instanceof ConstraintLayout) {
493             applyLayoutFeatures((ConstraintLayout) parent);
494         }
495     }
496 
497     /**
498      *
499      */
applyLayoutFeaturesInConstraintSet(ConstraintLayout container)500     protected void applyLayoutFeaturesInConstraintSet(ConstraintLayout container) {}
501 
502     /**
503      *
504      * Allows a helper a chance to update its internal object pre layout or
505      * set up connections for the pointed elements
506      *
507      * @param container
508      */
updatePreLayout(ConstraintLayout container)509     public void updatePreLayout(ConstraintLayout container) {
510         if (isInEditMode()) {
511             setIds(mReferenceIds);
512         }
513         if (mHelperWidget == null) {
514             return;
515         }
516         mHelperWidget.removeAllIds();
517         for (int i = 0; i < mCount; i++) {
518             int id = mIds[i];
519             View view = container.getViewById(id);
520             if (view == null) {
521                 // hm -- we couldn't find the view.
522                 // It might still be there though, but with the wrong id (with dynamic modules)
523                 String candidate = mMap.get(id);
524                 int foundId = findId(container, candidate);
525                 if (foundId != 0) {
526                     mIds[i] = foundId;
527                     mMap.put(foundId, candidate);
528                     view = container.getViewById(foundId);
529                 }
530             }
531             if (view != null) {
532                 mHelperWidget.add(container.getViewWidget(view));
533             }
534         }
535         mHelperWidget.updateConstraints(container.mLayoutWidget);
536     }
537 
538     /**
539      * called before solver resolution
540      * @param container
541      * @param helper
542      * @param map
543      */
updatePreLayout(ConstraintWidgetContainer container, Helper helper, SparseArray<ConstraintWidget> map)544     public void updatePreLayout(ConstraintWidgetContainer container,
545                                 Helper helper,
546                                 SparseArray<ConstraintWidget> map) {
547         helper.removeAllIds();
548         for (int i = 0; i < mCount; i++) {
549             int id = mIds[i];
550             helper.add(map.get(id));
551         }
552     }
553 
getViews(ConstraintLayout layout)554     protected View [] getViews(ConstraintLayout layout) {
555 
556         if (mViews == null || mViews.length != mCount) {
557             mViews = new View[mCount];
558         }
559 
560         for (int i = 0; i < mCount; i++) {
561             int id = mIds[i];
562             mViews[i] = layout.getViewById(id);
563         }
564         return mViews;
565     }
566 
567     /**
568      *
569      * Allows a helper a chance to update its internal object post layout or
570      * set up connections for the pointed elements
571      *
572      * @param container
573      */
updatePostLayout(ConstraintLayout container)574     public void updatePostLayout(ConstraintLayout container) {
575         // Do nothing
576     }
577 
578     /**
579      *
580      * @param container
581      */
updatePostMeasure(ConstraintLayout container)582     public void updatePostMeasure(ConstraintLayout container) {
583         // Do nothing
584     }
585 
586     /**
587      * update after constraints are resolved
588      * @param container
589      */
updatePostConstraints(ConstraintLayout container)590     public void updatePostConstraints(ConstraintLayout container) {
591         // Do nothing
592     }
593 
594     /**
595      * called before the draw
596      * @param container
597      */
updatePreDraw(ConstraintLayout container)598     public void updatePreDraw(ConstraintLayout container) {
599         // Do nothing
600     }
601 
602     /**
603      * Load the parameters
604      * @param constraint
605      * @param child
606      * @param layoutParams
607      * @param mapIdToWidget
608      */
loadParameters(ConstraintSet.Constraint constraint, HelperWidget child, ConstraintLayout.LayoutParams layoutParams, SparseArray<ConstraintWidget> mapIdToWidget)609     public void loadParameters(ConstraintSet.Constraint constraint,
610                                HelperWidget child,
611                                ConstraintLayout.LayoutParams layoutParams,
612                                SparseArray<ConstraintWidget> mapIdToWidget) {
613         // TODO: rethink this. The list of views shouldn't be resolved at updatePreLayout stage,
614         // as this makes changing referenced views tricky at runtime
615         if (constraint.layout.mReferenceIds != null) {
616             setReferencedIds(constraint.layout.mReferenceIds);
617         } else if (constraint.layout.mReferenceIdString != null) {
618             if (constraint.layout.mReferenceIdString.length() > 0) {
619                 constraint.layout.mReferenceIds = convertReferenceString(
620                         constraint.layout.mReferenceIdString);
621             } else {
622                 constraint.layout.mReferenceIds = null;
623             }
624         }
625         if (child != null) {
626             child.removeAllIds();
627             if (constraint.layout.mReferenceIds != null) {
628                 for (int i = 0; i < constraint.layout.mReferenceIds.length; i++) {
629                     int id = constraint.layout.mReferenceIds[i];
630                     ConstraintWidget widget = mapIdToWidget.get(id);
631                     if (widget != null) {
632                         child.add(widget);
633                     }
634                 }
635             }
636         }
637     }
638 
convertReferenceString(String referenceIdString)639     private int[] convertReferenceString(String referenceIdString) {
640         String[] split = referenceIdString.split(",");
641         int[] rscIds = new int[split.length];
642         int count = 0;
643         for (int i = 0; i < split.length; i++) {
644             String idString = split[i];
645             idString = idString.trim();
646             int id = findId(idString);
647             if (id != 0) {
648                 rscIds[count++] = id;
649             }
650         }
651         if (count != split.length) {
652             rscIds = Arrays.copyOf(rscIds, count);
653         }
654         return rscIds;
655     }
656 
657     /**
658      * resolve the RTL
659      * @param widget
660      * @param isRtl
661      */
resolveRtl(ConstraintWidget widget, boolean isRtl)662     public void resolveRtl(ConstraintWidget widget, boolean isRtl) {
663         // nothing here
664     }
665 
666     @Override
setTag(int key, Object tag)667     public void setTag(int key, Object tag) {
668         super.setTag(key, tag);
669         if (tag == null && mReferenceIds == null) {
670             addRscID(key);
671         }
672     }
673 
674     /**
675      * does id table contain the id
676      *
677      * @param id
678      * @return
679      */
containsId(final int id)680     public boolean containsId(final int id) {
681         boolean result = false;
682         for (int i : mIds) {
683             if (i == id) {
684                 result = true;
685                 break;
686             }
687         }
688         return result;
689     }
690 
691     /**
692      * find the position of an id
693      *
694      * @param id
695      * @return
696      */
indexFromId(final int id)697     public int indexFromId(final int id) {
698         int index = -1;
699         for (int i : mIds) {
700             index++;
701             if (i == id) {
702                 return index;
703             }
704         }
705         return index;
706     }
707 
708     /**
709      * hook for helpers to apply parameters in MotionLayout
710      */
applyHelperParams()711     public void applyHelperParams() {
712 
713     }
714 
isChildOfHelper(View v)715     public static boolean isChildOfHelper(View v) {
716        return CHILD_TAG == v.getTag();
717     }
718 }
719