| package com.airbnb.lottie.model; |
| |
| import androidx.annotation.CheckResult; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Defines which content to target. |
| * The keypath can contain wildcards ('*') with match exactly 1 item. |
| * or globstars ('**') which match 0 or more items. or KeyPath.COMPOSITION |
| * to represent the root composition layer. |
| * <p> |
| * For example, if your content were arranged like this: |
| * Gabriel (Shape Layer) |
| * Body (Shape Group) |
| * Left Hand (Shape) |
| * Fill (Fill) |
| * Transform (Transform) |
| * ... |
| * Brandon (Shape Layer) |
| * Body (Shape Group) |
| * Left Hand (Shape) |
| * Fill (Fill) |
| * Transform (Transform) |
| * ... |
| * <p> |
| * <p> |
| * You could: |
| * Match Gabriel left hand fill: |
| * new KeyPath("Gabriel", "Body", "Left Hand", "Fill"); |
| * Match Gabriel and Brandon's left hand fill: |
| * new KeyPath("*", "Body", Left Hand", "Fill"); |
| * Match anything with the name Fill: |
| * new KeyPath("**", "Fill"); |
| * Target the the root composition layer: |
| * KeyPath.COMPOSITION |
| * <p> |
| * <p> |
| * NOTE: Content that are part of merge paths or repeaters cannot currently be resolved with |
| * a {@link KeyPath}. This may be fixed in the future. |
| */ |
| public class KeyPath { |
| /** |
| * A singleton KeyPath that targets on the root composition layer. |
| * This is useful if you want to apply transformer to the animation as a whole. |
| */ |
| public final static KeyPath COMPOSITION = new KeyPath("COMPOSITION"); |
| |
| private final List<String> keys; |
| @Nullable private KeyPathElement resolvedElement; |
| |
| public KeyPath(String... keys) { |
| this.keys = Arrays.asList(keys); |
| } |
| |
| /** |
| * Copy constructor. Copies keys as well. |
| */ |
| private KeyPath(KeyPath keyPath) { |
| keys = new ArrayList<>(keyPath.keys); |
| resolvedElement = keyPath.resolvedElement; |
| } |
| |
| /** |
| * Returns a new KeyPath with the key added. |
| * This is used during keypath resolution. Children normally don't know about all of their parent |
| * elements so this is used to keep track of the fully qualified keypath. |
| * This returns a key keypath because during resolution, the full keypath element tree is walked |
| * and if this modified the original copy, it would remain after popping back up the element tree. |
| */ |
| @CheckResult |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public KeyPath addKey(String key) { |
| KeyPath newKeyPath = new KeyPath(this); |
| newKeyPath.keys.add(key); |
| return newKeyPath; |
| } |
| |
| /** |
| * Return a new KeyPath with the element resolved to the specified {@link KeyPathElement}. |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public KeyPath resolve(KeyPathElement element) { |
| KeyPath keyPath = new KeyPath(this); |
| keyPath.resolvedElement = element; |
| return keyPath; |
| } |
| |
| /** |
| * Returns a {@link KeyPathElement} that this has been resolved to. KeyPaths get resolved with |
| * resolveKeyPath on LottieDrawable or LottieAnimationView. |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| @Nullable |
| public KeyPathElement getResolvedElement() { |
| return resolvedElement; |
| } |
| |
| /** |
| * Returns whether they key matches at the specified depth. |
| */ |
| @SuppressWarnings("RedundantIfStatement") |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public boolean matches(String key, int depth) { |
| if (isContainer(key)) { |
| // This is an artificial layer we programatically create. |
| return true; |
| } |
| if (depth >= keys.size()) { |
| return false; |
| } |
| if (keys.get(depth).equals(key) || |
| keys.get(depth).equals("**") || |
| keys.get(depth).equals("*")) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * For a given key and depth, returns how much the depth should be incremented by when |
| * resolving a keypath to children. |
| * <p> |
| * This can be 0 or 2 when there is a globstar and the next key either matches or doesn't match |
| * the current key. |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public int incrementDepthBy(String key, int depth) { |
| if (isContainer(key)) { |
| // If it's a container then we added programatically and it isn't a part of the keypath. |
| return 0; |
| } |
| if (!keys.get(depth).equals("**")) { |
| // If it's not a globstar then it is part of the keypath. |
| return 1; |
| } |
| if (depth == keys.size() - 1) { |
| // The last key is a globstar. |
| return 0; |
| } |
| if (keys.get(depth + 1).equals(key)) { |
| // We are a globstar and the next key is our current key so consume both. |
| return 2; |
| } |
| return 0; |
| } |
| |
| /** |
| * Returns whether the key at specified depth is fully specific enough to match the full set of |
| * keys in this keypath. |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public boolean fullyResolvesTo(String key, int depth) { |
| if (depth >= keys.size()) { |
| return false; |
| } |
| boolean isLastDepth = depth == keys.size() - 1; |
| String keyAtDepth = keys.get(depth); |
| boolean isGlobstar = keyAtDepth.equals("**"); |
| |
| if (!isGlobstar) { |
| boolean matches = keyAtDepth.equals(key) || keyAtDepth.equals("*"); |
| return (isLastDepth || (depth == keys.size() - 2 && endsWithGlobstar())) && matches; |
| } |
| |
| boolean isGlobstarButNextKeyMatches = !isLastDepth && keys.get(depth + 1).equals(key); |
| if (isGlobstarButNextKeyMatches) { |
| return depth == keys.size() - 2 || |
| (depth == keys.size() - 3 && endsWithGlobstar()); |
| } |
| |
| if (isLastDepth) { |
| return true; |
| } |
| if (depth + 1 < keys.size() - 1) { |
| // We are a globstar but there is more than 1 key after the globstar we we can't fully match. |
| return false; |
| } |
| // Return whether the next key (which we now know is the last one) is the same as the current |
| // key. |
| return keys.get(depth + 1).equals(key); |
| } |
| |
| /** |
| * Returns whether the keypath resolution should propagate to children. Some keypaths resolve |
| * to content other than leaf contents (such as a layer or content group transform) so sometimes |
| * this will return false. |
| */ |
| @SuppressWarnings("SimplifiableIfStatement") |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public boolean propagateToChildren(String key, int depth) { |
| if ("__container".equals(key)) { |
| return true; |
| } |
| return depth < keys.size() - 1 || keys.get(depth).equals("**"); |
| } |
| |
| /** |
| * We artificially create some container groups (like a root ContentGroup for the entire animation |
| * and for the contents of a ShapeLayer). |
| */ |
| private boolean isContainer(String key) { |
| return "__container".equals(key); |
| } |
| |
| private boolean endsWithGlobstar() { |
| return keys.get(keys.size() - 1).equals("**"); |
| } |
| |
| public String keysToString() { |
| return keys.toString(); |
| } |
| |
| @Override public String toString() { |
| return "KeyPath{" + "keys=" + keys + ",resolved=" + (resolvedElement != null) + '}'; |
| } |
| } |