• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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