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