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