1 package com.airbnb.lottie.model; 2 3 import androidx.annotation.CheckResult; 4 import androidx.annotation.Nullable; 5 import androidx.annotation.RestrictTo; 6 7 import java.util.ArrayList; 8 import java.util.Arrays; 9 import java.util.List; 10 11 /** 12 * Defines which content to target. 13 * The keypath can contain wildcards ('*') with match exactly 1 item. 14 * or globstars ('**') which match 0 or more items. or KeyPath.COMPOSITION 15 * to represent the root composition layer. 16 * <p> 17 * For example, if your content were arranged like this: 18 * Gabriel (Shape Layer) 19 * Body (Shape Group) 20 * Left Hand (Shape) 21 * Fill (Fill) 22 * Transform (Transform) 23 * ... 24 * Brandon (Shape Layer) 25 * Body (Shape Group) 26 * Left Hand (Shape) 27 * Fill (Fill) 28 * Transform (Transform) 29 * ... 30 * <p> 31 * <p> 32 * You could: 33 * Match Gabriel left hand fill: 34 * new KeyPath("Gabriel", "Body", "Left Hand", "Fill"); 35 * Match Gabriel and Brandon's left hand fill: 36 * new KeyPath("*", "Body", Left Hand", "Fill"); 37 * Match anything with the name Fill: 38 * new KeyPath("**", "Fill"); 39 * Target the the root composition layer: 40 * KeyPath.COMPOSITION 41 * <p> 42 * <p> 43 * NOTE: Content that are part of merge paths or repeaters cannot currently be resolved with 44 * a {@link KeyPath}. This may be fixed in the future. 45 */ 46 public class KeyPath { 47 /** 48 * A singleton KeyPath that targets on the root composition layer. 49 * This is useful if you want to apply transformer to the animation as a whole. 50 */ 51 public final static KeyPath COMPOSITION = new KeyPath("COMPOSITION"); 52 53 private final List<String> keys; 54 @Nullable private KeyPathElement resolvedElement; 55 KeyPath(String... keys)56 public KeyPath(String... keys) { 57 this.keys = Arrays.asList(keys); 58 } 59 60 /** 61 * Copy constructor. Copies keys as well. 62 */ KeyPath(KeyPath keyPath)63 private KeyPath(KeyPath keyPath) { 64 keys = new ArrayList<>(keyPath.keys); 65 resolvedElement = keyPath.resolvedElement; 66 } 67 68 /** 69 * Returns a new KeyPath with the key added. 70 * This is used during keypath resolution. Children normally don't know about all of their parent 71 * elements so this is used to keep track of the fully qualified keypath. 72 * This returns a key keypath because during resolution, the full keypath element tree is walked 73 * and if this modified the original copy, it would remain after popping back up the element tree. 74 */ 75 @CheckResult 76 @RestrictTo(RestrictTo.Scope.LIBRARY) addKey(String key)77 public KeyPath addKey(String key) { 78 KeyPath newKeyPath = new KeyPath(this); 79 newKeyPath.keys.add(key); 80 return newKeyPath; 81 } 82 83 /** 84 * Return a new KeyPath with the element resolved to the specified {@link KeyPathElement}. 85 */ 86 @RestrictTo(RestrictTo.Scope.LIBRARY) resolve(KeyPathElement element)87 public KeyPath resolve(KeyPathElement element) { 88 KeyPath keyPath = new KeyPath(this); 89 keyPath.resolvedElement = element; 90 return keyPath; 91 } 92 93 /** 94 * Returns a {@link KeyPathElement} that this has been resolved to. KeyPaths get resolved with 95 * resolveKeyPath on LottieDrawable or LottieAnimationView. 96 */ 97 @RestrictTo(RestrictTo.Scope.LIBRARY) 98 @Nullable getResolvedElement()99 public KeyPathElement getResolvedElement() { 100 return resolvedElement; 101 } 102 103 /** 104 * Returns whether they key matches at the specified depth. 105 */ 106 @SuppressWarnings("RedundantIfStatement") 107 @RestrictTo(RestrictTo.Scope.LIBRARY) matches(String key, int depth)108 public boolean matches(String key, int depth) { 109 if (isContainer(key)) { 110 // This is an artificial layer we programatically create. 111 return true; 112 } 113 if (depth >= keys.size()) { 114 return false; 115 } 116 if (keys.get(depth).equals(key) || 117 keys.get(depth).equals("**") || 118 keys.get(depth).equals("*")) { 119 return true; 120 } 121 return false; 122 } 123 124 /** 125 * For a given key and depth, returns how much the depth should be incremented by when 126 * resolving a keypath to children. 127 * <p> 128 * This can be 0 or 2 when there is a globstar and the next key either matches or doesn't match 129 * the current key. 130 */ 131 @RestrictTo(RestrictTo.Scope.LIBRARY) incrementDepthBy(String key, int depth)132 public int incrementDepthBy(String key, int depth) { 133 if (isContainer(key)) { 134 // If it's a container then we added programatically and it isn't a part of the keypath. 135 return 0; 136 } 137 if (!keys.get(depth).equals("**")) { 138 // If it's not a globstar then it is part of the keypath. 139 return 1; 140 } 141 if (depth == keys.size() - 1) { 142 // The last key is a globstar. 143 return 0; 144 } 145 if (keys.get(depth + 1).equals(key)) { 146 // We are a globstar and the next key is our current key so consume both. 147 return 2; 148 } 149 return 0; 150 } 151 152 /** 153 * Returns whether the key at specified depth is fully specific enough to match the full set of 154 * keys in this keypath. 155 */ 156 @RestrictTo(RestrictTo.Scope.LIBRARY) fullyResolvesTo(String key, int depth)157 public boolean fullyResolvesTo(String key, int depth) { 158 if (depth >= keys.size()) { 159 return false; 160 } 161 boolean isLastDepth = depth == keys.size() - 1; 162 String keyAtDepth = keys.get(depth); 163 boolean isGlobstar = keyAtDepth.equals("**"); 164 165 if (!isGlobstar) { 166 boolean matches = keyAtDepth.equals(key) || keyAtDepth.equals("*"); 167 return (isLastDepth || (depth == keys.size() - 2 && endsWithGlobstar())) && matches; 168 } 169 170 boolean isGlobstarButNextKeyMatches = !isLastDepth && keys.get(depth + 1).equals(key); 171 if (isGlobstarButNextKeyMatches) { 172 return depth == keys.size() - 2 || 173 (depth == keys.size() - 3 && endsWithGlobstar()); 174 } 175 176 if (isLastDepth) { 177 return true; 178 } 179 if (depth + 1 < keys.size() - 1) { 180 // We are a globstar but there is more than 1 key after the globstar we we can't fully match. 181 return false; 182 } 183 // Return whether the next key (which we now know is the last one) is the same as the current 184 // key. 185 return keys.get(depth + 1).equals(key); 186 } 187 188 /** 189 * Returns whether the keypath resolution should propagate to children. Some keypaths resolve 190 * to content other than leaf contents (such as a layer or content group transform) so sometimes 191 * this will return false. 192 */ 193 @SuppressWarnings("SimplifiableIfStatement") 194 @RestrictTo(RestrictTo.Scope.LIBRARY) propagateToChildren(String key, int depth)195 public boolean propagateToChildren(String key, int depth) { 196 if ("__container".equals(key)) { 197 return true; 198 } 199 return depth < keys.size() - 1 || keys.get(depth).equals("**"); 200 } 201 202 /** 203 * We artificially create some container groups (like a root ContentGroup for the entire animation 204 * and for the contents of a ShapeLayer). 205 */ isContainer(String key)206 private boolean isContainer(String key) { 207 return "__container".equals(key); 208 } 209 endsWithGlobstar()210 private boolean endsWithGlobstar() { 211 return keys.get(keys.size() - 1).equals("**"); 212 } 213 keysToString()214 public String keysToString() { 215 return keys.toString(); 216 } 217 equals(Object o)218 @Override public boolean equals(Object o) { 219 if (this == o) { 220 return true; 221 } 222 if (o == null || getClass() != o.getClass()) { 223 return false; 224 } 225 226 KeyPath keyPath = (KeyPath) o; 227 228 if (!keys.equals(keyPath.keys)) { 229 return false; 230 } 231 return resolvedElement != null ? resolvedElement.equals(keyPath.resolvedElement) : keyPath.resolvedElement == null; 232 } 233 hashCode()234 @Override public int hashCode() { 235 int result = keys.hashCode(); 236 result = 31 * result + (resolvedElement != null ? resolvedElement.hashCode() : 0); 237 return result; 238 } 239 toString()240 @Override public String toString() { 241 return "KeyPath{" + "keys=" + keys + ",resolved=" + (resolvedElement != null) + '}'; 242 } 243 } 244