/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.common.layout.relative; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL; import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.INode; import com.android.ide.common.api.INode.IAttribute; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.util.List; import java.util.Map; import java.util.Set; /** * Handles deletions in a relative layout, transferring constraints across * deleted nodes * <p> * TODO: Consider adding the * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a * node if it's pointing to a node which is deleted and which has no transitive * reference to another node. */ public class DeletionHandler { private final INode mLayout; private final INode[] mChildren; private final List<INode> mDeleted; private final Set<String> mDeletedIds; private final Map<String, INode> mNodeMap; private final List<INode> mMoved; /** * Creates a new {@link DeletionHandler} * * @param deleted the deleted nodes * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere) * @param layout the parent layout of the deleted nodes */ public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved, @NonNull INode layout) { mDeleted = deleted; mMoved = moved; mLayout = layout; mChildren = mLayout.getChildren(); mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length); for (INode child : mChildren) { String id = child.getStringAttr(ANDROID_URI, ATTR_ID); if (id != null) { mNodeMap.put(stripIdPrefix(id), child); } } mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size()); for (INode node : mDeleted) { String id = node.getStringAttr(ANDROID_URI, ATTR_ID); if (id != null) { mDeletedIds.add(stripIdPrefix(id)); } } // Any widgets that remain (e.g. typically because they were moved) should // keep their incoming dependencies for (INode node : mMoved) { String id = node.getStringAttr(ANDROID_URI, ATTR_ID); if (id != null) { mDeletedIds.remove(stripIdPrefix(id)); } } } @Nullable private static String getId(@NonNull IAttribute attribute) { if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && ANDROID_URI.equals(attribute.getUri()) && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) { String id = attribute.getValue(); // It might not be an id reference, so check manually rather than just // calling stripIdPrefix(): if (id.startsWith(NEW_ID_PREFIX)) { return id.substring(NEW_ID_PREFIX.length()); } else if (id.startsWith(ID_PREFIX)) { return id.substring(ID_PREFIX.length()); } } return null; } /** * Updates the constraints in the layout to handle deletion of a set of * nodes. This ensures that any constraints pointing to one of the deleted * nodes are changed properly to point to a non-deleted node with similar * constraints. */ public void updateConstraints() { if (mChildren.length == mDeleted.size()) { // Deleting everything: Nothing to be done return; } // Now remove incoming edges to any views that were deleted. If possible, // don't just delete them but replace them with a transitive constraint, e.g. // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C", for (INode child : mChildren) { if (mDeleted.contains(child)) { continue; } for (IAttribute attribute : child.getLiveAttributes()) { String id = getId(attribute); if (id != null) { if (mDeletedIds.contains(id)) { // Unset this reference to a deleted widget. It might be // replaced if the pointed to node points to some other node // on the same side, but it may use a different constraint name, // or have none at all (e.g. parent). String name = attribute.getName(); child.setAttribute(ANDROID_URI, name, null); INode deleted = mNodeMap.get(id); if (deleted != null) { ConstraintType type = ConstraintType.fromAttribute(name); if (type != null) { transfer(deleted, child, type, 0); } } } } } } } private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) { if (depth == 20) { // Prevent really deep flow or unbounded recursion in case there is a bug in // the cycle detection code return; } assert mDeleted.contains(deleted); for (IAttribute attribute : deleted.getLiveAttributes()) { String name = attribute.getName(); ConstraintType type = ConstraintType.fromAttribute(name); if (type == null) { continue; } ConstraintType transfer = getCompatibleConstraint(type, targetType); if (transfer != null) { String id = getId(attribute); if (id != null) { if (mDeletedIds.contains(id)) { INode nextDeleted = mNodeMap.get(id); if (nextDeleted != null) { // Points to another deleted node: recurse transfer(nextDeleted, target, targetType, depth + 1); } } else { // Found an undeleted node destination: point to it directly. // Note that we're using the target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue()); } } else { // Pointing to parent or center etc (non-id ref): replicate this on the target target.setAttribute(ANDROID_URI, name, attribute.getValue()); } } } } /** * Determines if two constraints are in the same direction and if so returns * the constraint in the same direction. Rather than returning boolean true * or false, this returns the constraint which is sometimes modified. For * example, if you have a node which points left to a node which is centered * in parent, then the constraint is turned into center horizontal. */ @Nullable private static ConstraintType getCompatibleConstraint( @NonNull ConstraintType first, @NonNull ConstraintType second) { if (first == second) { return first; } switch (second) { case ALIGN_LEFT: case LAYOUT_RIGHT_OF: switch (first) { case LAYOUT_CENTER_HORIZONTAL: case LAYOUT_LEFT_OF: case ALIGN_LEFT: return first; case LAYOUT_CENTER_IN_PARENT: return LAYOUT_CENTER_HORIZONTAL; } return null; case ALIGN_RIGHT: case LAYOUT_LEFT_OF: switch (first) { case LAYOUT_CENTER_HORIZONTAL: case ALIGN_RIGHT: case LAYOUT_LEFT_OF: return first; case LAYOUT_CENTER_IN_PARENT: return LAYOUT_CENTER_HORIZONTAL; } return null; case ALIGN_TOP: case LAYOUT_BELOW: case ALIGN_BASELINE: switch (first) { case LAYOUT_CENTER_VERTICAL: case ALIGN_TOP: case LAYOUT_BELOW: case ALIGN_BASELINE: return first; case LAYOUT_CENTER_IN_PARENT: return LAYOUT_CENTER_VERTICAL; } return null; case ALIGN_BOTTOM: case LAYOUT_ABOVE: switch (first) { case LAYOUT_CENTER_VERTICAL: case ALIGN_BOTTOM: case LAYOUT_ABOVE: case ALIGN_BASELINE: return first; case LAYOUT_CENTER_IN_PARENT: return LAYOUT_CENTER_VERTICAL; } return null; } return null; } }