Home | History | Annotate | Download | only in relative
      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      */
     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
     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      */
    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 
    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
    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