blob: de5065266ba0184f1769c4899ddd51a750a6e20d [file] [log] [blame]
package com.airbnb.lottie.model;
import android.support.annotation.CheckResult;
import android.support.annotation.Nullable;;
import android.support.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.
*
* 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)
* ...
*
*
* 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");
*
*
* 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 {
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.
*
* 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) + '}';
}
}