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