1 /* 2 * Copyright (C) 2022 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 com.android.systemui.complication; 18 19 import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_DOWN; 20 import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_END; 21 import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_START; 22 import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_UP; 23 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM; 24 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_END; 25 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_START; 26 import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP; 27 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_IN_DURATION; 28 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DURATION; 29 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_DIRECTIONAL_SPACING_DEFAULT; 30 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_BOTTOM; 31 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_END; 32 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_START; 33 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_TOP; 34 import static com.android.systemui.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT; 35 36 import android.util.Log; 37 import android.view.View; 38 import android.view.ViewGroup; 39 40 import androidx.constraintlayout.widget.ConstraintLayout; 41 import androidx.constraintlayout.widget.Constraints; 42 43 import com.android.systemui.complication.ComplicationLayoutParams.Direction; 44 import com.android.systemui.complication.ComplicationLayoutParams.Position; 45 import com.android.systemui.complication.dagger.ComplicationModule; 46 import com.android.systemui.res.R; 47 import com.android.systemui.statusbar.CrossFadeHelper; 48 import com.android.systemui.touch.TouchInsetManager; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.Iterator; 54 import java.util.List; 55 import java.util.function.Consumer; 56 import java.util.stream.Collectors; 57 58 import javax.inject.Inject; 59 import javax.inject.Named; 60 61 /** 62 * {@link ComplicationLayoutEngine} arranges a collection of {@link ComplicationViewModel} based on 63 * their layout parameters and attributes. The management of this set is done by 64 * {@link ComplicationHostViewController}. 65 */ 66 @ComplicationModule.ComplicationScope 67 public class ComplicationLayoutEngine implements Complication.VisibilityController { 68 public static final String TAG = "ComplicationLayoutEng"; 69 70 /** 71 * Container for storing and operating on a tuple of margin values. 72 */ 73 public static class Margins { 74 public final int start; 75 public final int top; 76 public final int end; 77 public final int bottom; 78 79 /** 80 * Default constructor with all margins set to 0. 81 */ Margins()82 public Margins() { 83 this(0, 0, 0, 0); 84 } 85 86 /** 87 * Cosntructor to specify margin in each direction. 88 * @param start start margin 89 * @param top top margin 90 * @param end end margin 91 * @param bottom bottom margin 92 */ Margins(int start, int top, int end, int bottom)93 public Margins(int start, int top, int end, int bottom) { 94 this.start = start; 95 this.top = top; 96 this.end = end; 97 this.bottom = bottom; 98 } 99 100 /** 101 * Creates a new {@link Margins} by adding the corresponding dimensions together. 102 */ combine(Margins margins1, Margins margins2)103 public static Margins combine(Margins margins1, Margins margins2) { 104 return new Margins(margins1.start + margins2.start, 105 margins1.top + margins2.top, 106 margins1.end + margins2.end, 107 margins1.bottom + margins2.bottom); 108 } 109 } 110 111 /** 112 * {@link ViewEntry} is an internal container, capturing information necessary for working with 113 * a particular {@link Complication} view. 114 */ 115 private static class ViewEntry implements Comparable<ViewEntry> { 116 private final View mView; 117 private final ComplicationLayoutParams mLayoutParams; 118 private final TouchInsetManager.TouchInsetSession mTouchInsetSession; 119 private final Parent mParent; 120 @Complication.Category 121 private final int mCategory; 122 123 /** 124 * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding 125 * view hierarchy to be accessed without traversing the entire view tree. 126 */ ViewEntry(View view, ComplicationLayoutParams layoutParams, TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent)127 ViewEntry(View view, ComplicationLayoutParams layoutParams, 128 TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent) { 129 mView = view; 130 // Views that are generated programmatically do not have a unique id assigned to them 131 // at construction. A new id is assigned here to enable ConstraintLayout relative 132 // specifications. Existing ids for inflated views are not preserved. 133 // {@link Complication.ViewHolder} should not reference the root container by id. 134 mView.setId(View.generateViewId()); 135 mLayoutParams = layoutParams; 136 mTouchInsetSession = touchSession; 137 mCategory = category; 138 mParent = parent; 139 140 touchSession.addViewToTracking(mView); 141 } 142 143 /** 144 * Returns the {@link View} associated with the {@link Complication}. This is the instance 145 * passed in at construction. The reference to this {@link View} is captured when the 146 * {@link Complication} is added to the {@link ComplicationLayoutEngine}. The 147 * {@link Complication} cannot modify the {@link View} reference beyond this point. 148 */ getView()149 private View getView() { 150 return mView; 151 } 152 153 /** 154 * Returns The {@link ComplicationLayoutParams} associated with the view. 155 */ getLayoutParams()156 public ComplicationLayoutParams getLayoutParams() { 157 return mLayoutParams; 158 } 159 160 /** 161 * Interprets the {@link #getLayoutParams()} into {@link ConstraintLayout.LayoutParams} and 162 * applies them to the view. The method accounts for the relationship of the {@link View} to 163 * the other {@link Complication} views around it. The organization of the {@link View} 164 * instances in {@link ComplicationLayoutEngine} can be seen as lists. A {@link View} is 165 * either the head of its list or a following node. This head is passed into this method, 166 * which can be a reference to the {@link View} to indicate it is the head. 167 */ applyLayoutParams(View head)168 public void applyLayoutParams(View head) { 169 // Only the basic dimension parameters from the base ViewGroup.LayoutParams are carried 170 // over verbatim from the complication specified LayoutParam. Other fields are 171 // interpreted. 172 final ConstraintLayout.LayoutParams params = 173 new Constraints.LayoutParams(mLayoutParams.width, mLayoutParams.height); 174 175 final int direction = getLayoutParams().getDirection(); 176 177 final boolean snapsToGuide = getLayoutParams().snapsToGuide(); 178 179 // If no parent, view is the anchor. In this case, it is given the highest priority for 180 // alignment. All alignment preferences are done in relation to the parent container. 181 final boolean isRoot = head == mView; 182 183 // Each view can be seen as a vector, having a point (described here as position) and 184 // direction. When a view is the head of a position, then it is the first in a sequence 185 // of complications to appear from that position. For example, being the head for 186 // position POSITION_TOP | POSITION_END will cause the view to be shown as the first 187 // view in that corner. In this case, the positions specify which sides to align with 188 // the parent. If the view is not the head, the positions perpendicular to the direction 189 // of the view specify which side to align with the opposing side of the head view. 190 // Otherwise, the position aligns with the containing view. This means a 191 // POSITION_BOTTOM | POSITION_START with DIRECTION_UP non-head view's bottom to be 192 // aligned with the preceding view node's top and start to be aligned with the 193 // parent's start. 194 mLayoutParams.iteratePositions(position -> { 195 switch(position) { 196 case ComplicationLayoutParams.POSITION_START: 197 if (isRoot || direction != ComplicationLayoutParams.DIRECTION_END) { 198 params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; 199 } else { 200 params.startToEnd = head.getId(); 201 } 202 if (snapsToGuide 203 && (direction == ComplicationLayoutParams.DIRECTION_DOWN 204 || direction == ComplicationLayoutParams.DIRECTION_UP)) { 205 params.endToStart = R.id.complication_start_guide; 206 } 207 break; 208 case ComplicationLayoutParams.POSITION_TOP: 209 if (isRoot || direction != ComplicationLayoutParams.DIRECTION_DOWN) { 210 params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; 211 } else { 212 params.topToBottom = head.getId(); 213 } 214 if (snapsToGuide 215 && (direction == ComplicationLayoutParams.DIRECTION_END 216 || direction == ComplicationLayoutParams.DIRECTION_START)) { 217 params.endToStart = R.id.complication_top_guide; 218 } 219 break; 220 case ComplicationLayoutParams.POSITION_BOTTOM: 221 if (isRoot || direction != ComplicationLayoutParams.DIRECTION_UP) { 222 params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; 223 } else { 224 params.bottomToTop = head.getId(); 225 } 226 if (snapsToGuide 227 && (direction == ComplicationLayoutParams.DIRECTION_END 228 || direction == ComplicationLayoutParams.DIRECTION_START)) { 229 params.topToBottom = R.id.complication_bottom_guide; 230 } 231 break; 232 case ComplicationLayoutParams.POSITION_END: 233 if (isRoot || direction != ComplicationLayoutParams.DIRECTION_START) { 234 params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; 235 } else { 236 params.endToStart = head.getId(); 237 } 238 if (snapsToGuide 239 && (direction == ComplicationLayoutParams.DIRECTION_UP 240 || direction == ComplicationLayoutParams.DIRECTION_DOWN)) { 241 params.startToEnd = R.id.complication_end_guide; 242 } 243 break; 244 } 245 246 final Margins margins = mParent.getMargins(this, isRoot); 247 params.setMarginsRelative(margins.start, margins.top, margins.end, margins.bottom); 248 }); 249 250 if (mLayoutParams.constraintSpecified()) { 251 switch (direction) { 252 case ComplicationLayoutParams.DIRECTION_START: 253 case ComplicationLayoutParams.DIRECTION_END: 254 params.matchConstraintMaxWidth = mLayoutParams.getConstraint(); 255 break; 256 case ComplicationLayoutParams.DIRECTION_UP: 257 case ComplicationLayoutParams.DIRECTION_DOWN: 258 params.matchConstraintMaxHeight = mLayoutParams.getConstraint(); 259 break; 260 } 261 } 262 263 mView.setLayoutParams(params); 264 } 265 setGuide(ConstraintLayout.LayoutParams lp, int validDirections, Consumer<ConstraintLayout.LayoutParams> consumer)266 private void setGuide(ConstraintLayout.LayoutParams lp, int validDirections, 267 Consumer<ConstraintLayout.LayoutParams> consumer) { 268 final ComplicationLayoutParams layoutParams = getLayoutParams(); 269 if (!layoutParams.snapsToGuide()) { 270 return; 271 } 272 273 consumer.accept(lp); 274 } 275 276 /** 277 * Informs the {@link ViewEntry}'s parent entity to remove the {@link ViewEntry} from 278 * being shown further. 279 */ remove()280 public void remove() { 281 mParent.removeEntry(this); 282 283 ((ViewGroup) mView.getParent()).removeView(mView); 284 mTouchInsetSession.removeViewFromTracking(mView); 285 } 286 287 @Override compareTo(ViewEntry viewEntry)288 public int compareTo(ViewEntry viewEntry) { 289 // If the two entries have different categories, system complications take precedence. 290 if (viewEntry.mCategory != mCategory) { 291 // Note that this logic will need to be adjusted if more categories are introduced. 292 return mCategory == Complication.CATEGORY_SYSTEM ? 1 : -1; 293 } 294 295 // A higher weight indicates greater precedence if all else being equal. 296 if (viewEntry.mLayoutParams.getWeight() != mLayoutParams.getWeight()) { 297 return mLayoutParams.getWeight() > viewEntry.mLayoutParams.getWeight() ? 1 : -1; 298 } 299 300 return 0; 301 } 302 303 /** 304 * {@link Builder} allows for a multiple entities to contribute to the {@link ViewEntry} 305 * construction. This is necessary for setting an immutable parent, which might not be 306 * known until the view hierarchy is traversed. 307 */ 308 private static class Builder { 309 private final View mView; 310 private final TouchInsetManager.TouchInsetSession mTouchSession; 311 private final ComplicationLayoutParams mLayoutParams; 312 private final int mCategory; 313 private Parent mParent; 314 Builder(View view, TouchInsetManager.TouchInsetSession touchSession, ComplicationLayoutParams lp, @Complication.Category int category)315 Builder(View view, TouchInsetManager.TouchInsetSession touchSession, 316 ComplicationLayoutParams lp, @Complication.Category int category) { 317 mView = view; 318 mLayoutParams = lp; 319 mCategory = category; 320 mTouchSession = touchSession; 321 } 322 323 /** 324 * Returns the set {@link ComplicationLayoutParams} 325 */ getLayoutParams()326 public ComplicationLayoutParams getLayoutParams() { 327 return mLayoutParams; 328 } 329 330 /** 331 * Returns the set {@link Complication.Category}. 332 */ 333 @Complication.Category getCategory()334 public int getCategory() { 335 return mCategory; 336 } 337 338 /** 339 * Sets the parent. Note that this references to the entity for handling events, such as 340 * requesting the removal of the {@link View}. It is not the 341 * {@link android.view.ViewGroup} which contains the {@link View}. 342 */ setParent(Parent parent)343 Builder setParent(Parent parent) { 344 mParent = parent; 345 return this; 346 } 347 348 /** 349 * Builds and returns the resulting {@link ViewEntry}. 350 */ build()351 ViewEntry build() { 352 return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent); 353 } 354 } 355 356 /** 357 * An interface allowing an {@link ViewEntry} to signal events. 358 */ 359 interface Parent { 360 /** 361 * Indicates the {@link ViewEntry} requests removal. 362 */ removeEntry(ViewEntry entry)363 void removeEntry(ViewEntry entry); 364 365 /** 366 * Returns the margins to be applied to the entry 367 */ getMargins(ViewEntry entry, boolean isRoot)368 Margins getMargins(ViewEntry entry, boolean isRoot); 369 } 370 } 371 372 /** 373 * {@link PositionGroup} represents a collection of {@link Complication} at a given location. 374 * It further organizes the {@link Complication} by the direction in which they emanate from 375 * this position. 376 */ 377 private static class PositionGroup implements DirectionGroup.Parent { 378 private final HashMap<Integer, DirectionGroup> mDirectionGroups = new HashMap<>(); 379 380 private final HashMap<Integer, Margins> mDirectionalMargins; 381 382 private final int mDefaultDirectionalSpacing; 383 PositionGroup(int defaultDirectionalSpacing, HashMap<Integer, Margins> directionalMargins)384 PositionGroup(int defaultDirectionalSpacing, HashMap<Integer, Margins> directionalMargins) { 385 mDefaultDirectionalSpacing = defaultDirectionalSpacing; 386 mDirectionalMargins = directionalMargins; 387 } 388 389 /** 390 * Invoked by the {@link PositionGroup} holder to introduce a {@link Complication} view to 391 * this group. It is assumed that the caller has correctly identified this 392 * {@link PositionGroup} as the proper home for the {@link Complication} based on its 393 * declared position. 394 */ add(ViewEntry.Builder entryBuilder)395 public ViewEntry add(ViewEntry.Builder entryBuilder) { 396 final int direction = entryBuilder.getLayoutParams().getDirection(); 397 if (!mDirectionGroups.containsKey(direction)) { 398 mDirectionGroups.put(direction, new DirectionGroup(this)); 399 } 400 401 return mDirectionGroups.get(direction).add(entryBuilder); 402 } 403 404 @Override onEntriesChanged()405 public void onEntriesChanged() { 406 // Whenever an entry is added/removed from a child {@link DirectionGroup}, it is vital 407 // that all {@link DirectionGroup} children are visited. It is possible the overall 408 // head has changed, requiring constraints to be adjusted. 409 updateViews(); 410 } 411 412 @Override getDefaultDirectionalSpacing()413 public int getDefaultDirectionalSpacing() { 414 return mDefaultDirectionalSpacing; 415 } 416 417 @Override getMargins(ViewEntry entry, boolean isRoot)418 public Margins getMargins(ViewEntry entry, boolean isRoot) { 419 if (isRoot) { 420 Margins cumulativeMargins = new Margins(); 421 422 for (Margins margins : mDirectionalMargins.values()) { 423 cumulativeMargins = Margins.combine(margins, cumulativeMargins); 424 } 425 426 return cumulativeMargins; 427 } 428 429 return mDirectionalMargins.get(entry.getLayoutParams().getDirection()); 430 } 431 updateViews()432 private void updateViews() { 433 ViewEntry head = null; 434 435 // Identify which {@link Complication} head from the set of {@link DirectionGroup} 436 // should be treated as the {@link PositionGroup} head. 437 for (DirectionGroup directionGroup : mDirectionGroups.values()) { 438 final ViewEntry groupHead = directionGroup.getHead(); 439 if (head == null || (groupHead != null && groupHead.compareTo(head) > 0)) { 440 head = groupHead; 441 } 442 } 443 444 // A headless position group indicates no complications. 445 if (head == null) { 446 return; 447 } 448 449 for (DirectionGroup directionGroup : mDirectionGroups.values()) { 450 // Tell each {@link DirectionGroup} to update its containing {@link ViewEntry} based 451 // on the identified head. This iteration will also capture any newly added views. 452 directionGroup.updateViews(head.getView()); 453 } 454 } 455 getViews()456 private ArrayList<ViewEntry> getViews() { 457 final ArrayList<ViewEntry> views = new ArrayList<>(); 458 for (DirectionGroup directionGroup : mDirectionGroups.values()) { 459 views.addAll(directionGroup.getViews()); 460 } 461 return views; 462 } 463 } 464 465 /** 466 * A {@link DirectionGroup} organizes the {@link ViewEntry} of a parent group that point are 467 * laid out in the same direction. 468 */ 469 private static class DirectionGroup implements ViewEntry.Parent { 470 /** 471 * An interface implemented by the {@link DirectionGroup} parent to receive updates. 472 */ 473 interface Parent { 474 /** 475 * Invoked to indicate a change to the {@link ViewEntry} composition for this 476 * {@link DirectionGroup}. 477 */ onEntriesChanged()478 void onEntriesChanged(); 479 480 /** 481 * Returns the default spacing between elements. 482 */ getDefaultDirectionalSpacing()483 int getDefaultDirectionalSpacing(); 484 485 /** 486 * Returns the margins for the view entry. 487 */ getMargins(ViewEntry entry, boolean isRoot)488 Margins getMargins(ViewEntry entry, boolean isRoot); 489 } 490 private final ArrayList<ViewEntry> mViews = new ArrayList<>(); 491 private final Parent mParent; 492 493 /** 494 * Creates a new {@link DirectionGroup} with the specified parent. 495 */ DirectionGroup(Parent parent)496 DirectionGroup(Parent parent) { 497 mParent = parent; 498 } 499 500 /** 501 * Returns the head of the group. It is assumed that the order of the {@link ViewEntry} is 502 * proactively maintained. 503 */ getHead()504 public ViewEntry getHead() { 505 return mViews.isEmpty() ? null : mViews.get(0); 506 } 507 508 /** 509 * Adds a {@link ViewEntry} via {@link ViewEntry.Builder} to this group. 510 */ add(ViewEntry.Builder entryBuilder)511 public ViewEntry add(ViewEntry.Builder entryBuilder) { 512 final ViewEntry entry = entryBuilder.setParent(this).build(); 513 mViews.add(entry); 514 515 // After adding view, reverse sort collection. 516 Collections.sort(mViews); 517 Collections.reverse(mViews); 518 519 mParent.onEntriesChanged(); 520 521 return entry; 522 } 523 524 @Override removeEntry(ViewEntry entry)525 public void removeEntry(ViewEntry entry) { 526 // Sort is handled when the view is added, so should still be correct after removal. 527 // However, the head may have been removed, which may affect the layout of views in 528 // other DirectionGroups of the same PositionGroup. 529 mViews.remove(entry); 530 mParent.onEntriesChanged(); 531 } 532 533 @Override getMargins(ViewEntry entry, boolean isRoot)534 public Margins getMargins(ViewEntry entry, boolean isRoot) { 535 int directionalSpacing = entry.getLayoutParams().getDirectionalSpacing( 536 mParent.getDefaultDirectionalSpacing()); 537 538 Margins margins = new Margins(); 539 540 if (!isRoot) { 541 switch (entry.getLayoutParams().getDirection()) { 542 case ComplicationLayoutParams.DIRECTION_START: 543 margins = new Margins(0, 0, directionalSpacing, 0); 544 break; 545 case ComplicationLayoutParams.DIRECTION_UP: 546 margins = new Margins(0, 0, 0, directionalSpacing); 547 break; 548 case ComplicationLayoutParams.DIRECTION_END: 549 margins = new Margins(directionalSpacing, 0, 0, 0); 550 break; 551 case ComplicationLayoutParams.DIRECTION_DOWN: 552 margins = new Margins(0, directionalSpacing, 0, 0); 553 break; 554 } 555 } 556 557 return Margins.combine(mParent.getMargins(entry, isRoot), margins); 558 } 559 560 /** 561 * Invoked by {@link Parent} to update the layout of all children {@link ViewEntry} with 562 * the specified head. Note that the head might not be in this group and instead part of a 563 * neighboring group. 564 */ updateViews(View groupHead)565 public void updateViews(View groupHead) { 566 Iterator<ViewEntry> it = mViews.iterator(); 567 568 while (it.hasNext()) { 569 final ViewEntry viewEntry = it.next(); 570 viewEntry.applyLayoutParams(groupHead); 571 groupHead = viewEntry.getView(); 572 } 573 } 574 getViews()575 private List<ViewEntry> getViews() { 576 return mViews; 577 } 578 } 579 580 private final ConstraintLayout mLayout; 581 private final int mDefaultDirectionalSpacing; 582 private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>(); 583 private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>(); 584 private final TouchInsetManager.TouchInsetSession mSession; 585 private final int mFadeInDuration; 586 private final int mFadeOutDuration; 587 private final HashMap<Integer, HashMap<Integer, Margins>> mPositionDirectionMarginMapping; 588 589 /** */ 590 @Inject ComplicationLayoutEngine(@amedSCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, @Named(COMPLICATION_DIRECTIONAL_SPACING_DEFAULT) int defaultDirectionalSpacing, @Named(COMPLICATION_MARGIN_POSITION_START) int complicationMarginPositionStart, @Named(COMPLICATION_MARGIN_POSITION_TOP) int complicationMarginPositionTop, @Named(COMPLICATION_MARGIN_POSITION_END) int complicationMarginPositionEnd, @Named(COMPLICATION_MARGIN_POSITION_BOTTOM) int complicationMarginPositionBottom, TouchInsetManager.TouchInsetSession session, @Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration, @Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration)591 public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, 592 @Named(COMPLICATION_DIRECTIONAL_SPACING_DEFAULT) int defaultDirectionalSpacing, 593 @Named(COMPLICATION_MARGIN_POSITION_START) int complicationMarginPositionStart, 594 @Named(COMPLICATION_MARGIN_POSITION_TOP) int complicationMarginPositionTop, 595 @Named(COMPLICATION_MARGIN_POSITION_END) int complicationMarginPositionEnd, 596 @Named(COMPLICATION_MARGIN_POSITION_BOTTOM) int complicationMarginPositionBottom, 597 TouchInsetManager.TouchInsetSession session, 598 @Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration, 599 @Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration) { 600 mLayout = layout; 601 mDefaultDirectionalSpacing = defaultDirectionalSpacing; 602 mSession = session; 603 mFadeInDuration = fadeInDuration; 604 mFadeOutDuration = fadeOutDuration; 605 mPositionDirectionMarginMapping = generatePositionDirectionalMarginsMapping( 606 complicationMarginPositionStart, 607 complicationMarginPositionTop, 608 complicationMarginPositionEnd, 609 complicationMarginPositionBottom); 610 } 611 612 private static HashMap<Integer, HashMap<Integer, Margins>> generatePositionDirectionalMarginsMapping(int complicationMarginPositionStart, int complicationMarginPositionTop, int complicationMarginPositionEnd, int complicationMarginPositionBottom)613 generatePositionDirectionalMarginsMapping(int complicationMarginPositionStart, 614 int complicationMarginPositionTop, 615 int complicationMarginPositionEnd, 616 int complicationMarginPositionBottom) { 617 HashMap<Integer, HashMap<Integer, Margins>> mapping = new HashMap<>(); 618 619 final Margins startMargins = new Margins(complicationMarginPositionStart, 0, 0, 0); 620 final Margins topMargins = new Margins(0, complicationMarginPositionTop, 0, 0); 621 final Margins endMargins = new Margins(0, 0, complicationMarginPositionEnd, 0); 622 final Margins bottomMargins = new Margins(0, 0, 0, complicationMarginPositionBottom); 623 624 addToMapping(mapping, POSITION_START | POSITION_TOP, DIRECTION_END, topMargins); 625 addToMapping(mapping, POSITION_START | POSITION_TOP, DIRECTION_DOWN, startMargins); 626 627 addToMapping(mapping, POSITION_START | POSITION_BOTTOM, DIRECTION_END, bottomMargins); 628 addToMapping(mapping, POSITION_START | POSITION_BOTTOM, DIRECTION_UP, startMargins); 629 630 addToMapping(mapping, POSITION_END | POSITION_TOP, DIRECTION_START, topMargins); 631 addToMapping(mapping, POSITION_END | POSITION_TOP, DIRECTION_DOWN, endMargins); 632 633 addToMapping(mapping, POSITION_END | POSITION_BOTTOM, DIRECTION_START, bottomMargins); 634 addToMapping(mapping, POSITION_END | POSITION_BOTTOM, DIRECTION_UP, endMargins); 635 636 return mapping; 637 } 638 addToMapping(HashMap<Integer, HashMap<Integer, Margins>> mapping, @Position int position, @Direction int direction, Margins margins)639 private static void addToMapping(HashMap<Integer, HashMap<Integer, Margins>> mapping, 640 @Position int position, @Direction int direction, Margins margins) { 641 if (!mapping.containsKey(position)) { 642 mapping.put(position, new HashMap<>()); 643 } 644 mapping.get(position).put(direction, margins); 645 } 646 647 @Override setVisibility(int visibility)648 public void setVisibility(int visibility) { 649 if (visibility == View.VISIBLE) { 650 CrossFadeHelper.fadeIn(mLayout, mFadeInDuration, /* delay= */ 0); 651 } else { 652 CrossFadeHelper.fadeOut( 653 mLayout, 654 mFadeOutDuration, 655 /* delay= */ 0); 656 } 657 } 658 659 /** 660 * Adds a complication to this {@link ComplicationLayoutEngine}. 661 * @param id A {@link ComplicationId} unique to this complication. If this matches a 662 * complication within this {@link ComplicationViewModel}, the existing complication 663 * will be removed. 664 * @param view The {@link View} to be shown. 665 * @param lp The {@link ComplicationLayoutParams} as expressed by the {@link Complication}. 666 * These will be interpreted into the final applied parameters. 667 * @param category The {@link Complication.Category} for the {@link Complication}. 668 */ addComplication(ComplicationId id, View view, ComplicationLayoutParams lp, @Complication.Category int category)669 public void addComplication(ComplicationId id, View view, 670 ComplicationLayoutParams lp, @Complication.Category int category) { 671 Log.d(TAG, "@" + Integer.toHexString(this.hashCode()) + " addComplication: " + id); 672 673 // If the complication is present, remove. 674 if (mEntries.containsKey(id)) { 675 removeComplication(id); 676 } 677 678 final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category); 679 680 // Add position group if doesn't already exist 681 final int position = lp.getPosition(); 682 if (!mPositions.containsKey(position)) { 683 mPositions.put(position, new PositionGroup(mDefaultDirectionalSpacing, 684 mPositionDirectionMarginMapping.get(lp.getPosition()))); 685 } 686 687 // Insert entry into group 688 final ViewEntry entry = mPositions.get(position).add(entryBuilder); 689 mEntries.put(id, entry); 690 691 mLayout.addView(entry.getView()); 692 } 693 694 /** 695 * Removes a complication by {@link ComplicationId}. 696 */ removeComplication(ComplicationId id)697 public boolean removeComplication(ComplicationId id) { 698 final ViewEntry entry = mEntries.remove(id); 699 700 if (entry == null) { 701 Log.e(TAG, "could not find id:" + id); 702 return false; 703 } 704 705 entry.remove(); 706 return true; 707 } 708 709 /** 710 * Gets an unordered list of all the views at a particular position. 711 */ getViewsAtPosition(@osition int position)712 public List<View> getViewsAtPosition(@Position int position) { 713 return mPositions.entrySet().stream() 714 .filter(entry -> (entry.getKey() & position) == position) 715 .flatMap(entry -> entry.getValue().getViews().stream()) 716 .map(ViewEntry::getView) 717 .collect(Collectors.toList()); 718 } 719 } 720