• 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.
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