• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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