From f4389afb30a537a93a9f8f0bc36a66d65cfb7820 Mon Sep 17 00:00:00 2001 From: seven332 Date: Sun, 15 Mar 2015 23:30:59 +0800 Subject: [PATCH] Add vector support --- .../android/content/res/VectorResources.java | 219 +++ .../com/hippo/animation/AnimatorInflater.java | 611 +++++++ .../hippo/animation/InterpolatorInflater.java | 125 ++ .../com/hippo/animation/PathInterpolator.java | 434 +++++ .../java/com/hippo/content/VectorContext.java | 51 + .../drawable/AnimatedVectorDrawable.java | 430 +++++ .../com/hippo/drawable/VectorDrawable.java | 1399 +++++++++++++++++ .../hippo/ehviewer/ui/ContentActivity.java | 7 + .../ehviewer/ui/scene/GalleryListScene.java | 2 +- .../java/com/hippo/util/DrawableUtils.java | 58 + .../main/java/com/hippo/util/PathParser.java | 651 ++++++++ app/src/main/res/drawable/ic_vector_happy.xml | 39 + app/src/main/res/layout/fragment_list.xml | 30 + app/src/main/res/values/attrs.xml | 241 +++ app/src/main/res/values/colors.xml | 6 +- app/src/main/res/values/pathdata.xml | 18 + 16 files changed, 4317 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/android/content/res/VectorResources.java create mode 100644 app/src/main/java/com/hippo/animation/AnimatorInflater.java create mode 100644 app/src/main/java/com/hippo/animation/InterpolatorInflater.java create mode 100644 app/src/main/java/com/hippo/animation/PathInterpolator.java create mode 100644 app/src/main/java/com/hippo/content/VectorContext.java create mode 100644 app/src/main/java/com/hippo/drawable/AnimatedVectorDrawable.java create mode 100644 app/src/main/java/com/hippo/drawable/VectorDrawable.java create mode 100644 app/src/main/java/com/hippo/util/DrawableUtils.java create mode 100644 app/src/main/java/com/hippo/util/PathParser.java create mode 100644 app/src/main/res/drawable/ic_vector_happy.xml create mode 100644 app/src/main/res/layout/fragment_list.xml create mode 100644 app/src/main/res/values/pathdata.xml diff --git a/app/src/main/java/android/content/res/VectorResources.java b/app/src/main/java/android/content/res/VectorResources.java new file mode 100644 index 000000000..c9275fc34 --- /dev/null +++ b/app/src/main/java/android/content/res/VectorResources.java @@ -0,0 +1,219 @@ +/* + * 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 android.content.res; + +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.v4.util.LongSparseArray; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.util.Xml; + +import com.hippo.drawable.AnimatedVectorDrawable; +import com.hippo.drawable.VectorDrawable; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; + +/** + * It is a wrap + */ +public class VectorResources extends Resources { + + private static final String TAG = VectorResources.class.getSimpleName(); + + private Context mContext; + private Resources mBase; + + // These are protected by mAccessLock. + + /*package*/ final Object mAccessLock = new Object(); + /*package*/ final LongSparseArray > mDrawableCache + = new LongSparseArray >(0); + /*package*/ final LongSparseArray > mColorDrawableCache + = new LongSparseArray >(0); + + public VectorResources(Context context, Resources res) { + super(res.getAssets(), res.getDisplayMetrics(), res.getConfiguration()); + mContext = context; + mBase = res; + } + + public boolean isBase(Resources res) { + return mBase == res; + } + + @Override + Drawable loadDrawable(TypedValue value, int id) + throws NotFoundException { + boolean isColorDrawable = false; + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + isColorDrawable = true; + } + final long key = isColorDrawable ? value.data : + (((long) value.assetCookie) << 32) | value.data; + + Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); + + if (dr != null) { + return dr; + } + + if (isColorDrawable) { + dr = new ColorDrawable(value.data); + } + + if (dr == null) { + if (value.string == null) { + throw new NotFoundException( + "Resource is not a Drawable (color or path): " + value); + } + + String file = value.string.toString(); + + if (file.endsWith(".xml")) { + // Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); + try { + XmlResourceParser rp = getXml(id); + // XmlResourceParser rp = loadXmlResourceParser( + // file, id, value.assetCookie, "drawable"); + dr = createDrawableFromXml(rp); + rp.close(); + } catch (Exception e) { + // Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + // Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + + } else { + // Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); + try { + InputStream is = openRawResource(id, value); + //InputStream is = mAssets.openNonAsset( + // value.assetCookie, file, AssetManager.ACCESS_STREAMING); + // System.out.println("Opened file " + file + ": " + is); + dr = Drawable.createFromResourceStream(this, value, is, + file, null); + is.close(); + // System.out.println("Created stream: " + dr); + } catch (Exception e) { + // Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + // Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + Drawable.ConstantState cs; + if (dr != null) { + dr.setChangingConfigurations(value.changingConfigurations); + cs = dr.getConstantState(); + if (cs != null) { + synchronized (mAccessLock) { + //Log.i(TAG, "Saving cached drawable @ #" + + // Integer.toHexString(key.intValue()) + // + " in " + this + ": " + cs); + if (isColorDrawable) { + mColorDrawableCache.put(key, new WeakReference(cs)); + } else { + mDrawableCache.put(key, new WeakReference(cs)); + } + } + } + } + + return dr; + } + + private Drawable getCachedDrawable( + LongSparseArray> drawableCache, + long key) { + synchronized (mAccessLock) { + WeakReference wr = drawableCache.get(key); + if (wr != null) { // we have the key + Drawable.ConstantState entry = wr.get(); + if (entry != null) { + //Log.i(TAG, "Returning cached drawable @ #" + + // Integer.toHexString(((Integer)key).intValue()) + // + " in " + this + ": " + entry); + return entry.newDrawable(this); + } + else { // our entry has been purged + drawableCache.delete(key); + } + } + } + return null; + } + + private Drawable createDrawableFromXml(XmlResourceParser parser) + throws XmlPullParserException, IOException { + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty loop + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + Drawable drawable = createDrawableFromXmlInner(parser, attrs); + + if (drawable == null) { + throw new RuntimeException("Unknown initial tag: " + parser.getName()); + } + + return drawable; + } + + private Drawable createDrawableFromXmlInner(XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + final Drawable drawable; + final String name = parser.getName(); + switch (name) { + case "vector": + VectorDrawable vectorDrawable = new VectorDrawable(); + vectorDrawable.inflate(this, parser, attrs); + drawable = vectorDrawable; + break; + case "animated-vector": + AnimatedVectorDrawable animatedVectorDrawable = new AnimatedVectorDrawable(); + animatedVectorDrawable.inflate(mContext, parser, attrs); + drawable = animatedVectorDrawable; + break; + default: + drawable = Drawable.createFromXmlInner(this, parser, attrs); + } + return drawable; + } + +} diff --git a/app/src/main/java/com/hippo/animation/AnimatorInflater.java b/app/src/main/java/com/hippo/animation/AnimatorInflater.java new file mode 100644 index 000000000..77593530e --- /dev/null +++ b/app/src/main/java/com/hippo/animation/AnimatorInflater.java @@ -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. + *

+ * 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. + * something 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 { + 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 PathParser.PathDataNode[] will be allocated. + */ + private PathDataEvaluator() {} + + /** + * Create a PathDataEvaluator that reuses nodeArray 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 evaluate() call. + * + * @param nodeArray The array to modify and return from evaluate. + */ + 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 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; + } +} diff --git a/app/src/main/java/com/hippo/animation/InterpolatorInflater.java b/app/src/main/java/com/hippo/animation/InterpolatorInflater.java new file mode 100644 index 000000000..905ef3f9a --- /dev/null +++ b/app/src/main/java/com/hippo/animation/InterpolatorInflater.java @@ -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; + } +} diff --git a/app/src/main/java/com/hippo/animation/PathInterpolator.java b/app/src/main/java/com/hippo/animation/PathInterpolator.java new file mode 100644 index 000000000..6a42a6ff3 --- /dev/null +++ b/app/src/main/java/com/hippo/animation/PathInterpolator.java @@ -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 Point + * (0, 0) to (1, 1). The x coordinate along the Path + * 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 y = f(x). + * + *

The Path 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:

+ *

+ *     Path path = new Path();
+ *     path.lineTo(0.25f, 0.25f);
+ *     path.moveTo(0.25f, 0.5f);
+ *     path.lineTo(1f, 1f);
+ * 

+ */ +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 Path. The Path + * must begin at (0, 0) and end at (1, 1). + * + * @param path The Path 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 + * (0, 0) and (1, 1) 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 + * (0, 0) and (1, 1) 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 + * y = f(x), finds the y coordinate of the line given t + * 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 = t. + * @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; + } + } +} diff --git a/app/src/main/java/com/hippo/content/VectorContext.java b/app/src/main/java/com/hippo/content/VectorContext.java new file mode 100644 index 000000000..682a9c834 --- /dev/null +++ b/app/src/main/java/com/hippo/content/VectorContext.java @@ -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 super.attachBaseContext(Vector.wrapContext(newBase));. + * + * @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; + } +} diff --git a/app/src/main/java/com/hippo/drawable/AnimatedVectorDrawable.java b/app/src/main/java/com/hippo/drawable/AnimatedVectorDrawable.java new file mode 100644 index 000000000..1273e62b8 --- /dev/null +++ b/app/src/main/java/com/hippo/drawable/AnimatedVectorDrawable.java @@ -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. + *

+ * AnimatedVectorDrawable are normally defined as 3 separate XML files. + *

+ *

+ * 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. + *

+ *
  • Here is a simple VectorDrawable in this vectordrawable.xml file. + *
    + * <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>
    + * 
  • + *

    + * 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. + *

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

    + * 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. + *

    + *
  • Here is the rotation.xml, which will rotate the target group for 360 degrees. + *
    + * <objectAnimator
    + *     android:duration="6000"
    + *     android:propertyName="rotation"
    + *     android:valueFrom="0"
    + *     android:valueTo="360" />
    + * 
  • + *
  • 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. + *
    + * <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>
    + * 
  • + * + * @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 mAnimators; + ArrayMap 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 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 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 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 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. + *

    + * NOTE: Only works of all animations are ValueAnimators. + * @hide + */ + /* + public void reverse() { + final ArrayList 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 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; + } + */ +} diff --git a/app/src/main/java/com/hippo/drawable/VectorDrawable.java b/app/src/main/java/com/hippo/drawable/VectorDrawable.java new file mode 100644 index 000000000..155202b7d --- /dev/null +++ b/app/src/main/java/com/hippo/drawable/VectorDrawable.java @@ -0,0 +1,1399 @@ +/* + * 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.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import com.hippo.ehviewer.R; +import com.hippo.util.DrawableUtils; +import com.hippo.util.PathParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Stack; + +/** + * This lets you create a drawable based on an XML vector graphic. It can be + * defined in an XML file with the <vector> element. + *

    + * The vector drawable has the following elements: + *

    + *

    <vector>
    + *
    + *
    Used to defined a vector drawable + *
    + *
    android:name
    + *
    Defines the name of this vector drawable.
    + *
    android:width
    + *
    Used to defined the intrinsic width of the drawable. + * This support all the dimension units, normally specified with dp.
    + *
    android:height
    + *
    Used to defined the intrinsic height the drawable. + * This support all the dimension units, normally specified with dp.
    + *
    android:viewportWidth
    + *
    Used to defined the width of the viewport space. Viewport is basically + * the virtual canvas where the paths are drawn on.
    + *
    android:viewportHeight
    + *
    Used to defined the height of the viewport space. Viewport is basically + * the virtual canvas where the paths are drawn on.
    + *
    android:tint
    + *
    The color to apply to the drawable as a tint. By default, no tint is applied.
    + *
    android:tintMode
    + *
    The Porter-Duff blending mode for the tint color. The default value is src_in.
    + *
    android:autoMirrored
    + *
    Indicates if the drawable needs to be mirrored when its layout direction is + * RTL (right-to-left).
    + *
    android:alpha
    + *
    The opacity of this drawable.
    + *
    + *
    + * + *
    + *
    <group>
    + *
    Defines a group of paths or subgroups, plus transformation information. + * The transformations are defined in the same coordinates as the viewport. + * And the transformations are applied in the order of scale, rotate then translate. + *
    + *
    android:name
    + *
    Defines the name of the group.
    + *
    android:rotation
    + *
    The degrees of rotation of the group.
    + *
    android:pivotX
    + *
    The X coordinate of the pivot for the scale and rotation of the group. + * This is defined in the viewport space.
    + *
    android:pivotY
    + *
    The Y coordinate of the pivot for the scale and rotation of the group. + * This is defined in the viewport space.
    + *
    android:scaleX
    + *
    The amount of scale on the X Coordinate.
    + *
    android:scaleY
    + *
    The amount of scale on the Y coordinate.
    + *
    android:translateX
    + *
    The amount of translation on the X coordinate. + * This is defined in the viewport space.
    + *
    android:translateY
    + *
    The amount of translation on the Y coordinate. + * This is defined in the viewport space.
    + *
    + *
    + * + *
    + *
    <path>
    + *
    Defines paths to be drawn. + *
    + *
    android:name
    + *
    Defines the name of the path.
    + *
    android:pathData
    + *
    Defines path string. This is using exactly same format as "d" attribute + * in the SVG's path data. This is defined in the viewport space.
    + *
    android:fillColor
    + *
    Defines the color to fill the path (none if not present).
    + *
    android:strokeColor
    + *
    Defines the color to draw the path outline (none if not present).
    + *
    android:strokeWidth
    + *
    The width a path stroke.
    + *
    android:strokeAlpha
    + *
    The opacity of a path stroke.
    + *
    android:fillAlpha
    + *
    The opacity to fill the path with.
    + *
    android:trimPathStart
    + *
    The fraction of the path to trim from the start, in the range from 0 to 1.
    + *
    android:trimPathEnd
    + *
    The fraction of the path to trim from the end, in the range from 0 to 1.
    + *
    android:trimPathOffset
    + *
    Shift trim region (allows showed region to include the start and end), in the range + * from 0 to 1.
    + *
    android:strokeLineCap
    + *
    Sets the linecap for a stroked path: butt, round, square.
    + *
    android:strokeLineJoin
    + *
    Sets the lineJoin for a stroked path: miter,round,bevel.
    + *
    android:strokeMiterLimit
    + *
    Sets the Miter limit for a stroked path.
    + *
    + *
    + * + *
    + *
    <clip-path>
    + *
    Defines path to be the current clip. + *
    + *
    android:name
    + *
    Defines the name of the clip path.
    + *
    android:pathData
    + *
    Defines clip path string. This is using exactly same format as "d" attribute + * in the SVG's path data.
    + *
    + *
    + *
  • Here is a simple VectorDrawable in this vectordrawable.xml file. + *
    + * <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>
    + * 
  • + */ + +public class VectorDrawable extends Drawable { + private static final String LOGTAG = VectorDrawable.class.getSimpleName(); + + private static final String SHAPE_CLIP_PATH = "clip-path"; + private static final String SHAPE_GROUP = "group"; + private static final String SHAPE_PATH = "path"; + private static final String SHAPE_VECTOR = "vector"; + + private static final int LINECAP_BUTT = 0; + private static final int LINECAP_ROUND = 1; + private static final int LINECAP_SQUARE = 2; + + private static final int LINEJOIN_MITER = 0; + private static final int LINEJOIN_ROUND = 1; + private static final int LINEJOIN_BEVEL = 2; + + private static final boolean DBG_VECTOR_DRAWABLE = false; + + private VectorDrawableState mVectorState; + + private PorterDuffColorFilter mTintFilter; + private ColorFilter mColorFilter; + + private boolean mMutated; + + // AnimatedVectorDrawable needs to turn off the cache all the time, otherwise, + // caching the bitmap by default is allowed. + private boolean mAllowCaching = true; + + public VectorDrawable() { + mVectorState = new VectorDrawableState(); + } + + private VectorDrawable(@NonNull VectorDrawableState state, Resources res) { + mVectorState = state; + mTintFilter = DrawableUtils.updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mVectorState = new VectorDrawableState(mVectorState); + mMutated = true; + } + return this; + } + + Object getTargetByName(String name) { + return mVectorState.mVPathRenderer.mVGTargetsMap.get(name); + } + + @Override + public ConstantState getConstantState() { + mVectorState.mChangingConfigurations = getChangingConfigurations(); + return mVectorState; + } + + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + if (bounds.width() == 0 || bounds.height() == 0) { + // too small to draw + return; + } + + final int saveCount = canvas.save(); + final boolean needMirroring = needMirroring(); + + canvas.translate(bounds.left, bounds.top); + if (needMirroring) { + canvas.translate(bounds.width(), 0); + canvas.scale(-1.0f, 1.0f); + } + + // Color filters always override tint filters. + final ColorFilter colorFilter = mColorFilter == null ? mTintFilter : mColorFilter; + + if (!mAllowCaching) { + // AnimatedVectorDrawable + if (!mVectorState.hasTranslucentRoot()) { + mVectorState.mVPathRenderer.draw( + canvas, bounds.width(), bounds.height(), colorFilter); + } else { + mVectorState.createCachedBitmapIfNeeded(bounds); + mVectorState.updateCachedBitmap(bounds); + mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter); + } + } else { + // Static Vector Drawable case. + mVectorState.createCachedBitmapIfNeeded(bounds); + if (!mVectorState.canReuseCache()) { + mVectorState.updateCachedBitmap(bounds); + mVectorState.updateCacheStates(); + } + mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter); + } + + canvas.restoreToCount(saveCount); + } + + @Override + public int getAlpha() { + return mVectorState.mVPathRenderer.getRootAlpha(); + } + + @Override + public void setAlpha(int alpha) { + if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) { + mVectorState.mVPathRenderer.setRootAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mColorFilter = colorFilter; + invalidateSelf(); + } + + @Override + public void setTintList(ColorStateList tint) { + final VectorDrawableState state = mVectorState; + if (state.mTint != tint) { + state.mTint = tint; + mTintFilter = DrawableUtils.updateTintFilter(this, mTintFilter, tint, state.mTintMode); + invalidateSelf(); + } + } + + @Override + public void setTintMode(Mode tintMode) { + final VectorDrawableState state = mVectorState; + if (state.mTintMode != tintMode) { + state.mTintMode = tintMode; + mTintFilter = DrawableUtils.updateTintFilter(this, mTintFilter, state.mTint, tintMode); + invalidateSelf(); + } + } + + @Override + public boolean isStateful() { + return super.isStateful() || (mVectorState != null && mVectorState.mTint != null + && mVectorState.mTint.isStateful()); + } + + @Override + protected boolean onStateChange(int[] stateSet) { + final VectorDrawableState state = mVectorState; + if (state.mTint != null && state.mTintMode != null) { + mTintFilter = DrawableUtils.updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + invalidateSelf(); + return true; + } + return false; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getIntrinsicWidth() { + return (int) mVectorState.mVPathRenderer.mBaseWidth; + } + + @Override + public int getIntrinsicHeight() { + return (int) mVectorState.mVPathRenderer.mBaseHeight; + } + + /** + * The size of a pixel when scaled from the intrinsic dimension to the viewport dimension. + * This is used to calculate the path animation accuracy. + */ + public float getPixelSize() { + if (mVectorState == null || mVectorState.mVPathRenderer == null || + mVectorState.mVPathRenderer.mBaseWidth == 0 || + mVectorState.mVPathRenderer.mBaseHeight == 0 || + mVectorState.mVPathRenderer.mViewportHeight == 0 || + mVectorState.mVPathRenderer.mViewportWidth == 0) { + return 1; // fall back to 1:1 pixel mapping. + } + float intrinsicWidth = mVectorState.mVPathRenderer.mBaseWidth; + float intrinsicHeight = mVectorState.mVPathRenderer.mBaseHeight; + float viewportWidth = mVectorState.mVPathRenderer.mViewportWidth; + float viewportHeight = mVectorState.mVPathRenderer.mViewportHeight; + float scaleX = viewportWidth / intrinsicWidth; + float scaleY = viewportHeight / intrinsicHeight; + return Math.min(scaleX, scaleY); + } + + /** @hide */ + public static VectorDrawable create(Resources resources, int rid) { + try { + final XmlPullParser parser = resources.getXml(rid); + final AttributeSet attrs = Xml.asAttributeSet(parser); + int type; + while ((type=parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty loop + } + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + final VectorDrawable drawable = new VectorDrawable(); + drawable.inflate(resources, parser, attrs); + + return drawable; + } catch (XmlPullParserException e) { + Log.e(LOGTAG, "parser error", e); + } catch (IOException e) { + Log.e(LOGTAG, "parser error", e); + } + return null; + } + + private static int applyAlpha(int color, float alpha) { + int alphaBytes = Color.alpha(color); + color &= 0x00FFFFFF; + color |= ((int) (alphaBytes * alpha)) << 24; + return color; + } + + @Override + public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = new VPathRenderer(); + state.mVPathRenderer = pathRenderer; + + final TypedArray a = res.obtainAttributes(attrs, R.styleable.VectorDrawable); + updateStateFromTypedArray(a); + a.recycle(); + + state.mCacheDirty = true; + inflateInternal(res, parser, attrs); + + mTintFilter = DrawableUtils.updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + } + + private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = state.mVPathRenderer; + + // Account for any configuration changes. + state.mChangingConfigurations |= 0; //a.getChangingConfigurations(); + + final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1); + if (tintMode != -1) { + state.mTintMode = DrawableUtils.parseTintMode(tintMode, Mode.SRC_IN); + } + + final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint); + if (tint != null) { + state.mTint = tint; + } + + state.mAutoMirrored = a.getBoolean( + R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored); + + pathRenderer.mViewportWidth = a.getFloat( + R.styleable.VectorDrawable_viewportWidth, pathRenderer.mViewportWidth); + pathRenderer.mViewportHeight = a.getFloat( + R.styleable.VectorDrawable_viewportHeight, pathRenderer.mViewportHeight); + + if (pathRenderer.mViewportWidth <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires viewportWidth > 0"); + } else if (pathRenderer.mViewportHeight <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires viewportHeight > 0"); + } + + pathRenderer.mBaseWidth = a.getDimension( + R.styleable.VectorDrawable_width, pathRenderer.mBaseWidth); + pathRenderer.mBaseHeight = a.getDimension( + R.styleable.VectorDrawable_height, pathRenderer.mBaseHeight); + + if (pathRenderer.mBaseWidth <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires width > 0"); + } else if (pathRenderer.mBaseHeight <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires height > 0"); + } + + final float alphaInFloat = a.getFloat(R.styleable.VectorDrawable_alpha, + pathRenderer.getAlpha()); + pathRenderer.setAlpha(alphaInFloat); + + final String name = a.getString(R.styleable.VectorDrawable_name); + if (name != null) { + pathRenderer.mRootName = name; + pathRenderer.mVGTargetsMap.put(name, pathRenderer); + } + } + + private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = state.mVPathRenderer; + boolean noPathTag = true; + + // Use a stack to help to build the group tree. + // The top of the stack is always the current group. + final Stack groupStack = new Stack(); + groupStack.push(pathRenderer.mRootGroup); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + final String tagName = parser.getName(); + final VGroup currentGroup = groupStack.peek(); + + if (SHAPE_PATH.equals(tagName)) { + final VFullPath path = new VFullPath(); + path.inflate(res, attrs); + currentGroup.mChildren.add(path); + if (path.getPathName() != null) { + pathRenderer.mVGTargetsMap.put(path.getPathName(), path); + } + noPathTag = false; + state.mChangingConfigurations |= path.mChangingConfigurations; + } else if (SHAPE_CLIP_PATH.equals(tagName)) { + final VClipPath path = new VClipPath(); + path.inflate(res, attrs); + currentGroup.mChildren.add(path); + if (path.getPathName() != null) { + pathRenderer.mVGTargetsMap.put(path.getPathName(), path); + } + state.mChangingConfigurations |= path.mChangingConfigurations; + } else if (SHAPE_GROUP.equals(tagName)) { + VGroup newChildGroup = new VGroup(); + newChildGroup.inflate(res, attrs); + currentGroup.mChildren.add(newChildGroup); + groupStack.push(newChildGroup); + if (newChildGroup.getGroupName() != null) { + pathRenderer.mVGTargetsMap.put(newChildGroup.getGroupName(), + newChildGroup); + } + state.mChangingConfigurations |= newChildGroup.mChangingConfigurations; + } + } else if (eventType == XmlPullParser.END_TAG) { + final String tagName = parser.getName(); + if (SHAPE_GROUP.equals(tagName)) { + groupStack.pop(); + } + } + eventType = parser.next(); + } + + // Print the tree out for debug. + if (DBG_VECTOR_DRAWABLE) { + printGroupTree(pathRenderer.mRootGroup, 0); + } + + if (noPathTag) { + final StringBuffer tag = new StringBuffer(); + + if (tag.length() > 0) { + tag.append(" or "); + } + tag.append(SHAPE_PATH); + + throw new XmlPullParserException("no " + tag + " defined"); + } + } + + private void printGroupTree(VGroup currentGroup, int level) { + String indent = ""; + for (int i = 0; i < level; i++) { + indent += " "; + } + // Print the current node + Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName() + + " rotation is " + currentGroup.mRotate); + Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString()); + // Then print all the children groups + for (int i = 0; i < currentGroup.mChildren.size(); i++) { + Object child = currentGroup.mChildren.get(i); + if (child instanceof VGroup) { + printGroupTree((VGroup) child, level + 1); + } + } + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() | mVectorState.mChangingConfigurations; + } + + void setAllowCaching(boolean allowCaching) { + mAllowCaching = allowCaching; + } + + private boolean needMirroring() { + return isAutoMirrored(); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + if (mVectorState.mAutoMirrored != mirrored) { + mVectorState.mAutoMirrored = mirrored; + invalidateSelf(); + } + } + + @Override + public boolean isAutoMirrored() { + return mVectorState.mAutoMirrored; + } + + private static class VectorDrawableState extends ConstantState { + int mChangingConfigurations; + VPathRenderer mVPathRenderer; + ColorStateList mTint = null; + Mode mTintMode = DrawableUtils.DEFAULT_TINT_MODE; + boolean mAutoMirrored; + + Bitmap mCachedBitmap; + ColorStateList mCachedTint; + Mode mCachedTintMode; + int mCachedRootAlpha; + boolean mCachedAutoMirrored; + boolean mCacheDirty; + + /** Temporary paint object used to draw cached bitmaps. */ + Paint mTempPaint; + + // Deep copy for mutate() or implicitly mutate. + public VectorDrawableState(VectorDrawableState copy) { + if (copy != null) { + mChangingConfigurations = copy.mChangingConfigurations; + mVPathRenderer = new VPathRenderer(copy.mVPathRenderer); + if (copy.mVPathRenderer.mFillPaint != null) { + mVPathRenderer.mFillPaint = new Paint(copy.mVPathRenderer.mFillPaint); + } + if (copy.mVPathRenderer.mStrokePaint != null) { + mVPathRenderer.mStrokePaint = new Paint(copy.mVPathRenderer.mStrokePaint); + } + mTint = copy.mTint; + mTintMode = copy.mTintMode; + mAutoMirrored = copy.mAutoMirrored; + } + } + + public void drawCachedBitmapWithRootAlpha(Canvas canvas, ColorFilter filter) { + // The bitmap's size is the same as the bounds. + final Paint p = getPaint(filter); + canvas.drawBitmap(mCachedBitmap, 0, 0, p); + } + + public boolean hasTranslucentRoot() { + return mVPathRenderer.getRootAlpha() < 255; + } + + /** + * @return null when there is no need for alpha paint. + */ + public Paint getPaint(ColorFilter filter) { + if (!hasTranslucentRoot() && filter == null) { + return null; + } + + if (mTempPaint == null) { + mTempPaint = new Paint(); + mTempPaint.setFilterBitmap(true); + } + mTempPaint.setAlpha(mVPathRenderer.getRootAlpha()); + mTempPaint.setColorFilter(filter); + return mTempPaint; + } + + public void updateCachedBitmap(Rect bounds) { + mCachedBitmap.eraseColor(Color.TRANSPARENT); + Canvas tmpCanvas = new Canvas(mCachedBitmap); + mVPathRenderer.draw(tmpCanvas, bounds.width(), bounds.height(), null); + } + + public void createCachedBitmapIfNeeded(Rect bounds) { + if (mCachedBitmap == null || !canReuseBitmap(bounds.width(), + bounds.height())) { + mCachedBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), + Bitmap.Config.ARGB_8888); + mCacheDirty = true; + } + + } + + public boolean canReuseBitmap(int width, int height) { + if (width == mCachedBitmap.getWidth() + && height == mCachedBitmap.getHeight()) { + return true; + } + return false; + } + + public boolean canReuseCache() { + if (!mCacheDirty + && mCachedTint == mTint + && mCachedTintMode == mTintMode + && mCachedAutoMirrored == mAutoMirrored + && mCachedRootAlpha == mVPathRenderer.getRootAlpha()) { + return true; + } + return false; + } + + public void updateCacheStates() { + // Use shallow copy here and shallow comparison in canReuseCache(), + // likely hit cache miss more, but practically not much difference. + mCachedTint = mTint; + mCachedTintMode = mTintMode; + mCachedRootAlpha = mVPathRenderer.getRootAlpha(); + mCachedAutoMirrored = mAutoMirrored; + mCacheDirty = false; + } + + public VectorDrawableState() { + mVPathRenderer = new VPathRenderer(); + } + + @Override + public Drawable newDrawable() { + return new VectorDrawable(this, null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new VectorDrawable(this, res); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations; + } + } + + private static class VPathRenderer { + /* Right now the internal data structure is organized as a tree. + * Each node can be a group node, or a path. + * A group node can have groups or paths as children, but a path node has + * no children. + * One example can be: + * Root Group + * / | \ + * Group Path Group + * / \ | + * Path Path Path + * + */ + // Variables that only used temporarily inside the draw() call, so there + // is no need for deep copying. + private final Path mPath; + private final Path mRenderPath; + private static final Matrix IDENTITY_MATRIX = new Matrix(); + private final Matrix mFinalPathMatrix = new Matrix(); + + private Paint mStrokePaint; + private Paint mFillPaint; + private PathMeasure mPathMeasure; + + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + private int mChangingConfigurations; + private final VGroup mRootGroup; + float mBaseWidth = 0; + float mBaseHeight = 0; + float mViewportWidth = 0; + float mViewportHeight = 0; + int mRootAlpha = 0xFF; + String mRootName = null; + + final ArrayMap mVGTargetsMap = new ArrayMap(); + + public VPathRenderer() { + mRootGroup = new VGroup(); + mPath = new Path(); + mRenderPath = new Path(); + } + + public void setRootAlpha(int alpha) { + mRootAlpha = alpha; + } + + public int getRootAlpha() { + return mRootAlpha; + } + + // setAlpha() and getAlpha() are used mostly for animation purpose, since + // Animator like to use alpha from 0 to 1. + public void setAlpha(float alpha) { + setRootAlpha((int) (alpha * 255)); + } + + @SuppressWarnings("unused") + public float getAlpha() { + return getRootAlpha() / 255.0f; + } + + public VPathRenderer(VPathRenderer copy) { + mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap); + mPath = new Path(copy.mPath); + mRenderPath = new Path(copy.mRenderPath); + mBaseWidth = copy.mBaseWidth; + mBaseHeight = copy.mBaseHeight; + mViewportWidth = copy.mViewportWidth; + mViewportHeight = copy.mViewportHeight; + mChangingConfigurations = copy.mChangingConfigurations; + mRootAlpha = copy.mRootAlpha; + mRootName = copy.mRootName; + if (copy.mRootName != null) { + mVGTargetsMap.put(copy.mRootName, this); + } + } + + private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix, + Canvas canvas, int w, int h, ColorFilter filter) { + // Calculate current group's matrix by preConcat the parent's and + // and the current one on the top of the stack. + // Basically the Mfinal = Mviewport * M0 * M1 * M2; + // Mi the local matrix at level i of the group tree. + currentGroup.mStackedMatrix.set(currentMatrix); + + currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix); + + // Draw the group tree in the same order as the XML file. + for (int i = 0; i < currentGroup.mChildren.size(); i++) { + Object child = currentGroup.mChildren.get(i); + if (child instanceof VGroup) { + VGroup childGroup = (VGroup) child; + drawGroupTree(childGroup, currentGroup.mStackedMatrix, + canvas, w, h, filter); + } else if (child instanceof VPath) { + VPath childPath = (VPath) child; + drawPath(currentGroup, childPath, canvas, w, h, filter); + } + } + } + + public void draw(Canvas canvas, int w, int h, ColorFilter filter) { + // Travese the tree in pre-order to draw. + drawGroupTree(mRootGroup, IDENTITY_MATRIX, canvas, w, h, filter); + } + + private void drawPath(VGroup vGroup, VPath vPath, Canvas canvas, int w, int h, + ColorFilter filter) { + final float scaleX = w / mViewportWidth; + final float scaleY = h / mViewportHeight; + final float minScale = Math.min(scaleX, scaleY); + + mFinalPathMatrix.set(vGroup.mStackedMatrix); + mFinalPathMatrix.postScale(scaleX, scaleY); + + vPath.toPath(mPath); + final Path path = mPath; + + mRenderPath.reset(); + + if (vPath.isClipPath()) { + mRenderPath.addPath(path, mFinalPathMatrix); + canvas.clipPath(mRenderPath, Region.Op.REPLACE); + } else { + VFullPath fullPath = (VFullPath) vPath; + if (fullPath.mTrimPathStart != 0.0f || fullPath.mTrimPathEnd != 1.0f) { + float start = (fullPath.mTrimPathStart + fullPath.mTrimPathOffset) % 1.0f; + float end = (fullPath.mTrimPathEnd + fullPath.mTrimPathOffset) % 1.0f; + + if (mPathMeasure == null) { + mPathMeasure = new PathMeasure(); + } + mPathMeasure.setPath(mPath, false); + + float len = mPathMeasure.getLength(); + start = start * len; + end = end * len; + path.reset(); + if (start > end) { + mPathMeasure.getSegment(start, len, path, true); + mPathMeasure.getSegment(0f, end, path, true); + } else { + mPathMeasure.getSegment(start, end, path, true); + } + path.rLineTo(0, 0); // fix bug in measure + } + mRenderPath.addPath(path, mFinalPathMatrix); + + if (fullPath.mFillColor != Color.TRANSPARENT) { + if (mFillPaint == null) { + mFillPaint = new Paint(); + mFillPaint.setStyle(Paint.Style.FILL); + mFillPaint.setAntiAlias(true); + } + + final Paint fillPaint = mFillPaint; + fillPaint.setColor(applyAlpha(fullPath.mFillColor, fullPath.mFillAlpha)); + fillPaint.setColorFilter(filter); + canvas.drawPath(mRenderPath, fillPaint); + } + + if (fullPath.mStrokeColor != Color.TRANSPARENT) { + if (mStrokePaint == null) { + mStrokePaint = new Paint(); + mStrokePaint.setStyle(Paint.Style.STROKE); + mStrokePaint.setAntiAlias(true); + } + + final Paint strokePaint = mStrokePaint; + if (fullPath.mStrokeLineJoin != null) { + strokePaint.setStrokeJoin(fullPath.mStrokeLineJoin); + } + + if (fullPath.mStrokeLineCap != null) { + strokePaint.setStrokeCap(fullPath.mStrokeLineCap); + } + + strokePaint.setStrokeMiter(fullPath.mStrokeMiterlimit); + strokePaint.setColor(applyAlpha(fullPath.mStrokeColor, fullPath.mStrokeAlpha)); + strokePaint.setColorFilter(filter); + strokePaint.setStrokeWidth(fullPath.mStrokeWidth * minScale); + canvas.drawPath(mRenderPath, strokePaint); + } + } + } + } + + private static class VGroup { + // mStackedMatrix is only used temporarily when drawing, it combines all + // the parents' local matrices with the current one. + private final Matrix mStackedMatrix = new Matrix(); + + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + final ArrayList mChildren = new ArrayList(); + + private float mRotate = 0; + private float mPivotX = 0; + private float mPivotY = 0; + private float mScaleX = 1; + private float mScaleY = 1; + private float mTranslateX = 0; + private float mTranslateY = 0; + + // mLocalMatrix is updated based on the update of transformation information, + // either parsed from the XML or by animation. + private final Matrix mLocalMatrix = new Matrix(); + private int mChangingConfigurations; + private String mGroupName = null; + + public VGroup(VGroup copy, ArrayMap targetsMap) { + mRotate = copy.mRotate; + mPivotX = copy.mPivotX; + mPivotY = copy.mPivotY; + mScaleX = copy.mScaleX; + mScaleY = copy.mScaleY; + mTranslateX = copy.mTranslateX; + mTranslateY = copy.mTranslateY; + mGroupName = copy.mGroupName; + mChangingConfigurations = copy.mChangingConfigurations; + if (mGroupName != null) { + targetsMap.put(mGroupName, this); + } + + mLocalMatrix.set(copy.mLocalMatrix); + + final ArrayList children = copy.mChildren; + for (int i = 0; i < children.size(); i++) { + Object copyChild = children.get(i); + if (copyChild instanceof VGroup) { + VGroup copyGroup = (VGroup) copyChild; + mChildren.add(new VGroup(copyGroup, targetsMap)); + } else { + VPath newPath = null; + if (copyChild instanceof VFullPath) { + newPath = new VFullPath((VFullPath) copyChild); + } else if (copyChild instanceof VClipPath) { + newPath = new VClipPath((VClipPath) copyChild); + } else { + throw new IllegalStateException("Unknown object in the tree!"); + } + mChildren.add(newPath); + if (newPath.mPathName != null) { + targetsMap.put(newPath.mPathName, newPath); + } + } + } + } + + public VGroup() { + } + + public String getGroupName() { + return mGroupName; + } + + public Matrix getLocalMatrix() { + return mLocalMatrix; + } + + public void inflate(Resources res, AttributeSet attrs) { + final TypedArray a = res.obtainAttributes(attrs, + R.styleable.VectorDrawableGroup); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= 0; //a.getChangingConfigurations(); + + mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate); + mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX); + mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY); + mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX); + mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY); + mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX); + mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY); + + final String groupName = a.getString(R.styleable.VectorDrawableGroup_name); + if (groupName != null) { + mGroupName = groupName; + } + + updateLocalMatrix(); + } + + private void updateLocalMatrix() { + // The order we apply is the same as the + // RenderNode.cpp::applyViewPropertyTransforms(). + mLocalMatrix.reset(); + mLocalMatrix.postTranslate(-mPivotX, -mPivotY); + mLocalMatrix.postScale(mScaleX, mScaleY); + mLocalMatrix.postRotate(mRotate, 0, 0); + mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY); + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + public float getRotation() { + return mRotate; + } + + @SuppressWarnings("unused") + public void setRotation(float rotation) { + if (rotation != mRotate) { + mRotate = rotation; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getPivotX() { + return mPivotX; + } + + @SuppressWarnings("unused") + public void setPivotX(float pivotX) { + if (pivotX != mPivotX) { + mPivotX = pivotX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getPivotY() { + return mPivotY; + } + + @SuppressWarnings("unused") + public void setPivotY(float pivotY) { + if (pivotY != mPivotY) { + mPivotY = pivotY; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getScaleX() { + return mScaleX; + } + + @SuppressWarnings("unused") + public void setScaleX(float scaleX) { + if (scaleX != mScaleX) { + mScaleX = scaleX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getScaleY() { + return mScaleY; + } + + @SuppressWarnings("unused") + public void setScaleY(float scaleY) { + if (scaleY != mScaleY) { + mScaleY = scaleY; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getTranslateX() { + return mTranslateX; + } + + @SuppressWarnings("unused") + public void setTranslateX(float translateX) { + if (translateX != mTranslateX) { + mTranslateX = translateX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getTranslateY() { + return mTranslateY; + } + + @SuppressWarnings("unused") + public void setTranslateY(float translateY) { + if (translateY != mTranslateY) { + mTranslateY = translateY; + updateLocalMatrix(); + } + } + } + + /** + * Common Path information for clip path and normal path. + */ + private static class VPath { + protected PathParser.PathDataNode[] mNodes = null; + String mPathName; + int mChangingConfigurations; + + public VPath() { + // Empty constructor. + } + + public VPath(VPath copy) { + mPathName = copy.mPathName; + mChangingConfigurations = copy.mChangingConfigurations; + mNodes = PathParser.deepCopyNodes(copy.mNodes); + } + + public void toPath(Path path) { + path.reset(); + if (mNodes != null) { + PathParser.PathDataNode.nodesToPath(mNodes, path); + } + } + + public String getPathName() { + return mPathName; + } + + public boolean isClipPath() { + return false; + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + public PathParser.PathDataNode[] getPathData() { + return mNodes; + } + + @SuppressWarnings("unused") + public void setPathData(PathParser.PathDataNode[] nodes) { + if (!PathParser.canMorph(mNodes, nodes)) { + // This should not happen in the middle of animation. + mNodes = PathParser.deepCopyNodes(nodes); + } else { + PathParser.updateNodes(mNodes, nodes); + } + } + } + + /** + * Clip path, which only has name and pathData. + */ + private static class VClipPath extends VPath { + public VClipPath() { + // Empty constructor. + } + + public VClipPath(VClipPath copy) { + super(copy); + } + + public void inflate(Resources r, AttributeSet attrs) { + final TypedArray a = r.obtainAttributes(attrs, + R.styleable.VectorDrawableClipPath); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= 0; //a.getChangingConfigurations(); + + final String pathName = a.getString(R.styleable.VectorDrawableClipPath_name); + if (pathName != null) { + mPathName = pathName; + } + + final String pathData = a.getString(R.styleable.VectorDrawableClipPath_pathData); + if (pathData != null) { + mNodes = PathParser.createNodesFromPathData(pathData); + } + } + + @Override + public boolean isClipPath() { + return true; + } + } + + /** + * Normal path, which contains all the fill / paint information. + */ + private static class VFullPath extends VPath { + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + int mStrokeColor = Color.TRANSPARENT; + float mStrokeWidth = 0; + + int mFillColor = Color.TRANSPARENT; + float mStrokeAlpha = 1.0f; + int mFillRule; + float mFillAlpha = 1.0f; + float mTrimPathStart = 0; + float mTrimPathEnd = 1; + float mTrimPathOffset = 0; + + Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; + Paint.Join mStrokeLineJoin = Paint.Join.MITER; + float mStrokeMiterlimit = 4; + + public VFullPath() { + // Empty constructor. + } + + public VFullPath(VFullPath copy) { + super(copy); + + mStrokeColor = copy.mStrokeColor; + mStrokeWidth = copy.mStrokeWidth; + mStrokeAlpha = copy.mStrokeAlpha; + mFillColor = copy.mFillColor; + mFillRule = copy.mFillRule; + mFillAlpha = copy.mFillAlpha; + mTrimPathStart = copy.mTrimPathStart; + mTrimPathEnd = copy.mTrimPathEnd; + mTrimPathOffset = copy.mTrimPathOffset; + + mStrokeLineCap = copy.mStrokeLineCap; + mStrokeLineJoin = copy.mStrokeLineJoin; + mStrokeMiterlimit = copy.mStrokeMiterlimit; + } + + private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) { + switch (id) { + case LINECAP_BUTT: + return Paint.Cap.BUTT; + case LINECAP_ROUND: + return Paint.Cap.ROUND; + case LINECAP_SQUARE: + return Paint.Cap.SQUARE; + default: + return defValue; + } + } + + private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) { + switch (id) { + case LINEJOIN_MITER: + return Paint.Join.MITER; + case LINEJOIN_ROUND: + return Paint.Join.ROUND; + case LINEJOIN_BEVEL: + return Paint.Join.BEVEL; + default: + return defValue; + } + } + + public void inflate(Resources r, AttributeSet attrs) { + final TypedArray a = r.obtainAttributes(attrs, + R.styleable.VectorDrawablePath); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= 0; //a.getChangingConfigurations(); + + final String pathName = a.getString(R.styleable.VectorDrawablePath_name); + if (pathName != null) { + mPathName = pathName; + } + + final String pathData = a.getString(R.styleable.VectorDrawablePath_pathData); + if (pathData != null) { + mNodes = PathParser.createNodesFromPathData(pathData); + } + + mFillColor = a.getColor(R.styleable.VectorDrawablePath_fillColor, + mFillColor); + mFillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha, + mFillAlpha); + mStrokeLineCap = getStrokeLineCap(a.getInt( + R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); + mStrokeLineJoin = getStrokeLineJoin(a.getInt( + R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); + mStrokeMiterlimit = a.getFloat( + R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); + mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_strokeColor, + mStrokeColor); + mStrokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha, + mStrokeAlpha); + mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, + mStrokeWidth); + mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, + mTrimPathEnd); + mTrimPathOffset = a.getFloat( + R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); + mTrimPathStart = a.getFloat( + R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + int getStrokeColor() { + return mStrokeColor; + } + + @SuppressWarnings("unused") + void setStrokeColor(int strokeColor) { + mStrokeColor = strokeColor; + } + + @SuppressWarnings("unused") + float getStrokeWidth() { + return mStrokeWidth; + } + + @SuppressWarnings("unused") + void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + } + + @SuppressWarnings("unused") + float getStrokeAlpha() { + return mStrokeAlpha; + } + + @SuppressWarnings("unused") + void setStrokeAlpha(float strokeAlpha) { + mStrokeAlpha = strokeAlpha; + } + + @SuppressWarnings("unused") + int getFillColor() { + return mFillColor; + } + + @SuppressWarnings("unused") + void setFillColor(int fillColor) { + mFillColor = fillColor; + } + + @SuppressWarnings("unused") + float getFillAlpha() { + return mFillAlpha; + } + + @SuppressWarnings("unused") + void setFillAlpha(float fillAlpha) { + mFillAlpha = fillAlpha; + } + + @SuppressWarnings("unused") + float getTrimPathStart() { + return mTrimPathStart; + } + + @SuppressWarnings("unused") + void setTrimPathStart(float trimPathStart) { + mTrimPathStart = trimPathStart; + } + + @SuppressWarnings("unused") + float getTrimPathEnd() { + return mTrimPathEnd; + } + + @SuppressWarnings("unused") + void setTrimPathEnd(float trimPathEnd) { + mTrimPathEnd = trimPathEnd; + } + + @SuppressWarnings("unused") + float getTrimPathOffset() { + return mTrimPathOffset; + } + + @SuppressWarnings("unused") + void setTrimPathOffset(float trimPathOffset) { + mTrimPathOffset = trimPathOffset; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java index 780a7cc10..3291d213d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/ContentActivity.java @@ -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)); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index f79f44bc5..a2c6f3edc 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -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); diff --git a/app/src/main/java/com/hippo/util/DrawableUtils.java b/app/src/main/java/com/hippo/util/DrawableUtils.java new file mode 100644 index 000000000..2d3fd3a41 --- /dev/null +++ b/app/src/main/java/com/hippo/util/DrawableUtils.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/hippo/util/PathParser.java b/app/src/main/java/com/hippo/util/PathParser.java new file mode 100644 index 000000000..3bd595dc9 --- /dev/null +++ b/app/src/main/java/com/hippo/util/PathParser.java @@ -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 list = new ArrayList(); + 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 source. + */ + 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 nodesFrom can morph into nodesTo + */ + 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 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 + * nodeFrom and nodeTo according to the + * fraction. + * + * @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; + } + } + } +} diff --git a/app/src/main/res/drawable/ic_vector_happy.xml b/app/src/main/res/drawable/ic_vector_happy.xml new file mode 100644 index 000000000..eecdf5776 --- /dev/null +++ b/app/src/main/res/drawable/ic_vector_happy.xml @@ -0,0 +1,39 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml new file mode 100644 index 000000000..eff7dd3ec --- /dev/null +++ b/app/src/main/res/layout/fragment_list.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index a9fb5f9e8..9e9abe6c8 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -40,4 +40,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7f045a173..4374a3033 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -14,13 +14,13 @@ ~ limitations under the License. --> diff --git a/app/src/main/res/values/pathdata.xml b/app/src/main/res/values/pathdata.xml new file mode 100644 index 000000000..f389c0cef --- /dev/null +++ b/app/src/main/res/values/pathdata.xml @@ -0,0 +1,18 @@ + + + + M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z +