Add vector support
This commit is contained in:
1 parent
d137151e7a
commit
f4389afb30
16 files changed
+4317
-4
No files matched your search
@@ -0,0 +1,611 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.NotFoundException;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.util.Xml;
|
||||
import android.view.InflateException;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.util.PathParser;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class is used to instantiate animator XML files into Animator objects.
|
||||
* <p>
|
||||
* For performance reasons, inflation relies heavily on pre-processing of
|
||||
* XML files that is done at build time. Therefore, it is not currently possible
|
||||
* to use this inflater with an XmlPullParser over a plain XML file at runtime;
|
||||
* it only works with an XmlPullParser returned from a compiled resource (R.
|
||||
* <em>something</em> file.)
|
||||
*/
|
||||
public class AnimatorInflater {
|
||||
private static final String TAG = "AnimatorInflater";
|
||||
/**
|
||||
* These flags are used when parsing AnimatorSet objects
|
||||
*/
|
||||
private static final int TOGETHER = 0;
|
||||
private static final int SEQUENTIALLY = 1;
|
||||
|
||||
/**
|
||||
* Enum values used in XML attributes to indicate the value for mValueType
|
||||
*/
|
||||
private static final int VALUE_TYPE_FLOAT = 0;
|
||||
private static final int VALUE_TYPE_INT = 1;
|
||||
private static final int VALUE_TYPE_PATH = 2;
|
||||
private static final int VALUE_TYPE_COLOR = 4;
|
||||
private static final int VALUE_TYPE_CUSTOM = 5;
|
||||
|
||||
private static final boolean DBG_ANIMATOR_INFLATER = false;
|
||||
|
||||
private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
|
||||
|
||||
/**
|
||||
* Loads an {@link Animator} object from a resource
|
||||
*
|
||||
* @param context Application context used to access resources
|
||||
* @param id The resource id of the animation to load
|
||||
* @return The animator object reference by the specified id
|
||||
* @throws NotFoundException when the animation cannot be loaded
|
||||
*/
|
||||
public static Animator loadAnimator(Context context, int id)
|
||||
throws NotFoundException {
|
||||
return loadAnimator(context, id, 1);
|
||||
}
|
||||
|
||||
public static Animator loadAnimator(Context context, int id, float pathErrorScale) {
|
||||
Resources resources = context.getResources();
|
||||
XmlResourceParser parser = null;
|
||||
try {
|
||||
parser = resources.getAnimation(id);
|
||||
return createAnimatorFromXml(context, parser, pathErrorScale);
|
||||
} catch (XmlPullParserException ex) {
|
||||
NotFoundException rnf =
|
||||
new NotFoundException("Can't load animation resource ID #0x" +
|
||||
Integer.toHexString(id));
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} catch (IOException ex) {
|
||||
NotFoundException rnf =
|
||||
new NotFoundException("Can't load animation resource ID #0x" +
|
||||
Integer.toHexString(id));
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} finally {
|
||||
if (parser != null) parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO Not support StateListAnimator
|
||||
public static StateListAnimator loadStateListAnimator(Context context, int id)
|
||||
throws NotFoundException {
|
||||
XmlResourceParser parser = null;
|
||||
try {
|
||||
parser = context.getResources().getAnimation(id);
|
||||
return createStateListAnimatorFromXml(context, parser, Xml.asAttributeSet(parser));
|
||||
} catch (XmlPullParserException ex) {
|
||||
NotFoundException rnf =
|
||||
new NotFoundException(
|
||||
"Can't load state list animator resource ID #0x" +
|
||||
Integer.toHexString(id)
|
||||
);
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} catch (IOException ex) {
|
||||
NotFoundException rnf =
|
||||
new NotFoundException(
|
||||
"Can't load state list animator resource ID #0x" +
|
||||
Integer.toHexString(id)
|
||||
);
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} finally {
|
||||
if (parser != null) {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/* TODO Not support StateListAnimator
|
||||
private static StateListAnimator createStateListAnimatorFromXml(Context context,
|
||||
XmlPullParser parser, AttributeSet attributeSet)
|
||||
throws IOException, XmlPullParserException {
|
||||
int type;
|
||||
StateListAnimator stateListAnimator = new StateListAnimator();
|
||||
|
||||
while (true) {
|
||||
type = parser.next();
|
||||
switch (type) {
|
||||
case XmlPullParser.END_DOCUMENT:
|
||||
case XmlPullParser.END_TAG:
|
||||
return stateListAnimator;
|
||||
|
||||
case XmlPullParser.START_TAG:
|
||||
// parse item
|
||||
Animator animator = null;
|
||||
if ("item".equals(parser.getName())) {
|
||||
int attributeCount = parser.getAttributeCount();
|
||||
int[] states = new int[attributeCount];
|
||||
int stateIndex = 0;
|
||||
for (int i = 0; i < attributeCount; i++) {
|
||||
int attrName = attributeSet.getAttributeNameResource(i);
|
||||
if (attrName == R.attr.animation) {
|
||||
animator = loadAnimator(context,
|
||||
attributeSet.getAttributeResourceValue(i, 0));
|
||||
} else {
|
||||
states[stateIndex++] =
|
||||
attributeSet.getAttributeBooleanValue(i, false) ?
|
||||
attrName : -attrName;
|
||||
}
|
||||
|
||||
}
|
||||
if (animator == null) {
|
||||
animator = createAnimatorFromXml(context, parser, 1f);
|
||||
}
|
||||
|
||||
if (animator == null) {
|
||||
throw new NotFoundException(
|
||||
"animation state item must have a valid animation");
|
||||
}
|
||||
stateListAnimator
|
||||
.addState(StateSet.trimStateSet(states, stateIndex), animator);
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* PathDataEvaluator is used to interpolate between two paths which are
|
||||
* represented in the same format but different control points' values.
|
||||
* The path is represented as an array of PathDataNode here, which is
|
||||
* fundamentally an array of floating point numbers.
|
||||
*/
|
||||
private static class PathDataEvaluator implements TypeEvaluator<PathParser.PathDataNode[]> {
|
||||
private PathParser.PathDataNode[] mNodeArray;
|
||||
|
||||
/**
|
||||
* Create a PathParser.PathDataNode[] that does not reuse the animated value.
|
||||
* Care must be taken when using this option because on every evaluation
|
||||
* a new <code>PathParser.PathDataNode[]</code> will be allocated.
|
||||
*/
|
||||
private PathDataEvaluator() {}
|
||||
|
||||
/**
|
||||
* Create a PathDataEvaluator that reuses <code>nodeArray</code> for every evaluate() call.
|
||||
* Caution must be taken to ensure that the value returned from
|
||||
* {@link ValueAnimator#getAnimatedValue()} is not cached, modified, or
|
||||
* used across threads. The value will be modified on each <code>evaluate()</code> call.
|
||||
*
|
||||
* @param nodeArray The array to modify and return from <code>evaluate</code>.
|
||||
*/
|
||||
public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) {
|
||||
mNodeArray = nodeArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathParser.PathDataNode[] evaluate(float fraction,
|
||||
PathParser.PathDataNode[] startPathData,
|
||||
PathParser.PathDataNode[] endPathData) {
|
||||
if (!PathParser.canMorph(startPathData, endPathData)) {
|
||||
throw new IllegalArgumentException("Can't interpolate between"
|
||||
+ " two incompatible pathData");
|
||||
}
|
||||
|
||||
if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) {
|
||||
mNodeArray = PathParser.deepCopyNodes(startPathData);
|
||||
}
|
||||
|
||||
for (int i = 0; i < startPathData.length; i++) {
|
||||
mNodeArray[i].interpolatePathDataNode(startPathData[i],
|
||||
endPathData[i], fraction);
|
||||
}
|
||||
|
||||
return mNodeArray;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param anim The animator, must not be null
|
||||
* @param arrayAnimator Incoming typed array for Animator's attributes.
|
||||
* @param arrayObjectAnimator Incoming typed array for Object Animator's
|
||||
* attributes.
|
||||
* @param pixelSize The relative pixel size, used to calculate the
|
||||
* maximum error for path animations.
|
||||
*/
|
||||
private static void parseAnimatorFromTypeArray(ValueAnimator anim,
|
||||
TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
|
||||
long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300);
|
||||
|
||||
long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
|
||||
|
||||
int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType,
|
||||
VALUE_TYPE_FLOAT);
|
||||
|
||||
TypeEvaluator evaluator = null;
|
||||
|
||||
boolean getFloats = (valueType == VALUE_TYPE_FLOAT);
|
||||
|
||||
TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom);
|
||||
boolean hasFrom = (tvFrom != null);
|
||||
int fromType = hasFrom ? tvFrom.type : 0;
|
||||
TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo);
|
||||
boolean hasTo = (tvTo != null);
|
||||
int toType = hasTo ? tvTo.type : 0;
|
||||
|
||||
// TODO: Further clean up this part of code into 4 types : path, color,
|
||||
// integer and float.
|
||||
if (valueType == VALUE_TYPE_PATH) {
|
||||
evaluator = setupAnimatorForPath(anim, arrayAnimator);
|
||||
} else {
|
||||
// Integer and float value types are handled here.
|
||||
if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
|
||||
(fromType <= TypedValue.TYPE_LAST_COLOR_INT)) ||
|
||||
(hasTo && (toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
|
||||
(toType <= TypedValue.TYPE_LAST_COLOR_INT))) {
|
||||
// special case for colors: ignore valueType and get ints
|
||||
getFloats = false;
|
||||
evaluator = ARGB_EVALUATOR;
|
||||
}
|
||||
setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType);
|
||||
}
|
||||
|
||||
anim.setDuration(duration);
|
||||
anim.setStartDelay(startDelay);
|
||||
|
||||
if (arrayAnimator.hasValue(R.styleable.Animator_repeatCount)) {
|
||||
anim.setRepeatCount(
|
||||
arrayAnimator.getInt(R.styleable.Animator_repeatCount, 0));
|
||||
}
|
||||
if (arrayAnimator.hasValue(R.styleable.Animator_repeatMode)) {
|
||||
anim.setRepeatMode(
|
||||
arrayAnimator.getInt(R.styleable.Animator_repeatMode,
|
||||
ValueAnimator.RESTART));
|
||||
}
|
||||
if (evaluator != null) {
|
||||
anim.setEvaluator(evaluator);
|
||||
}
|
||||
|
||||
if (arrayObjectAnimator != null) {
|
||||
setupObjectAnimator(anim, arrayObjectAnimator, getFloats, pixelSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the Animator to achieve path morphing.
|
||||
*
|
||||
* @param anim The target Animator which will be updated.
|
||||
* @param arrayAnimator TypedArray for the ValueAnimator.
|
||||
* @return the PathDataEvaluator.
|
||||
*/
|
||||
private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim,
|
||||
TypedArray arrayAnimator) {
|
||||
TypeEvaluator evaluator = null;
|
||||
String fromString = arrayAnimator.getString(R.styleable.Animator_valueFrom);
|
||||
String toString = arrayAnimator.getString(R.styleable.Animator_valueTo);
|
||||
PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString);
|
||||
PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString);
|
||||
|
||||
if (nodesFrom != null) {
|
||||
if (nodesTo != null) {
|
||||
anim.setObjectValues(nodesFrom, nodesTo);
|
||||
if (!PathParser.canMorph(nodesFrom, nodesTo)) {
|
||||
throw new InflateException(arrayAnimator.getPositionDescription()
|
||||
+ " Can't morph from " + fromString + " to " + toString);
|
||||
}
|
||||
} else {
|
||||
anim.setObjectValues((Object)nodesFrom);
|
||||
}
|
||||
evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom));
|
||||
} else if (nodesTo != null) {
|
||||
anim.setObjectValues((Object)nodesTo);
|
||||
evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo));
|
||||
}
|
||||
|
||||
if (DBG_ANIMATOR_INFLATER && evaluator != null) {
|
||||
Log.v(TAG, "create a new PathDataEvaluator here");
|
||||
}
|
||||
|
||||
return evaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup ObjectAnimator's property or values from pathData.
|
||||
*
|
||||
* @param anim The target Animator which will be updated.
|
||||
* @param arrayObjectAnimator TypedArray for the ObjectAnimator.
|
||||
* @param getFloats True if the value type is float.
|
||||
* @param pixelSize The relative pixel size, used to calculate the
|
||||
* maximum error for path animations.
|
||||
*/
|
||||
private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
|
||||
boolean getFloats, float pixelSize) {
|
||||
ObjectAnimator oa = (ObjectAnimator) anim;
|
||||
String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
|
||||
|
||||
// Note that if there is a pathData defined in the Object Animator,
|
||||
// valueFrom / valueTo will be ignored.
|
||||
if (pathData != null) {
|
||||
throw new IllegalStateException("Not support PropertyAnimator_pathData");
|
||||
/* TODO Not support pathData
|
||||
String propertyXName =
|
||||
arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyXName);
|
||||
String propertyYName =
|
||||
arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyYName);
|
||||
|
||||
if (propertyXName == null && propertyYName == null) {
|
||||
throw new InflateException(arrayObjectAnimator.getPositionDescription()
|
||||
+ " propertyXName or propertyYName is needed for PathData");
|
||||
} else {
|
||||
Path path = PathParser.createPathFromPathData(pathData);
|
||||
float error = 0.5f * pixelSize; // max half a pixel error
|
||||
PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
|
||||
Keyframes xKeyframes;
|
||||
Keyframes yKeyframes;
|
||||
if (getFloats) {
|
||||
xKeyframes = keyframeSet.createXFloatKeyframes();
|
||||
yKeyframes = keyframeSet.createYFloatKeyframes();
|
||||
} else {
|
||||
xKeyframes = keyframeSet.createXIntKeyframes();
|
||||
yKeyframes = keyframeSet.createYIntKeyframes();
|
||||
}
|
||||
PropertyValuesHolder x = null;
|
||||
PropertyValuesHolder y = null;
|
||||
if (propertyXName != null) {
|
||||
x = PropertyValuesHolder.ofKeyframes(propertyXName, xKeyframes);
|
||||
}
|
||||
if (propertyYName != null) {
|
||||
y = PropertyValuesHolder.ofKeyframes(propertyYName, yKeyframes);
|
||||
}
|
||||
if (x == null) {
|
||||
oa.setValues(y);
|
||||
} else if (y == null) {
|
||||
oa.setValues(x);
|
||||
} else {
|
||||
oa.setValues(x, y);
|
||||
}
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
String propertyName =
|
||||
arrayObjectAnimator.getString(R.styleable.PropertyAnimator_propertyName);
|
||||
oa.setPropertyName(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup ValueAnimator's values.
|
||||
* This will handle all of the integer, float and color types.
|
||||
*
|
||||
* @param anim The target Animator which will be updated.
|
||||
* @param arrayAnimator TypedArray for the ValueAnimator.
|
||||
* @param getFloats True if the value type is float.
|
||||
* @param hasFrom True if "valueFrom" exists.
|
||||
* @param fromType The type of "valueFrom".
|
||||
* @param hasTo True if "valueTo" exists.
|
||||
* @param toType The type of "valueTo".
|
||||
*/
|
||||
private static void setupValues(ValueAnimator anim, TypedArray arrayAnimator,
|
||||
boolean getFloats, boolean hasFrom, int fromType, boolean hasTo, int toType) {
|
||||
int valueFromIndex = R.styleable.Animator_valueFrom;
|
||||
int valueToIndex = R.styleable.Animator_valueTo;
|
||||
if (getFloats) {
|
||||
float valueFrom;
|
||||
float valueTo;
|
||||
if (hasFrom) {
|
||||
if (fromType == TypedValue.TYPE_DIMENSION) {
|
||||
valueFrom = arrayAnimator.getDimension(valueFromIndex, 0f);
|
||||
} else {
|
||||
valueFrom = arrayAnimator.getFloat(valueFromIndex, 0f);
|
||||
}
|
||||
if (hasTo) {
|
||||
if (toType == TypedValue.TYPE_DIMENSION) {
|
||||
valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
|
||||
} else {
|
||||
valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
|
||||
}
|
||||
anim.setFloatValues(valueFrom, valueTo);
|
||||
} else {
|
||||
anim.setFloatValues(valueFrom);
|
||||
}
|
||||
} else {
|
||||
if (toType == TypedValue.TYPE_DIMENSION) {
|
||||
valueTo = arrayAnimator.getDimension(valueToIndex, 0f);
|
||||
} else {
|
||||
valueTo = arrayAnimator.getFloat(valueToIndex, 0f);
|
||||
}
|
||||
anim.setFloatValues(valueTo);
|
||||
}
|
||||
} else {
|
||||
int valueFrom;
|
||||
int valueTo;
|
||||
if (hasFrom) {
|
||||
if (fromType == TypedValue.TYPE_DIMENSION) {
|
||||
valueFrom = (int) arrayAnimator.getDimension(valueFromIndex, 0f);
|
||||
} else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
|
||||
(fromType <= TypedValue.TYPE_LAST_COLOR_INT)) {
|
||||
valueFrom = arrayAnimator.getColor(valueFromIndex, 0);
|
||||
} else {
|
||||
valueFrom = arrayAnimator.getInt(valueFromIndex, 0);
|
||||
}
|
||||
if (hasTo) {
|
||||
if (toType == TypedValue.TYPE_DIMENSION) {
|
||||
valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
|
||||
} else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
|
||||
(toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
|
||||
valueTo = arrayAnimator.getColor(valueToIndex, 0);
|
||||
} else {
|
||||
valueTo = arrayAnimator.getInt(valueToIndex, 0);
|
||||
}
|
||||
anim.setIntValues(valueFrom, valueTo);
|
||||
} else {
|
||||
anim.setIntValues(valueFrom);
|
||||
}
|
||||
} else {
|
||||
if (hasTo) {
|
||||
if (toType == TypedValue.TYPE_DIMENSION) {
|
||||
valueTo = (int) arrayAnimator.getDimension(valueToIndex, 0f);
|
||||
} else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) &&
|
||||
(toType <= TypedValue.TYPE_LAST_COLOR_INT)) {
|
||||
valueTo = arrayAnimator.getColor(valueToIndex, 0);
|
||||
} else {
|
||||
valueTo = arrayAnimator.getInt(valueToIndex, 0);
|
||||
}
|
||||
anim.setIntValues(valueTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Animator createAnimatorFromXml(Context context, XmlPullParser parser,
|
||||
float pixelSize)
|
||||
throws XmlPullParserException, IOException {
|
||||
return createAnimatorFromXml(context, parser, Xml.asAttributeSet(parser), null, 0,
|
||||
pixelSize);
|
||||
}
|
||||
|
||||
private static Animator createAnimatorFromXml(Context context, XmlPullParser parser,
|
||||
AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
|
||||
throws XmlPullParserException, IOException {
|
||||
|
||||
Animator anim = null;
|
||||
ArrayList<Animator> childAnims = null;
|
||||
|
||||
// Make sure we are on a start tag.
|
||||
int type;
|
||||
int depth = parser.getDepth();
|
||||
|
||||
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
||||
&& type != XmlPullParser.END_DOCUMENT) {
|
||||
|
||||
if (type != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = parser.getName();
|
||||
|
||||
if (name.equals("objectAnimator")) {
|
||||
anim = loadObjectAnimator(context, attrs, pixelSize);
|
||||
} else if (name.equals("animator")) {
|
||||
anim = loadAnimator(context, attrs, null, pixelSize);
|
||||
} else if (name.equals("set")) {
|
||||
anim = new AnimatorSet();
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatorSet);
|
||||
int ordering = a.getInt(R.styleable.AnimatorSet_ordering,
|
||||
TOGETHER);
|
||||
createAnimatorFromXml(context, parser, attrs, (AnimatorSet) anim, ordering,
|
||||
pixelSize);
|
||||
a.recycle();
|
||||
} else {
|
||||
throw new RuntimeException("Unknown animator name: " + parser.getName());
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
if (childAnims == null) {
|
||||
childAnims = new ArrayList<>();
|
||||
}
|
||||
childAnims.add(anim);
|
||||
}
|
||||
}
|
||||
if (parent != null && childAnims != null) {
|
||||
Animator[] animsArray = new Animator[childAnims.size()];
|
||||
int index = 0;
|
||||
for (Animator a : childAnims) {
|
||||
animsArray[index++] = a;
|
||||
}
|
||||
if (sequenceOrdering == TOGETHER) {
|
||||
parent.playTogether(animsArray);
|
||||
} else {
|
||||
parent.playSequentially(animsArray);
|
||||
}
|
||||
}
|
||||
|
||||
return anim;
|
||||
|
||||
}
|
||||
|
||||
private static ObjectAnimator loadObjectAnimator(Context context, AttributeSet attrs,
|
||||
float pathErrorScale) throws NotFoundException {
|
||||
ObjectAnimator anim = new ObjectAnimator();
|
||||
|
||||
loadAnimator(context, attrs, anim, pathErrorScale);
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new animation whose parameters come from the specified context
|
||||
* and attributes set.
|
||||
*
|
||||
* @param context the context
|
||||
* @param attrs The set of attributes holding the animation parameters
|
||||
* @param anim Null if this is a ValueAnimator, otherwise this is an
|
||||
* ObjectAnimator
|
||||
*/
|
||||
private static ValueAnimator loadAnimator(Context context, AttributeSet attrs,
|
||||
ValueAnimator anim, float pathErrorScale)
|
||||
throws NotFoundException {
|
||||
|
||||
TypedArray arrayAnimator = context.obtainStyledAttributes(attrs, R.styleable.Animator);
|
||||
TypedArray arrayObjectAnimator = null;
|
||||
|
||||
// If anim is not null, then it is an object animator.
|
||||
if (anim != null) {
|
||||
arrayObjectAnimator = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.PropertyAnimator);
|
||||
}
|
||||
|
||||
if (anim == null) {
|
||||
anim = new ValueAnimator();
|
||||
}
|
||||
|
||||
parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
|
||||
|
||||
final int resID =
|
||||
arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
|
||||
if (resID > 0) {
|
||||
anim.setInterpolator(InterpolatorInflater.loadInterpolator(context, resID));
|
||||
}
|
||||
|
||||
arrayAnimator.recycle();
|
||||
if (arrayObjectAnimator != null) {
|
||||
arrayObjectAnimator.recycle();
|
||||
}
|
||||
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.animation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Xml;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.AnticipateOvershootInterpolator;
|
||||
import android.view.animation.BounceInterpolator;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.PathInterpolator;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class InterpolatorInflater {
|
||||
|
||||
/**
|
||||
* Loads an {@link Interpolator} object from a resource
|
||||
*
|
||||
* @param context The Context
|
||||
* @param id The resource id of the animation to load
|
||||
* @return The interpolator object reference by the specified id
|
||||
* @throws Resources.NotFoundException
|
||||
*/
|
||||
public static Interpolator loadInterpolator(Context context, int id) throws Resources.NotFoundException {
|
||||
XmlResourceParser parser = null;
|
||||
try {
|
||||
parser = context.getResources().getAnimation(id);
|
||||
return createInterpolatorFromXml(context, parser);
|
||||
} catch (XmlPullParserException ex) {
|
||||
Resources.NotFoundException rnf = new Resources.NotFoundException("Can't load animation resource ID #0x" +
|
||||
Integer.toHexString(id));
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} catch (IOException ex) {
|
||||
Resources.NotFoundException rnf = new Resources.NotFoundException("Can't load animation resource ID #0x" +
|
||||
Integer.toHexString(id));
|
||||
rnf.initCause(ex);
|
||||
throw rnf;
|
||||
} finally {
|
||||
if (parser != null)
|
||||
parser.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static Interpolator createInterpolatorFromXml(Context context, XmlPullParser parser)
|
||||
throws XmlPullParserException, IOException {
|
||||
|
||||
Interpolator interpolator = null;
|
||||
|
||||
// Make sure we are on a start tag.
|
||||
int type;
|
||||
int depth = parser.getDepth();
|
||||
|
||||
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
||||
&& type != XmlPullParser.END_DOCUMENT) {
|
||||
|
||||
if (type != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeSet attrs = Xml.asAttributeSet(parser);
|
||||
|
||||
String name = parser.getName();
|
||||
|
||||
if (name.equals("linearInterpolator")) {
|
||||
interpolator = new LinearInterpolator();
|
||||
} else if (name.equals("accelerateInterpolator")) {
|
||||
interpolator = new AccelerateInterpolator(context, attrs);
|
||||
} else if (name.equals("decelerateInterpolator")) {
|
||||
interpolator = new DecelerateInterpolator(context, attrs);
|
||||
} else if (name.equals("accelerateDecelerateInterpolator")) {
|
||||
interpolator = new AccelerateDecelerateInterpolator();
|
||||
} else if (name.equals("cycleInterpolator")) {
|
||||
interpolator = new CycleInterpolator(context, attrs);
|
||||
} else if (name.equals("anticipateInterpolator")) {
|
||||
interpolator = new AnticipateInterpolator(context, attrs);
|
||||
} else if (name.equals("overshootInterpolator")) {
|
||||
interpolator = new OvershootInterpolator(context, attrs);
|
||||
} else if (name.equals("anticipateOvershootInterpolator")) {
|
||||
interpolator = new AnticipateOvershootInterpolator(context, attrs);
|
||||
} else if (name.equals("bounceInterpolator")) {
|
||||
interpolator = new BounceInterpolator();
|
||||
} else if (name.equals("pathInterpolator")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
interpolator = new android.view.animation.PathInterpolator(context, attrs);
|
||||
} else {
|
||||
interpolator = new PathInterpolator(context, attrs);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Unknown interpolator name: " + parser.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return interpolator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.animation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.InflateException;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
/**
|
||||
* An interpolator that can traverse a Path that extends from <code>Point</code>
|
||||
* <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code>
|
||||
* is the input value and the output is the y coordinate of the line at that point.
|
||||
* This means that the Path must conform to a function <code>y = f(x)</code>.
|
||||
*
|
||||
* <p>The <code>Path</code> must not have gaps in the x direction and must not
|
||||
* loop back on itself such that there can be two points sharing the same x coordinate.
|
||||
* It is alright to have a disjoint line in the vertical direction:</p>
|
||||
* <p><blockquote><pre>
|
||||
* Path path = new Path();
|
||||
* path.lineTo(0.25f, 0.25f);
|
||||
* path.moveTo(0.25f, 0.5f);
|
||||
* path.lineTo(1f, 1f);
|
||||
* </pre></blockquote></p>
|
||||
*/
|
||||
public class PathInterpolator implements Interpolator {
|
||||
|
||||
// This governs how accurate the approximation of the Path is.
|
||||
private static final float PRECISION = 0.002f;
|
||||
private static final float ERROR_SQUARED = PRECISION * PRECISION;
|
||||
|
||||
private float[] mX; // x coordinates in the line
|
||||
|
||||
private float[] mY; // y coordinates in the line
|
||||
|
||||
/**
|
||||
* Create an interpolator for an arbitrary <code>Path</code>. The <code>Path</code>
|
||||
* must begin at <code>(0, 0)</code> and end at <code>(1, 1)</code>.
|
||||
*
|
||||
* @param path The <code>Path</code> to use to make the line representing the interpolator.
|
||||
*/
|
||||
/* // TODO I think I do not need it
|
||||
public PathInterpolator(Path path) {
|
||||
initPath(path);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an interpolator for a quadratic Bezier curve. The end points
|
||||
* <code>(0, 0)</code> and <code>(1, 1)</code> are assumed.
|
||||
*
|
||||
* @param controlX The x coordinate of the quadratic Bezier control point.
|
||||
* @param controlY The y coordinate of the quadratic Bezier control point.
|
||||
*/
|
||||
public PathInterpolator(float controlX, float controlY) {
|
||||
initQuad(controlX, controlY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an interpolator for a cubic Bezier curve. The end points
|
||||
* <code>(0, 0)</code> and <code>(1, 1)</code> are assumed.
|
||||
*
|
||||
* @param controlX1 The x coordinate of the first control point of the cubic Bezier.
|
||||
* @param controlY1 The y coordinate of the first control point of the cubic Bezier.
|
||||
* @param controlX2 The x coordinate of the second control point of the cubic Bezier.
|
||||
* @param controlY2 The y coordinate of the second control point of the cubic Bezier.
|
||||
*/
|
||||
public PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2) {
|
||||
initCubic(controlX1, controlY1, controlX2, controlY2);
|
||||
}
|
||||
|
||||
public PathInterpolator(Context context, AttributeSet attrs) {
|
||||
this(context.getResources(), context.getTheme(), attrs);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public PathInterpolator(Resources res, Theme theme, AttributeSet attrs) {
|
||||
TypedArray a;
|
||||
if (theme != null) {
|
||||
a = theme.obtainStyledAttributes(attrs, R.styleable.PathInterpolator, 0, 0);
|
||||
} else {
|
||||
a = res.obtainAttributes(attrs, R.styleable.PathInterpolator);
|
||||
}
|
||||
parseInterpolatorFromTypeArray(a);
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
private void parseInterpolatorFromTypeArray(TypedArray a) {
|
||||
// If there is pathData defined in the xml file, then the controls points
|
||||
// will be all coming from pathData.
|
||||
if (a.hasValue(R.styleable.PathInterpolator_pathData)) {
|
||||
/* TODO I think I do not need it
|
||||
String pathData = a.getString(R.styleable.PathInterpolator_pathData);
|
||||
Path path = PathParser.createPathFromPathData(pathData);
|
||||
if (path == null) {
|
||||
throw new InflateException("The path is null, which is created"
|
||||
+ " from " + pathData);
|
||||
}
|
||||
initPath(path);
|
||||
*/
|
||||
} else {
|
||||
if (!a.hasValue(R.styleable.PathInterpolator_controlX1)) {
|
||||
throw new InflateException("pathInterpolator requires the controlX1 attribute");
|
||||
} else if (!a.hasValue(R.styleable.PathInterpolator_controlY1)) {
|
||||
throw new InflateException("pathInterpolator requires the controlY1 attribute");
|
||||
}
|
||||
float x1 = a.getFloat(R.styleable.PathInterpolator_controlX1, 0);
|
||||
float y1 = a.getFloat(R.styleable.PathInterpolator_controlY1, 0);
|
||||
|
||||
boolean hasX2 = a.hasValue(R.styleable.PathInterpolator_controlX2);
|
||||
boolean hasY2 = a.hasValue(R.styleable.PathInterpolator_controlY2);
|
||||
|
||||
if (hasX2 != hasY2) {
|
||||
throw new InflateException(
|
||||
"pathInterpolator requires both controlX2 and controlY2 for cubic Beziers.");
|
||||
}
|
||||
|
||||
if (!hasX2) {
|
||||
initQuad(x1, y1);
|
||||
} else {
|
||||
float x2 = a.getFloat(R.styleable.PathInterpolator_controlX2, 0);
|
||||
float y2 = a.getFloat(R.styleable.PathInterpolator_controlY2, 0);
|
||||
initCubic(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initQuad(float controlX, float controlY) {
|
||||
float[] points = bezierApproximate(new float[]{controlX, controlY}, ERROR_SQUARED, false);
|
||||
loadPointComponents(points);
|
||||
}
|
||||
|
||||
private void initCubic(float x1, float y1, float x2, float y2) {
|
||||
float[] points = bezierApproximate(new float[]{x1, y1, x2, y2}, ERROR_SQUARED, true);
|
||||
loadPointComponents(points);
|
||||
}
|
||||
|
||||
private void loadPointComponents(float[] pointComponents) {
|
||||
int numPoints = pointComponents.length / 3;
|
||||
if (pointComponents[1] != 0 || pointComponents[2] != 0
|
||||
|| pointComponents[pointComponents.length - 2] != 1
|
||||
|| pointComponents[pointComponents.length - 1] != 1) {
|
||||
throw new IllegalArgumentException("The Path must start at (0,0) and end at (1,1)");
|
||||
}
|
||||
|
||||
mX = new float[numPoints];
|
||||
mY = new float[numPoints];
|
||||
float prevX = 0;
|
||||
float prevFraction = 0;
|
||||
int componentIndex = 0;
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
float fraction = pointComponents[componentIndex++];
|
||||
float x = pointComponents[componentIndex++];
|
||||
float y = pointComponents[componentIndex++];
|
||||
if (fraction == prevFraction && x != prevX) {
|
||||
throw new IllegalArgumentException(
|
||||
"The Path cannot have discontinuity in the X axis.");
|
||||
}
|
||||
if (x < prevX) {
|
||||
throw new IllegalArgumentException("The Path cannot loop back on itself.");
|
||||
}
|
||||
mX[i] = x;
|
||||
mY[i] = y;
|
||||
prevX = x;
|
||||
prevFraction = fraction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the line in the Path in this interpolator that can be described as
|
||||
* <code>y = f(x)</code>, finds the y coordinate of the line given <code>t</code>
|
||||
* as the x coordinate. Values less than 0 will always return 0 and values greater
|
||||
* than 1 will always return 1.
|
||||
*
|
||||
* @param t Treated as the x coordinate along the line.
|
||||
* @return The y coordinate of the Path along the line where x = <code>t</code>.
|
||||
* @see Interpolator#getInterpolation(float)
|
||||
*/
|
||||
@Override
|
||||
public float getInterpolation(float t) {
|
||||
if (t <= 0) {
|
||||
return 0;
|
||||
} else if (t >= 1) {
|
||||
return 1;
|
||||
}
|
||||
// Do a binary search for the correct x to interpolate between.
|
||||
int startIndex = 0;
|
||||
int endIndex = mX.length - 1;
|
||||
|
||||
while (endIndex - startIndex > 1) {
|
||||
int midIndex = (startIndex + endIndex) / 2;
|
||||
if (t < mX[midIndex]) {
|
||||
endIndex = midIndex;
|
||||
} else {
|
||||
startIndex = midIndex;
|
||||
}
|
||||
}
|
||||
|
||||
float xRange = mX[endIndex] - mX[startIndex];
|
||||
if (xRange == 0) {
|
||||
return mY[startIndex];
|
||||
}
|
||||
|
||||
float tInRange = t - mX[startIndex];
|
||||
float fraction = tInRange / xRange;
|
||||
|
||||
float startY = mY[startIndex];
|
||||
float endY = mY[endIndex];
|
||||
return startY + (fraction * (endY - startY));
|
||||
}
|
||||
|
||||
// Divides Bezier curves until linear interpolation is very close to accurate, using
|
||||
// errorSquared as a metric. Cubic Bezier curves can have an inflection point that improperly
|
||||
// short-circuit subdivision. If you imagine an S shape, the top and bottom points being the
|
||||
// starting and end points, linear interpolation would mark the center where the curve places
|
||||
// the point. It is clearly not the case that we can linearly interpolate at that point.
|
||||
// doubleCheckDivision forces a second examination between subdivisions to ensure that linear
|
||||
// interpolation works.
|
||||
private static float[] bezierApproximate(float[] control, float errorSquared,
|
||||
boolean isCubic) {
|
||||
PointMap pointMap = new PointMap();
|
||||
|
||||
pointMap.add(0f, 0f, 0f);
|
||||
pointMap.add(1f, 1f, 1f);
|
||||
|
||||
PointMap.Iterator iter = pointMap.iterator();
|
||||
PointMap.Iterator next = iter.nextIterator();
|
||||
|
||||
boolean doubleCheckDivision = isCubic;
|
||||
while (next != null) {
|
||||
boolean needsSubdivision;
|
||||
|
||||
do {
|
||||
PointMap.Link iterLink = iter.link;
|
||||
PointMap.Link nextLink = next.link;
|
||||
|
||||
float midT = (iterLink.t + nextLink.t) / 2;
|
||||
float midX = (iterLink.x + nextLink.x) / 2;
|
||||
float midY = (iterLink.y + nextLink.y) / 2;
|
||||
|
||||
float midPointX = bezierX(midT, control, isCubic);
|
||||
float midPointY = bezierY(midT, control, isCubic);
|
||||
|
||||
needsSubdivision = isOutOfErrorSquared(midX, midY, midPointX, midPointY, errorSquared);
|
||||
|
||||
if (!needsSubdivision && doubleCheckDivision) {
|
||||
float quarterT = (iterLink.t + midT) / 2;
|
||||
float quarterX = (iterLink.x + midPointX) / 2;
|
||||
float quarterY = (iterLink.y + midPointY) / 2;
|
||||
|
||||
float quarterPointX = bezierX(quarterT, control, isCubic);
|
||||
float quarterPointY = bezierY(quarterT, control, isCubic);
|
||||
|
||||
needsSubdivision = isOutOfErrorSquared(quarterX, quarterY, quarterPointX, quarterPointY, errorSquared);
|
||||
if (needsSubdivision) {
|
||||
// Found an inflection point. No need to double-check.
|
||||
doubleCheckDivision = false;
|
||||
}
|
||||
}
|
||||
if (needsSubdivision) {
|
||||
next.addBefore(midT, midPointX, midPointY);
|
||||
} else {
|
||||
float length = dist(iterLink.x, iterLink.y, nextLink.x, nextLink.y);
|
||||
iterLink.f = pointMap.totalLength;
|
||||
pointMap.totalLength += length;
|
||||
}
|
||||
} while (needsSubdivision);
|
||||
|
||||
iter = next;
|
||||
next = next.nextIterator();
|
||||
}
|
||||
|
||||
iter.link.f = pointMap.totalLength;
|
||||
|
||||
return pointMap.generate();
|
||||
}
|
||||
|
||||
private static float dist(float x1, float y1, float x2, float y2) {
|
||||
final float x = (x2 - x1);
|
||||
final float y = (y2 - y1);
|
||||
return (float) Math.hypot(x, y);
|
||||
}
|
||||
|
||||
private static boolean isOutOfErrorSquared(float midX, float midY,
|
||||
float midPointX, float midPointY, float errorSquared) {
|
||||
float xError = midPointX - midX;
|
||||
float yError = midPointY - midY;
|
||||
float midErrorSquared = (xError * xError) + (yError * yError);
|
||||
return midErrorSquared > errorSquared;
|
||||
}
|
||||
|
||||
private static float bezierX(float t, float[] control, boolean isCubic) {
|
||||
if (isCubic) {
|
||||
return cubicCoordinateCalculation(t, 0f, control[0], control[2], 1f);
|
||||
} else {
|
||||
return quadraticCoordinateCalculation(t, 0f, control[0], 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private static float bezierY(float t, float[] control, boolean isCubic) {
|
||||
if (isCubic) {
|
||||
return cubicCoordinateCalculation(t, 0f, control[1], control[3], 1f);
|
||||
} else {
|
||||
return quadraticCoordinateCalculation(t, 0f, control[1], 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private static float quadraticCoordinateCalculation(float t, float p0, float p1, float p2) {
|
||||
float oneMinusT = 1 - t;
|
||||
return oneMinusT * ((oneMinusT * p0) + (t * p1)) + t * ((oneMinusT * p1) + (t * p2));
|
||||
}
|
||||
|
||||
private static float cubicCoordinateCalculation(float t, float p0, float p1, float p2, float p3) {
|
||||
float oneMinusT = 1 - t;
|
||||
float oneMinusTSquared = oneMinusT * oneMinusT;
|
||||
float oneMinusTCubed = oneMinusTSquared * oneMinusT;
|
||||
float tSquared = t * t;
|
||||
float tCubed = tSquared * t;
|
||||
return (oneMinusTCubed * p0) + (3 * oneMinusTSquared * t * p1)
|
||||
+ (3 * oneMinusT * tSquared * p2) + (tCubed * p3);
|
||||
}
|
||||
|
||||
private static final class PointMap {
|
||||
|
||||
private int size = 0;
|
||||
|
||||
private Link firstLink;
|
||||
private Link lastLink;
|
||||
|
||||
float totalLength = 0f;
|
||||
|
||||
private static final class Link {
|
||||
float t;
|
||||
float x;
|
||||
float y;
|
||||
float f = -1;
|
||||
|
||||
private Link previous, next;
|
||||
|
||||
Link(float t, float x, float y, Link p, Link n) {
|
||||
this.t = t;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.previous = p;
|
||||
this.next = n;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Iterator {
|
||||
|
||||
final PointMap parent;
|
||||
Link link;
|
||||
|
||||
private Iterator(PointMap p, Link l) {
|
||||
parent = p;
|
||||
link = l;
|
||||
}
|
||||
|
||||
boolean hasNext() {
|
||||
return link.next != null;
|
||||
}
|
||||
|
||||
Iterator nextIterator() {
|
||||
if (hasNext()) {
|
||||
return new Iterator(parent, link.next);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Move iterator to the new data
|
||||
void addBefore(float t, float x, float y) {
|
||||
Link p = link.previous;
|
||||
Link l = new Link(t, x, y, p, link);
|
||||
link.previous = l;
|
||||
p.next = l;
|
||||
link = l;
|
||||
parent.size++;
|
||||
}
|
||||
}
|
||||
|
||||
public void add(float t, float x, float y) {
|
||||
Link link = new Link(t, x, y, null, null);
|
||||
if (lastLink != null) {
|
||||
link.previous = lastLink;
|
||||
lastLink.next = link;
|
||||
}
|
||||
lastLink = link;
|
||||
if (firstLink == null) {
|
||||
firstLink = link;
|
||||
}
|
||||
size++;
|
||||
}
|
||||
|
||||
// Get a iterator at first data or null
|
||||
public Iterator iterator() {
|
||||
if (firstLink != null) {
|
||||
return new Iterator(this, firstLink);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public float[] generate() {
|
||||
int index = 0;
|
||||
float[] result = new float[size * 3];
|
||||
for (Link link = firstLink; link != null; link = link.next) {
|
||||
result[index++] = link.f / totalLength;
|
||||
result[index++] = link.x;
|
||||
result[index++] = link.y;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.content;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.VectorResources;
|
||||
import android.os.Build;
|
||||
|
||||
public class VectorContext extends ContextWrapper {
|
||||
|
||||
private VectorResources mVectorResources;
|
||||
|
||||
public VectorContext(Context base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources getResources() {
|
||||
final Resources superResources = super.getResources();
|
||||
if (mVectorResources == null || mVectorResources.isBase(superResources)) {
|
||||
mVectorResources = new VectorResources(this, superResources);
|
||||
}
|
||||
return mVectorResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* In {@link android.content.ContextWrapper#attachBaseContext(Context)},
|
||||
* do <code>super.attachBaseContext(Vector.wrapContext(newBase));</code>.
|
||||
*
|
||||
* @param context the context
|
||||
* @return new context
|
||||
*/
|
||||
public static Context wrapContext(Context context) {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? new VectorContext(context) : context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.util.ArrayMap;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.hippo.animation.AnimatorInflater;
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class uses {@link android.animation.ObjectAnimator} and
|
||||
* {@link android.animation.AnimatorSet} to animate the properties of a
|
||||
* {@link android.graphics.drawable.VectorDrawable} to create an animated drawable.
|
||||
* <p>
|
||||
* AnimatedVectorDrawable are normally defined as 3 separate XML files.
|
||||
* </p>
|
||||
* <p>
|
||||
* First is the XML file for {@link android.graphics.drawable.VectorDrawable}.
|
||||
* Note that we allow the animation happen on the group's attributes and path's
|
||||
* attributes, which requires they are uniquely named in this xml file. Groups
|
||||
* and paths without animations do not need names.
|
||||
* </p>
|
||||
* <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
|
||||
* <pre>
|
||||
* <vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
* android:height="64dp"
|
||||
* android:width="64dp"
|
||||
* android:viewportHeight="600"
|
||||
* android:viewportWidth="600" >
|
||||
* <group
|
||||
* android:name="rotationGroup"
|
||||
* android:pivotX="300.0"
|
||||
* android:pivotY="300.0"
|
||||
* android:rotation="45.0" >
|
||||
* <path
|
||||
* android:name="v"
|
||||
* android:fillColor="#000000"
|
||||
* android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
|
||||
* </group>
|
||||
* </vector>
|
||||
* </pre></li>
|
||||
* <p>
|
||||
* Second is the AnimatedVectorDrawable's xml file, which defines the target
|
||||
* VectorDrawable, the target paths and groups to animate, the properties of the
|
||||
* path and group to animate and the animations defined as the ObjectAnimators
|
||||
* or AnimatorSets.
|
||||
* </p>
|
||||
* <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file.
|
||||
* Note how we use the names to refer to the groups and paths in the vectordrawable.xml.
|
||||
* <pre>
|
||||
* <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
* android:drawable="@drawable/vectordrawable" >
|
||||
* <target
|
||||
* android:name="rotationGroup"
|
||||
* android:animation="@anim/rotation" />
|
||||
* <target
|
||||
* android:name="v"
|
||||
* android:animation="@anim/path_morph" />
|
||||
* </animated-vector>
|
||||
* </pre></li>
|
||||
* <p>
|
||||
* Last is the Animator xml file, which is the same as a normal ObjectAnimator
|
||||
* or AnimatorSet.
|
||||
* To complete this example, here are the 2 animator files used in avd.xml:
|
||||
* rotation.xml and path_morph.xml.
|
||||
* </p>
|
||||
* <li>Here is the rotation.xml, which will rotate the target group for 360 degrees.
|
||||
* <pre>
|
||||
* <objectAnimator
|
||||
* android:duration="6000"
|
||||
* android:propertyName="rotation"
|
||||
* android:valueFrom="0"
|
||||
* android:valueTo="360" />
|
||||
* </pre></li>
|
||||
* <li>Here is the path_morph.xml, which will morph the path from one shape to
|
||||
* the other. Note that the paths must be compatible for morphing.
|
||||
* In more details, the paths should have exact same length of commands , and
|
||||
* exact same length of parameters for each commands.
|
||||
* Note that the path string are better stored in strings.xml for reusing.
|
||||
* <pre>
|
||||
* <set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <objectAnimator
|
||||
* android:duration="3000"
|
||||
* android:propertyName="pathData"
|
||||
* android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
|
||||
* android:valueTo="M300,70 l 0,-70 70,0 0,140 -70,0 z"
|
||||
* android:valueType="pathType"/>
|
||||
* </set>
|
||||
* </pre></li>
|
||||
*
|
||||
* @attr ref android.R.styleable#AnimatedVectorDrawable_drawable
|
||||
* @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name
|
||||
* @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
|
||||
*/
|
||||
public class AnimatedVectorDrawable extends Drawable implements Animatable {
|
||||
private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName();
|
||||
|
||||
private static final String ANIMATED_VECTOR = "animated-vector";
|
||||
private static final String TARGET = "target";
|
||||
|
||||
private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
|
||||
|
||||
private AnimatedVectorDrawableState mAnimatedVectorState;
|
||||
|
||||
private boolean mMutated;
|
||||
|
||||
public AnimatedVectorDrawable() {
|
||||
mAnimatedVectorState = new AnimatedVectorDrawableState(null);
|
||||
}
|
||||
|
||||
private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
|
||||
mAnimatedVectorState = new AnimatedVectorDrawableState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable mutate() {
|
||||
if (!mMutated && super.mutate() == this) {
|
||||
mAnimatedVectorState.mVectorDrawable.mutate();
|
||||
mMutated = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConstantState getConstantState() {
|
||||
mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
|
||||
return mAnimatedVectorState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangingConfigurations() {
|
||||
return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
mAnimatedVectorState.mVectorDrawable.draw(canvas);
|
||||
if (isStarted()) {
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] state) {
|
||||
return mAnimatedVectorState.mVectorDrawable.setState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onLevelChange(int level) {
|
||||
return mAnimatedVectorState.mVectorDrawable.setLevel(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlpha() {
|
||||
return mAnimatedVectorState.mVectorDrawable.getAlpha();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintList(ColorStateList tint) {
|
||||
mAnimatedVectorState.mVectorDrawable.setTintList(tint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintMode(PorterDuff.Mode tintMode) {
|
||||
mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
|
||||
return super.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return mAnimatedVectorState.mVectorDrawable.isStateful();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return mAnimatedVectorState.mVectorDrawable.getOpacity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
public void inflate(Context context, XmlPullParser parser, AttributeSet attrs)
|
||||
throws XmlPullParserException, IOException {
|
||||
Resources res = context.getResources();
|
||||
int eventType = parser.getEventType();
|
||||
float pathErrorScale = 1;
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
final String tagName = parser.getName();
|
||||
if (ANIMATED_VECTOR.equals(tagName)) {
|
||||
final TypedArray a = res.obtainAttributes(attrs,
|
||||
R.styleable.AnimatedVectorDrawable);
|
||||
int drawableRes = a.getResourceId(
|
||||
R.styleable.AnimatedVectorDrawable_drawable, 0);
|
||||
if (drawableRes != 0) {
|
||||
VectorDrawable vectorDrawable =(VectorDrawable) res.getDrawable(
|
||||
drawableRes).mutate();
|
||||
vectorDrawable.setAllowCaching(false);
|
||||
pathErrorScale = vectorDrawable.getPixelSize();
|
||||
mAnimatedVectorState.mVectorDrawable = vectorDrawable;
|
||||
}
|
||||
a.recycle();
|
||||
} else if (TARGET.equals(tagName)) {
|
||||
final TypedArray a = res.obtainAttributes(attrs,
|
||||
R.styleable.AnimatedVectorDrawableTarget);
|
||||
final String target = a.getString(
|
||||
R.styleable.AnimatedVectorDrawableTarget_name);
|
||||
|
||||
int id = a.getResourceId(
|
||||
R.styleable.AnimatedVectorDrawableTarget_animation, 0);
|
||||
if (id != 0) {
|
||||
Animator objectAnimator = AnimatorInflater.loadAnimator(context, id, pathErrorScale);
|
||||
setupAnimatorsForTarget(target, objectAnimator);
|
||||
}
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
eventType = parser.next();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AnimatedVectorDrawableState extends ConstantState {
|
||||
int mChangingConfigurations;
|
||||
VectorDrawable mVectorDrawable;
|
||||
ArrayList<Animator> mAnimators;
|
||||
ArrayMap<Animator, String> mTargetNameMap;
|
||||
|
||||
public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy) {
|
||||
if (copy != null) {
|
||||
mChangingConfigurations = copy.mChangingConfigurations;
|
||||
if (copy.mVectorDrawable != null) {
|
||||
mVectorDrawable = (VectorDrawable) copy.mVectorDrawable.getConstantState().newDrawable();
|
||||
mVectorDrawable.mutate();
|
||||
mVectorDrawable.setAllowCaching(false);
|
||||
mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
|
||||
}
|
||||
if (copy.mAnimators != null) {
|
||||
final int numAnimators = copy.mAnimators.size();
|
||||
mAnimators = new ArrayList<>(numAnimators);
|
||||
mTargetNameMap = new ArrayMap<>(numAnimators);
|
||||
for (int i = 0; i < numAnimators; ++i) {
|
||||
Animator anim = copy.mAnimators.get(i);
|
||||
Animator animClone = anim.clone();
|
||||
String targetName = copy.mTargetNameMap.get(anim);
|
||||
Object targetObject = mVectorDrawable.getTargetByName(targetName);
|
||||
animClone.setTarget(targetObject);
|
||||
mAnimators.add(animClone);
|
||||
mTargetNameMap.put(animClone, targetName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mVectorDrawable = new VectorDrawable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable newDrawable() {
|
||||
return new AnimatedVectorDrawable(this, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable newDrawable(Resources res) {
|
||||
return new AnimatedVectorDrawable(this, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangingConfigurations() {
|
||||
return mChangingConfigurations;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupAnimatorsForTarget(String name, Animator animator) {
|
||||
Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
|
||||
animator.setTarget(target);
|
||||
if (mAnimatedVectorState.mAnimators == null) {
|
||||
mAnimatedVectorState.mAnimators = new ArrayList<>();
|
||||
mAnimatedVectorState.mTargetNameMap = new ArrayMap<>();
|
||||
}
|
||||
mAnimatedVectorState.mAnimators.add(animator);
|
||||
mAnimatedVectorState.mTargetNameMap.put(animator, name);
|
||||
if (DBG_ANIMATION_VECTOR_DRAWABLE) {
|
||||
Log.v(LOGTAG, "add animator for target " + name + " " + animator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
if (animator.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
if (animator.isStarted()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
if (!animator.isStarted()) {
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
animator.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses ongoing animations or starts pending animations in reverse.
|
||||
* <p>
|
||||
* NOTE: Only works of all animations are ValueAnimators.
|
||||
* @hide
|
||||
*/
|
||||
/*
|
||||
public void reverse() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
if (animator.canReverse()) {
|
||||
animator.reverse();
|
||||
} else {
|
||||
Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
/*
|
||||
public boolean canReverse() {
|
||||
final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
|
||||
final int size = animators.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final Animator animator = animators.get(i);
|
||||
if (!animator.canReverse()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,10 +15,12 @@
|
||||
|
||||
package com.hippo.ehviewer.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
|
||||
import com.hippo.content.VectorContext;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||
import com.hippo.ehviewer.widget.ContentLayout;
|
||||
@@ -61,4 +63,9 @@ public class ContentActivity extends StageActivity {
|
||||
public StageLayout getStageLayout() {
|
||||
return mContentLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(VectorContext.wrapContext(newBase));
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class GalleryListScene extends Scene implements SearchLayout.SearhLayoutH
|
||||
|
||||
// List View
|
||||
// TODO
|
||||
mListView = new View(mActivity);
|
||||
mListView = inflater.inflate(R.layout.fragment_list, null);
|
||||
|
||||
// Main View
|
||||
View sceneView = inflater.inflate(R.layout.scene_gallery_list, null);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.util;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
public class DrawableUtils {
|
||||
|
||||
public static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
|
||||
|
||||
/**
|
||||
* Ensures the tint filter is consistent with the current tint color and
|
||||
* mode.
|
||||
*/
|
||||
public static PorterDuffColorFilter updateTintFilter(Drawable drawable,
|
||||
PorterDuffColorFilter tintFilter, ColorStateList tint,
|
||||
PorterDuff.Mode tintMode) {
|
||||
if (tint == null || tintMode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int color = tint.getColorForState(drawable.getState(), Color.TRANSPARENT);
|
||||
return new PorterDuffColorFilter(color, tintMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a {@link PorterDuff.Mode} from a tintMode
|
||||
* attribute's enum value.
|
||||
*/
|
||||
public static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
|
||||
switch (value) {
|
||||
case 3: return PorterDuff.Mode.SRC_OVER;
|
||||
case 5: return PorterDuff.Mode.SRC_IN;
|
||||
case 9: return PorterDuff.Mode.SRC_ATOP;
|
||||
case 14: return PorterDuff.Mode.MULTIPLY;
|
||||
case 15: return PorterDuff.Mode.SCREEN;
|
||||
case 16: return PorterDuff.Mode.ADD;
|
||||
default: return defaultMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.hippo.util;
|
||||
|
||||
import android.graphics.Path;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public class PathParser {
|
||||
static final String LOGTAG = PathParser.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* @param pathData The string representing a path, the same as "d" string in svg file.
|
||||
* @return the generated Path object.
|
||||
*/
|
||||
public static Path createPathFromPathData(String pathData) {
|
||||
Path path = new Path();
|
||||
PathDataNode[] nodes = createNodesFromPathData(pathData);
|
||||
if (nodes != null) {
|
||||
PathDataNode.nodesToPath(nodes, path);
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pathData The string representing a path, the same as "d" string in svg file.
|
||||
* @return an array of the PathDataNode.
|
||||
*/
|
||||
public static PathDataNode[] createNodesFromPathData(String pathData) {
|
||||
if (pathData == null) {
|
||||
return null;
|
||||
}
|
||||
int start = 0;
|
||||
int end = 1;
|
||||
|
||||
ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
|
||||
while (end < pathData.length()) {
|
||||
end = nextStart(pathData, end);
|
||||
String s = pathData.substring(start, end).trim();
|
||||
if (s.length() > 0) {
|
||||
float[] val = getFloats(s);
|
||||
addNode(list, s.charAt(0), val);
|
||||
}
|
||||
|
||||
start = end;
|
||||
end++;
|
||||
}
|
||||
if ((end - start) == 1 && start < pathData.length()) {
|
||||
addNode(list, pathData.charAt(start), new float[0]);
|
||||
}
|
||||
return list.toArray(new PathDataNode[list.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The array of PathDataNode to be duplicated.
|
||||
* @return a deep copy of the <code>source</code>.
|
||||
*/
|
||||
public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
PathDataNode[] copy = new PathDataNode[source.length];
|
||||
for (int i = 0; i < source.length; i ++) {
|
||||
copy[i] = new PathDataNode(source[i]);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nodesFrom The source path represented in an array of PathDataNode
|
||||
* @param nodesTo The target path represented in an array of PathDataNode
|
||||
* @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
|
||||
*/
|
||||
public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
|
||||
if (nodesFrom == null || nodesTo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nodesFrom.length != nodesTo.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nodesFrom.length; i ++) {
|
||||
if (nodesFrom[i].mType != nodesTo[i].mType
|
||||
|| nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the target's data to match the source.
|
||||
* Before calling this, make sure canMorph(target, source) is true.
|
||||
*
|
||||
* @param target The target path represented in an array of PathDataNode
|
||||
* @param source The source path represented in an array of PathDataNode
|
||||
*/
|
||||
public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
|
||||
for (int i = 0; i < source.length; i ++) {
|
||||
target[i].mType = source[i].mType;
|
||||
for (int j = 0; j < source[i].mParams.length; j ++) {
|
||||
target[i].mParams[j] = source[i].mParams[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int nextStart(String s, int end) {
|
||||
char c;
|
||||
|
||||
while (end < s.length()) {
|
||||
c = s.charAt(end);
|
||||
if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) {
|
||||
return end;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
|
||||
list.add(new PathDataNode(cmd, val));
|
||||
}
|
||||
|
||||
private static class ExtractFloatResult {
|
||||
// We need to return the position of the next separator and whether the
|
||||
// next float starts with a '-'.
|
||||
int mEndPosition;
|
||||
boolean mEndWithNegSign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the floats in the string.
|
||||
* This is an optimized version of parseFloat(s.split(",|\\s"));
|
||||
*
|
||||
* @param s the string containing a command and list of floats
|
||||
* @return array of floats
|
||||
*/
|
||||
private static float[] getFloats(String s) {
|
||||
if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
|
||||
return new float[0];
|
||||
}
|
||||
try {
|
||||
float[] results = new float[s.length()];
|
||||
int count = 0;
|
||||
int startPosition = 1;
|
||||
int endPosition = 0;
|
||||
|
||||
ExtractFloatResult result = new ExtractFloatResult();
|
||||
int totalLength = s.length();
|
||||
|
||||
// The startPosition should always be the first character of the
|
||||
// current number, and endPosition is the character after the current
|
||||
// number.
|
||||
while (startPosition < totalLength) {
|
||||
extract(s, startPosition, result);
|
||||
endPosition = result.mEndPosition;
|
||||
|
||||
if (startPosition < endPosition) {
|
||||
results[count++] = Float.parseFloat(
|
||||
s.substring(startPosition, endPosition));
|
||||
}
|
||||
|
||||
if (result.mEndWithNegSign) {
|
||||
// Keep the '-' sign with next number.
|
||||
startPosition = endPosition;
|
||||
} else {
|
||||
startPosition = endPosition + 1;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOf(results, count);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(LOGTAG, "error in parsing \"" + s + "\"");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the position of the next comma or space or negative sign
|
||||
* @param s the string to search
|
||||
* @param start the position to start searching
|
||||
* @param result the result of the extraction, including the position of the
|
||||
* the starting position of next number, whether it is ending with a '-'.
|
||||
*/
|
||||
private static void extract(String s, int start, ExtractFloatResult result) {
|
||||
// Now looking for ' ', ',' or '-' from the start.
|
||||
int currentIndex = start;
|
||||
boolean foundSeparator = false;
|
||||
result.mEndWithNegSign = false;
|
||||
for (; currentIndex < s.length(); currentIndex++) {
|
||||
char currentChar = s.charAt(currentIndex);
|
||||
switch (currentChar) {
|
||||
case ' ':
|
||||
case ',':
|
||||
foundSeparator = true;
|
||||
break;
|
||||
case '-':
|
||||
if (currentIndex != start) {
|
||||
foundSeparator = true;
|
||||
result.mEndWithNegSign = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (foundSeparator) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// When there is nothing found, then we put the end position to the end
|
||||
// of the string.
|
||||
result.mEndPosition = currentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each PathDataNode represents one command in the "d" attribute of the svg
|
||||
* file.
|
||||
* An array of PathDataNode can represent the whole "d" attribute.
|
||||
*/
|
||||
public static class PathDataNode {
|
||||
private char mType;
|
||||
private float[] mParams;
|
||||
|
||||
private PathDataNode(char type, float[] params) {
|
||||
mType = type;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
private PathDataNode(PathDataNode n) {
|
||||
mType = n.mType;
|
||||
mParams = Arrays.copyOf(n.mParams, n.mParams.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of PathDataNode to Path.
|
||||
*
|
||||
* @param node The source array of PathDataNode.
|
||||
* @param path The target Path object.
|
||||
*/
|
||||
public static void nodesToPath(PathDataNode[] node, Path path) {
|
||||
float[] current = new float[4];
|
||||
char previousCommand = 'm';
|
||||
for (int i = 0; i < node.length; i++) {
|
||||
addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
|
||||
previousCommand = node[i].mType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current PathDataNode will be interpolated between the
|
||||
* <code>nodeFrom</code> and <code>nodeTo</code> according to the
|
||||
* <code>fraction</code>.
|
||||
*
|
||||
* @param nodeFrom The start value as a PathDataNode.
|
||||
* @param nodeTo The end value as a PathDataNode
|
||||
* @param fraction The fraction to interpolate.
|
||||
*/
|
||||
public void interpolatePathDataNode(PathDataNode nodeFrom,
|
||||
PathDataNode nodeTo, float fraction) {
|
||||
for (int i = 0; i < nodeFrom.mParams.length; i++) {
|
||||
mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
|
||||
+ nodeTo.mParams[i] * fraction;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addCommand(Path path, float[] current,
|
||||
char previousCmd, char cmd, float[] val) {
|
||||
|
||||
int incr = 2;
|
||||
float currentX = current[0];
|
||||
float currentY = current[1];
|
||||
float ctrlPointX = current[2];
|
||||
float ctrlPointY = current[3];
|
||||
float reflectiveCtrlPointX;
|
||||
float reflectiveCtrlPointY;
|
||||
|
||||
switch (cmd) {
|
||||
case 'z':
|
||||
case 'Z':
|
||||
path.close();
|
||||
return;
|
||||
case 'm':
|
||||
case 'M':
|
||||
case 'l':
|
||||
case 'L':
|
||||
case 't':
|
||||
case 'T':
|
||||
incr = 2;
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
case 'v':
|
||||
case 'V':
|
||||
incr = 1;
|
||||
break;
|
||||
case 'c':
|
||||
case 'C':
|
||||
incr = 6;
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
case 'q':
|
||||
case 'Q':
|
||||
incr = 4;
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
incr = 7;
|
||||
break;
|
||||
}
|
||||
for (int k = 0; k < val.length; k += incr) {
|
||||
switch (cmd) {
|
||||
case 'm': // moveto - Start a new sub-path (relative)
|
||||
path.rMoveTo(val[k + 0], val[k + 1]);
|
||||
currentX += val[k + 0];
|
||||
currentY += val[k + 1];
|
||||
break;
|
||||
case 'M': // moveto - Start a new sub-path
|
||||
path.moveTo(val[k + 0], val[k + 1]);
|
||||
currentX = val[k + 0];
|
||||
currentY = val[k + 1];
|
||||
break;
|
||||
case 'l': // lineto - Draw a line from the current point (relative)
|
||||
path.rLineTo(val[k + 0], val[k + 1]);
|
||||
currentX += val[k + 0];
|
||||
currentY += val[k + 1];
|
||||
break;
|
||||
case 'L': // lineto - Draw a line from the current point
|
||||
path.lineTo(val[k + 0], val[k + 1]);
|
||||
currentX = val[k + 0];
|
||||
currentY = val[k + 1];
|
||||
break;
|
||||
case 'z': // closepath - Close the current subpath
|
||||
case 'Z': // closepath - Close the current subpath
|
||||
path.close();
|
||||
break;
|
||||
case 'h': // horizontal lineto - Draws a horizontal line (relative)
|
||||
path.rLineTo(val[k + 0], 0);
|
||||
currentX += val[k + 0];
|
||||
break;
|
||||
case 'H': // horizontal lineto - Draws a horizontal line
|
||||
path.lineTo(val[k + 0], currentY);
|
||||
currentX = val[k + 0];
|
||||
break;
|
||||
case 'v': // vertical lineto - Draws a vertical line from the current point (r)
|
||||
path.rLineTo(0, val[k + 0]);
|
||||
currentY += val[k + 0];
|
||||
break;
|
||||
case 'V': // vertical lineto - Draws a vertical line from the current point
|
||||
path.lineTo(currentX, val[k + 0]);
|
||||
currentY = val[k + 0];
|
||||
break;
|
||||
case 'c': // curveto - Draws a cubic Bézier curve (relative)
|
||||
path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
|
||||
val[k + 4], val[k + 5]);
|
||||
|
||||
ctrlPointX = currentX + val[k + 2];
|
||||
ctrlPointY = currentY + val[k + 3];
|
||||
currentX += val[k + 4];
|
||||
currentY += val[k + 5];
|
||||
|
||||
break;
|
||||
case 'C': // curveto - Draws a cubic Bézier curve
|
||||
path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
|
||||
val[k + 4], val[k + 5]);
|
||||
currentX = val[k + 4];
|
||||
currentY = val[k + 5];
|
||||
ctrlPointX = val[k + 2];
|
||||
ctrlPointY = val[k + 3];
|
||||
break;
|
||||
case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
|
||||
reflectiveCtrlPointX = 0;
|
||||
reflectiveCtrlPointY = 0;
|
||||
if (previousCmd == 'c' || previousCmd == 's'
|
||||
|| previousCmd == 'C' || previousCmd == 'S') {
|
||||
reflectiveCtrlPointX = currentX - ctrlPointX;
|
||||
reflectiveCtrlPointY = currentY - ctrlPointY;
|
||||
}
|
||||
path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
|
||||
val[k + 0], val[k + 1],
|
||||
val[k + 2], val[k + 3]);
|
||||
|
||||
ctrlPointX = currentX + val[k + 0];
|
||||
ctrlPointY = currentY + val[k + 1];
|
||||
currentX += val[k + 2];
|
||||
currentY += val[k + 3];
|
||||
break;
|
||||
case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
|
||||
reflectiveCtrlPointX = currentX;
|
||||
reflectiveCtrlPointY = currentY;
|
||||
if (previousCmd == 'c' || previousCmd == 's'
|
||||
|| previousCmd == 'C' || previousCmd == 'S') {
|
||||
reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
|
||||
reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
|
||||
}
|
||||
path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
|
||||
val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
|
||||
ctrlPointX = val[k + 0];
|
||||
ctrlPointY = val[k + 1];
|
||||
currentX = val[k + 2];
|
||||
currentY = val[k + 3];
|
||||
break;
|
||||
case 'q': // Draws a quadratic Bézier (relative)
|
||||
path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
|
||||
ctrlPointX = currentX + val[k + 0];
|
||||
ctrlPointY = currentY + val[k + 1];
|
||||
currentX += val[k + 2];
|
||||
currentY += val[k + 3];
|
||||
break;
|
||||
case 'Q': // Draws a quadratic Bézier
|
||||
path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
|
||||
ctrlPointX = val[k + 0];
|
||||
ctrlPointY = val[k + 1];
|
||||
currentX = val[k + 2];
|
||||
currentY = val[k + 3];
|
||||
break;
|
||||
case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
|
||||
reflectiveCtrlPointX = 0;
|
||||
reflectiveCtrlPointY = 0;
|
||||
if (previousCmd == 'q' || previousCmd == 't'
|
||||
|| previousCmd == 'Q' || previousCmd == 'T') {
|
||||
reflectiveCtrlPointX = currentX - ctrlPointX;
|
||||
reflectiveCtrlPointY = currentY - ctrlPointY;
|
||||
}
|
||||
path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
|
||||
val[k + 0], val[k + 1]);
|
||||
ctrlPointX = currentX + reflectiveCtrlPointX;
|
||||
ctrlPointY = currentY + reflectiveCtrlPointY;
|
||||
currentX += val[k + 0];
|
||||
currentY += val[k + 1];
|
||||
break;
|
||||
case 'T': // Draws a quadratic Bézier curve (reflective control point)
|
||||
reflectiveCtrlPointX = currentX;
|
||||
reflectiveCtrlPointY = currentY;
|
||||
if (previousCmd == 'q' || previousCmd == 't'
|
||||
|| previousCmd == 'Q' || previousCmd == 'T') {
|
||||
reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
|
||||
reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
|
||||
}
|
||||
path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
|
||||
val[k + 0], val[k + 1]);
|
||||
ctrlPointX = reflectiveCtrlPointX;
|
||||
ctrlPointY = reflectiveCtrlPointY;
|
||||
currentX = val[k + 0];
|
||||
currentY = val[k + 1];
|
||||
break;
|
||||
case 'a': // Draws an elliptical arc
|
||||
// (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
|
||||
drawArc(path,
|
||||
currentX,
|
||||
currentY,
|
||||
val[k + 5] + currentX,
|
||||
val[k + 6] + currentY,
|
||||
val[k + 0],
|
||||
val[k + 1],
|
||||
val[k + 2],
|
||||
val[k + 3] != 0,
|
||||
val[k + 4] != 0);
|
||||
currentX += val[k + 5];
|
||||
currentY += val[k + 6];
|
||||
ctrlPointX = currentX;
|
||||
ctrlPointY = currentY;
|
||||
break;
|
||||
case 'A': // Draws an elliptical arc
|
||||
drawArc(path,
|
||||
currentX,
|
||||
currentY,
|
||||
val[k + 5],
|
||||
val[k + 6],
|
||||
val[k + 0],
|
||||
val[k + 1],
|
||||
val[k + 2],
|
||||
val[k + 3] != 0,
|
||||
val[k + 4] != 0);
|
||||
currentX = val[k + 5];
|
||||
currentY = val[k + 6];
|
||||
ctrlPointX = currentX;
|
||||
ctrlPointY = currentY;
|
||||
break;
|
||||
}
|
||||
previousCmd = cmd;
|
||||
}
|
||||
current[0] = currentX;
|
||||
current[1] = currentY;
|
||||
current[2] = ctrlPointX;
|
||||
current[3] = ctrlPointY;
|
||||
}
|
||||
|
||||
private static void drawArc(Path p,
|
||||
float x0,
|
||||
float y0,
|
||||
float x1,
|
||||
float y1,
|
||||
float a,
|
||||
float b,
|
||||
float theta,
|
||||
boolean isMoreThanHalf,
|
||||
boolean isPositiveArc) {
|
||||
|
||||
/* Convert rotation angle from degrees to radians */
|
||||
double thetaD = Math.toRadians(theta);
|
||||
/* Pre-compute rotation matrix entries */
|
||||
double cosTheta = Math.cos(thetaD);
|
||||
double sinTheta = Math.sin(thetaD);
|
||||
/* Transform (x0, y0) and (x1, y1) into unit space */
|
||||
/* using (inverse) rotation, followed by (inverse) scale */
|
||||
double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
|
||||
double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
|
||||
double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
|
||||
double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
|
||||
|
||||
/* Compute differences and averages */
|
||||
double dx = x0p - x1p;
|
||||
double dy = y0p - y1p;
|
||||
double xm = (x0p + x1p) / 2;
|
||||
double ym = (y0p + y1p) / 2;
|
||||
/* Solve for intersecting unit circles */
|
||||
double dsq = dx * dx + dy * dy;
|
||||
if (dsq == 0.0) {
|
||||
Log.w(LOGTAG, " Points are coincident");
|
||||
return; /* Points are coincident */
|
||||
}
|
||||
double disc = 1.0 / dsq - 1.0 / 4.0;
|
||||
if (disc < 0.0) {
|
||||
Log.w(LOGTAG, "Points are too far apart " + dsq);
|
||||
float adjust = (float) (Math.sqrt(dsq) / 1.99999);
|
||||
drawArc(p, x0, y0, x1, y1, a * adjust,
|
||||
b * adjust, theta, isMoreThanHalf, isPositiveArc);
|
||||
return; /* Points are too far apart */
|
||||
}
|
||||
double s = Math.sqrt(disc);
|
||||
double sdx = s * dx;
|
||||
double sdy = s * dy;
|
||||
double cx;
|
||||
double cy;
|
||||
if (isMoreThanHalf == isPositiveArc) {
|
||||
cx = xm - sdy;
|
||||
cy = ym + sdx;
|
||||
} else {
|
||||
cx = xm + sdy;
|
||||
cy = ym - sdx;
|
||||
}
|
||||
|
||||
double eta0 = Math.atan2((y0p - cy), (x0p - cx));
|
||||
|
||||
double eta1 = Math.atan2((y1p - cy), (x1p - cx));
|
||||
|
||||
double sweep = (eta1 - eta0);
|
||||
if (isPositiveArc != (sweep >= 0)) {
|
||||
if (sweep > 0) {
|
||||
sweep -= 2 * Math.PI;
|
||||
} else {
|
||||
sweep += 2 * Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
cx *= a;
|
||||
cy *= b;
|
||||
double tcx = cx;
|
||||
cx = cx * cosTheta - cy * sinTheta;
|
||||
cy = tcx * sinTheta + cy * cosTheta;
|
||||
|
||||
arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an arc to cubic Bezier segments and records them in p.
|
||||
*
|
||||
* @param p The target for the cubic Bezier segments
|
||||
* @param cx The x coordinate center of the ellipse
|
||||
* @param cy The y coordinate center of the ellipse
|
||||
* @param a The radius of the ellipse in the horizontal direction
|
||||
* @param b The radius of the ellipse in the vertical direction
|
||||
* @param e1x E(eta1) x coordinate of the starting point of the arc
|
||||
* @param e1y E(eta2) y coordinate of the starting point of the arc
|
||||
* @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
|
||||
* @param start The start angle of the arc on the ellipse
|
||||
* @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
|
||||
*/
|
||||
private static void arcToBezier(Path p,
|
||||
double cx,
|
||||
double cy,
|
||||
double a,
|
||||
double b,
|
||||
double e1x,
|
||||
double e1y,
|
||||
double theta,
|
||||
double start,
|
||||
double sweep) {
|
||||
// Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
|
||||
// and http://www.spaceroots.org/documents/ellipse/node22.html
|
||||
|
||||
// Maximum of 45 degrees per cubic Bezier segment
|
||||
int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI));
|
||||
|
||||
double eta1 = start;
|
||||
double cosTheta = Math.cos(theta);
|
||||
double sinTheta = Math.sin(theta);
|
||||
double cosEta1 = Math.cos(eta1);
|
||||
double sinEta1 = Math.sin(eta1);
|
||||
double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
|
||||
double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
|
||||
|
||||
double anglePerSegment = sweep / numSegments;
|
||||
for (int i = 0; i < numSegments; i++) {
|
||||
double eta2 = eta1 + anglePerSegment;
|
||||
double sinEta2 = Math.sin(eta2);
|
||||
double cosEta2 = Math.cos(eta2);
|
||||
double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
|
||||
double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
|
||||
double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
|
||||
double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
|
||||
double tanDiff2 = Math.tan((eta2 - eta1) / 2);
|
||||
double alpha =
|
||||
Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
|
||||
double q1x = e1x + alpha * ep1x;
|
||||
double q1y = e1y + alpha * ep1y;
|
||||
double q2x = e2x - alpha * ep2x;
|
||||
double q2y = e2y - alpha * ep2y;
|
||||
|
||||
p.cubicTo((float) q1x,
|
||||
(float) q1y,
|
||||
(float) q2x,
|
||||
(float) q2y,
|
||||
(float) e2x,
|
||||
(float) e2y);
|
||||
eta1 = eta2;
|
||||
e1x = e2x;
|
||||
e1y = e2y;
|
||||
ep1x = ep2x;
|
||||
ep1y = ep2y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user