• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2   * Copyright (C) 2012 The Android Open Source Project
3   *
4   * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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  package com.android.ide.common.layout.relative;
17  
18  import static com.android.SdkConstants.ANDROID_URI;
19  import static com.android.SdkConstants.ATTR_ID;
20  import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
21  import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
22  import static com.android.SdkConstants.ID_PREFIX;
23  import static com.android.SdkConstants.NEW_ID_PREFIX;
24  import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
25  import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL;
26  import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL;
27  
28  import com.android.SdkConstants;
29  import com.android.annotations.NonNull;
30  import com.android.annotations.Nullable;
31  import com.android.ide.common.api.INode;
32  import com.android.ide.common.api.INode.IAttribute;
33  import com.google.common.collect.Maps;
34  import com.google.common.collect.Sets;
35  
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  
40  /**
41   * Handles deletions in a relative layout, transferring constraints across
42   * deleted nodes
43   * <p>
44   * TODO: Consider adding the
45   * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a
46   * node if it's pointing to a node which is deleted and which has no transitive
47   * reference to another node.
48   */
49  public class DeletionHandler {
50      private final INode mLayout;
51      private final INode[] mChildren;
52      private final List<INode> mDeleted;
53      private final Set<String> mDeletedIds;
54      private final Map<String, INode> mNodeMap;
55      private final List<INode> mMoved;
56  
57      /**
58       * Creates a new {@link DeletionHandler}
59       *
60       * @param deleted the deleted nodes
61       * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere)
62       * @param layout the parent layout of the deleted nodes
63       */
DeletionHandler(@onNull List<INode> deleted, @NonNull List<INode> moved, @NonNull INode layout)64      public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved,
65              @NonNull INode layout) {
66          mDeleted = deleted;
67          mMoved = moved;
68          mLayout = layout;
69  
70          mChildren = mLayout.getChildren();
71          mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length);
72          for (INode child : mChildren) {
73              String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
74              if (id != null) {
75                  mNodeMap.put(stripIdPrefix(id), child);
76              }
77          }
78  
79          mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size());
80          for (INode node : mDeleted) {
81              String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
82              if (id != null) {
83                  mDeletedIds.add(stripIdPrefix(id));
84              }
85          }
86  
87          // Any widgets that remain (e.g. typically because they were moved) should
88          // keep their incoming dependencies
89          for (INode node : mMoved) {
90              String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
91              if (id != null) {
92                  mDeletedIds.remove(stripIdPrefix(id));
93              }
94          }
95      }
96  
97      @Nullable
getId(@onNull IAttribute attribute)98      private static String getId(@NonNull IAttribute attribute) {
99          if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
100                  && ANDROID_URI.equals(attribute.getUri())
101                  && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) {
102              String id = attribute.getValue();
103              // It might not be an id reference, so check manually rather than just
104              // calling stripIdPrefix():
105              if (id.startsWith(NEW_ID_PREFIX)) {
106                  return id.substring(NEW_ID_PREFIX.length());
107              } else if (id.startsWith(ID_PREFIX)) {
108                  return id.substring(ID_PREFIX.length());
109              }
110          }
111  
112          return null;
113      }
114  
115      /**
116       * Updates the constraints in the layout to handle deletion of a set of
117       * nodes. This ensures that any constraints pointing to one of the deleted
118       * nodes are changed properly to point to a non-deleted node with similar
119       * constraints.
120       */
updateConstraints()121      public void updateConstraints() {
122          if (mChildren.length == mDeleted.size()) {
123              // Deleting everything: Nothing to be done
124              return;
125          }
126  
127          // Now remove incoming edges to any views that were deleted. If possible,
128          // don't just delete them but replace them with a transitive constraint, e.g.
129          // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C",
130  
131          for (INode child : mChildren) {
132              if (mDeleted.contains(child)) {
133                  continue;
134              }
135  
136              for (IAttribute attribute : child.getLiveAttributes()) {
137                  String id = getId(attribute);
138                  if (id != null) {
139                      if (mDeletedIds.contains(id)) {
140                          // Unset this reference to a deleted widget. It might be
141                          // replaced if the pointed to node points to some other node
142                          // on the same side, but it may use a different constraint name,
143                          // or have none at all (e.g. parent).
144                          String name = attribute.getName();
145                          child.setAttribute(ANDROID_URI, name, null);
146  
147                          INode deleted = mNodeMap.get(id);
148                          if (deleted != null) {
149                              ConstraintType type = ConstraintType.fromAttribute(name);
150                              if (type != null) {
151                                  transfer(deleted, child, type, 0);
152                              }
153                          }
154                      }
155                  }
156              }
157          }
158      }
159  
transfer(INode deleted, INode target, ConstraintType targetType, int depth)160      private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) {
161          if (depth == 20) {
162              // Prevent really deep flow or unbounded recursion in case there is a bug in
163              // the cycle detection code
164              return;
165          }
166  
167          assert mDeleted.contains(deleted);
168  
169          for (IAttribute attribute : deleted.getLiveAttributes()) {
170              String name = attribute.getName();
171              ConstraintType type = ConstraintType.fromAttribute(name);
172              if (type == null) {
173                  continue;
174              }
175  
176              ConstraintType transfer = getCompatibleConstraint(type, targetType);
177              if (transfer != null) {
178                  String id = getId(attribute);
179                  if (id != null) {
180                      if (mDeletedIds.contains(id)) {
181                          INode nextDeleted = mNodeMap.get(id);
182                          if (nextDeleted != null) {
183                              // Points to another deleted node: recurse
184                              transfer(nextDeleted, target, targetType, depth + 1);
185                          }
186                      } else {
187                          // Found an undeleted node destination: point to it directly.
188                          // Note that we're using the
189                          target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue());
190                      }
191                  } else {
192                      // Pointing to parent or center etc (non-id ref): replicate this on the target
193                      target.setAttribute(ANDROID_URI, name, attribute.getValue());
194                  }
195              }
196          }
197      }
198  
199      /**
200       * Determines if two constraints are in the same direction and if so returns
201       * the constraint in the same direction. Rather than returning boolean true
202       * or false, this returns the constraint which is sometimes modified. For
203       * example, if you have a node which points left to a node which is centered
204       * in parent, then the constraint is turned into center horizontal.
205       */
206      @Nullable
getCompatibleConstraint( @onNull ConstraintType first, @NonNull ConstraintType second)207      private static ConstraintType getCompatibleConstraint(
208              @NonNull ConstraintType first, @NonNull ConstraintType second) {
209          if (first == second) {
210              return first;
211          }
212  
213          switch (second) {
214              case ALIGN_LEFT:
215              case LAYOUT_RIGHT_OF:
216                  switch (first) {
217                      case LAYOUT_CENTER_HORIZONTAL:
218                      case LAYOUT_LEFT_OF:
219                      case ALIGN_LEFT:
220                          return first;
221                      case LAYOUT_CENTER_IN_PARENT:
222                          return LAYOUT_CENTER_HORIZONTAL;
223                  }
224                  return null;
225  
226              case ALIGN_RIGHT:
227              case LAYOUT_LEFT_OF:
228                  switch (first) {
229                      case LAYOUT_CENTER_HORIZONTAL:
230                      case ALIGN_RIGHT:
231                      case LAYOUT_LEFT_OF:
232                          return first;
233                      case LAYOUT_CENTER_IN_PARENT:
234                          return LAYOUT_CENTER_HORIZONTAL;
235                  }
236                  return null;
237  
238              case ALIGN_TOP:
239              case LAYOUT_BELOW:
240              case ALIGN_BASELINE:
241                  switch (first) {
242                      case LAYOUT_CENTER_VERTICAL:
243                      case ALIGN_TOP:
244                      case LAYOUT_BELOW:
245                      case ALIGN_BASELINE:
246                          return first;
247                      case LAYOUT_CENTER_IN_PARENT:
248                          return LAYOUT_CENTER_VERTICAL;
249                  }
250                  return null;
251              case ALIGN_BOTTOM:
252              case LAYOUT_ABOVE:
253                  switch (first) {
254                      case LAYOUT_CENTER_VERTICAL:
255                      case ALIGN_BOTTOM:
256                      case LAYOUT_ABOVE:
257                      case ALIGN_BASELINE:
258                          return first;
259                      case LAYOUT_CENTER_IN_PARENT:
260                          return LAYOUT_CENTER_VERTICAL;
261                  }
262                  return null;
263          }
264  
265          return null;
266      }
267  }
268