├── .gitignore ├── README.md ├── artwork ├── example.gif ├── example_1.gif └── example_2.gif ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wnafee │ │ └── vector │ │ └── compat │ │ └── demo │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wnafee │ │ └── vector │ │ └── compat │ │ └── demo │ │ └── MainActivity.java │ └── res │ ├── color │ ├── background_tint_color.xml │ └── foreground_tint_color.xml │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wnafee │ │ └── vector │ │ └── compat │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wnafee │ │ └── vector │ │ ├── MorphButton.java │ │ └── compat │ │ ├── AnimatedVectorDrawable.java │ │ ├── DrawableCompat.java │ │ ├── Outline.java │ │ ├── PathAnimatorInflater.java │ │ ├── PathParser.java │ │ ├── ResourcesCompat.java │ │ ├── Tintable.java │ │ └── VectorDrawable.java │ └── res │ ├── anim │ ├── arrow_to_drawer_path.xml │ ├── arrow_to_drawer_rotation.xml │ ├── drawer_to_arrow_path.xml │ ├── drawer_to_arrow_rotation.xml │ ├── pause_to_play_path.xml │ ├── pause_to_play_rotation.xml │ ├── play_to_pause_path.xml │ ├── play_to_pause_rotation.xml │ ├── play_to_stop_path.xml │ ├── play_to_stop_rotation.xml │ ├── stop_to_play_path.xml │ └── stop_to_play_rotation.xml │ ├── drawable │ ├── ic_arrow_to_drawer.xml │ ├── ic_arrow_vector.xml │ ├── ic_drawer_to_arrow.xml │ ├── ic_drawer_vector.xml │ ├── ic_pause_to_play.xml │ ├── ic_pause_vector.xml │ ├── ic_play_to_pause.xml │ ├── ic_play_to_stop.xml │ ├── ic_play_vector.xml │ ├── ic_stop_to_play.xml │ └── ic_stop_vector.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── attr.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .DS_Store 3 | build 4 | .project 5 | .settings 6 | bin 7 | local.properties 8 | .idea 9 | *.iml 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vector-compat 2 | A support library for [`VectorDrawable`][1] and [`AnimatedVectorDrawable`][2] introduced in Lollipop with fully backwards compatible tint support (api 14+ so far) 3 | 4 | `vector-compat` provides the necessary tools to make animated icons similar to the new drawer hamburger icon that morphs to a back arrow when clicked. Any other morph animation between icons can be defined _purely in `xml` (**no java code required**)_ and the library takes care of the transformation animation. Because they are in vector format, these drawables can be of any height and width with no resulting pixelation. 5 | 6 | ![Example](https://github.com/wnafee/vector-compat/blob/master/artwork/example.gif) 7 | ![Example 1](https://github.com/wnafee/vector-compat/blob/master/artwork/example_1.gif) 8 | ![Example 2](https://github.com/wnafee/vector-compat/blob/master/artwork/example_2.gif) 9 | 10 | The library will transparently fall back to the lollipop implementation of `VectorDrawable` and `AnimatedVectorDrawable` on api 21+ devices 11 | 12 | ##Commonly used animations 13 | The library packs some ready-made morph animations developers can use in their code with `MorphButton`. More will be added soon as this is a work-in-progress. The library has the following morph animations : 14 | * Play-Pause morph animation (bi-directional morph) 15 | * Play-Stop morph animation (bi-directional morph) 16 | * Arrow-Hamburger menu morph animation (bi-directional morph) 17 | 18 | **The goal is to, with time, create a repo of commonly used morph animations that lots of developers find useful.** 19 | 20 | If you have requests for particular morph animations, please open a [new issue](https://github.com/wnafee/vector-compat/issues/new) and I'll work on adding them to the library. You are also welcome to create a [pull request](https://github.com/wnafee/vector-compat/compare) if you've created some of your own. **_Please contribute_** :) 21 | 22 | ## Download 23 | Add the `vector-compat` dependency to your `build.gradle` file and make sure to use `buildToolsVersion` 22 or higher: 24 | 25 | [![Maven Central](https://img.shields.io/maven-central/v/com.wnafee/vector-compat.svg)](http://search.maven.org/#search%7Cga%7C1%7Cvector-compat) 26 | ```groovy 27 | android { 28 | // use version 22 or higher 29 | buildToolsVersion "22.0.1" 30 | ... 31 | } 32 | dependencies { 33 | compile 'com.wnafee:vector-compat:1.0.5' 34 | ... 35 | } 36 | ``` 37 | ## Proguard 38 | If you're using proguard for code shrinking and obfuscation, make sure to add the following: 39 | ```proguard 40 | -keep class com.wnafee.vector.** { *; } 41 | ``` 42 | ## Usage 43 | `VectorDrawable` and `AnimatedVectorDrawable` xml drawable syntax is exactly the same as the lollipop documentation (can be seen [here][1] and [here][2] respectively). With 2 caveats: 44 | * Some attributes under the `` nodes must be listed once for the `android:` namespace and once for the local namespace with a `vc_` prefix (e.g. `app:vc_fillColor`). See example [here][4]. (For a complete list of `vc_` prefixed attributes see [attr.xml][6] for ) 45 | * Any `pathType` anim xml must have the `android:valueType="pathType"` in addition to `app:vc_valueType="pathType"` to allow for lollipop implementation fallback. See example [here][5]. 46 | 47 | 48 | #### Inflation 49 | `VectorDrawable` and `AnimatedVectorDrawable` in this support library can be inflated in one of 2 ways: 50 | 51 | * Calling static `getDrawable()` methods: 52 | ```java 53 | //This will only inflate a drawable with as the root element 54 | VectorDrawable.getDrawable(context, R.drawable.ic_arrow_vector); 55 | 56 | //This will only inflate a drawable with as the root element 57 | AnimatedVectorDrawable.getDrawable(context, R.drawable.ic_arrow_to_menu_animated_vector); 58 | 59 | // This will inflate any drawable and will auto-fallback to the lollipop implementation on api 21+ devices 60 | ResourcesCompat.getDrawable(context, R.drawable.any_drawable); 61 | ```` 62 | _If inflating the Drawable in java code, it is recommended to always use `ResourcesCompat.getDrawable()` as this handles Lollipop fallback when applicable. This allows the system to cache Drawable ConstantState and hence is more efficient_ 63 | 64 | * directly from the `MorphButton` view in xml: 65 | ```xml 66 | 67 | 73 | ``` 74 | #### MorphButton 75 | `MorphButton` is a `CompoundButton` with 2 states: `MorphState.START` or `MorphState.END`. The attributes `vc_startDrawable` and `vc_endDrawable` define which foreground drawables to use for the button depending on the button's state. These can be any type of drawable (e.g. `BitmapDrawable`, `ColorDrawable`, `VectorDrawable`, `AnimatedVectorDrawable` etc.) 76 | 77 | To use MorphButton in your app, make sure to include the `morphButtonStyle` item in your base app theme: 78 | ```xml 79 | 82 | ``` 83 | 84 | `MorphButtons` allow you to tint your foreground drawables (i.e. `vc_startDrawable` and `vc_endDrawable`) and background drawable separately in both xml and java. See the following examples for defining `MorphButtons`: 85 | 86 | **XML**: 87 | ```xml 88 | 97 | ``` 98 | 99 | **Java**: 100 | ```java 101 | MorphButton mb = new MorphButton(this); 102 | mb.setBackgroundTintList(getResources().getColorStateList(R.color.background_tint_color)); 103 | mb.setForegroundTintList(ColorStateList.valueOf(Color.RED)); 104 | mb.setStartDrawable(R.drawable.ic_pause_to_play); 105 | mb.setEndDrawable(R.drawable.ic_play_to_pause); 106 | mb.setState(MorphState.END); 107 | ``` 108 | The `scaleType` attribute defines how to scale the foreground drawable to fill the button's background. This is the same as [`ImageView.ScaleType`][7] which you can take a look at [here][7]. 109 | 110 | Button clicks will toggle between the foreground drawables. If the drawables happen to implement the [`Animatable`][3] interface (e.g. `AnimatedVectorDrawable` or `AnimationDrawable`) then `start()` will be automatically called to animate between the start and end drawables defined in xml. 111 | 112 | MorphButton states can be set manually via `setState()` methods: 113 | ```java 114 | // transition with no animation 115 | myMorphButton.setState(MorphState.END) 116 | 117 | // ... or transition with animation if drawable is Animatable 118 | myMorphButton.setState(MorphState.START, true) 119 | ```` 120 | 121 | If you need to be informed of button state changes you need to add an `OnStateChangedListener`: 122 | ```java 123 | MyMorphButton.setOnStateChangedListener(new OnStateChangedListener() { 124 | @Override 125 | public void onStateChanged(MorphState changedTo, boolean isAnimating) { 126 | // changeTo is the new state 127 | // isAnimating = true if the state changed with animation 128 | // Do something here 129 | } 130 | }); 131 | ``` 132 | 133 | ## License 134 | 135 | Copyright 2015 Wael Nafee 136 | 137 | Licensed under the Apache License, Version 2.0 (the "License"); 138 | you may not use this file except in compliance with the License. 139 | You may obtain a copy of the License at 140 | 141 | http://www.apache.org/licenses/LICENSE-2.0 142 | 143 | Unless required by applicable law or agreed to in writing, software 144 | distributed under the License is distributed on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 146 | See the License for the specific language governing permissions and 147 | limitations under the License. 148 | 149 | [1]: http://developer.android.com/reference/android/graphics/drawable/VectorDrawable.html 150 | [2]: http://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html 151 | [3]: http://developer.android.com/reference/android/graphics/drawable/Animatable.html 152 | [4]: https://github.com/wnafee/vector-compat/blob/master/library/src/main/res/drawable/ic_arrow_vector.xml 153 | [5]: https://github.com/wnafee/vector-compat/blob/master/library/src/main/res/anim/arrow_to_drawer_path.xml 154 | [6]: https://github.com/wnafee/vector-compat/blob/master/library/src/main/res/values/attr.xml 155 | [7]: http://developer.android.com/reference/android/widget/ImageView.ScaleType.html 156 | -------------------------------------------------------------------------------- /artwork/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/artwork/example.gif -------------------------------------------------------------------------------- /artwork/example_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/artwork/example_1.gif -------------------------------------------------------------------------------- /artwork/example_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/artwork/example_2.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.wnafee.vector.compat.demo" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:22.1.1' 25 | compile project(':library') 26 | } 27 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/wnafee/android-sdks/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /demo/src/androidTest/java/com/wnafee/vector/compat/demo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat.demo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/src/main/java/com/wnafee/vector/compat/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat.demo; 2 | 3 | /* 4 | * Copyright (C) 2015 Wael Nafee 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import android.annotation.TargetApi; 18 | import android.os.Build; 19 | import android.os.Bundle; 20 | import android.support.v7.app.ActionBarActivity; 21 | import android.view.Menu; 22 | import android.view.MenuItem; 23 | import android.widget.LinearLayout; 24 | import android.widget.LinearLayout.LayoutParams; 25 | import android.widget.Toast; 26 | 27 | import com.wnafee.vector.MorphButton; 28 | import com.wnafee.vector.MorphButton.MorphState; 29 | import com.wnafee.vector.MorphButton.OnStateChangedListener; 30 | 31 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 32 | public class MainActivity extends ActionBarActivity { 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | 40 | // Example of adding MorphButton in java 41 | MorphButton mb = new MorphButton(this); 42 | LayoutParams p = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 43 | mb.setLayoutParams(p); 44 | 45 | mb.setBackgroundTintList(getResources().getColorStateList(R.color.background_tint_color)); 46 | mb.setForegroundTintList(getResources().getColorStateList(R.color.foreground_tint_color)); 47 | mb.setStartDrawable(R.drawable.ic_pause_to_play); 48 | mb.setEndDrawable(R.drawable.ic_play_to_pause); 49 | mb.setOnStateChangedListener(new OnStateChangedListener() { 50 | @Override 51 | public void onStateChanged(MorphState changedTo, boolean isAnimating) { 52 | // Do something here 53 | Toast.makeText(MainActivity.this, "Changed to: " + changedTo, Toast.LENGTH_SHORT).show(); 54 | } 55 | }); 56 | 57 | LinearLayout ll = (LinearLayout) findViewById(R.id.base_view); 58 | ll.addView(mb); 59 | } 60 | 61 | 62 | @Override 63 | public boolean onCreateOptionsMenu(Menu menu) { 64 | // Inflate the menu; this adds items to the action bar if it is present. 65 | getMenuInflater().inflate(R.menu.menu_main, menu); 66 | return true; 67 | } 68 | 69 | @Override 70 | public boolean onOptionsItemSelected(MenuItem item) { 71 | // Handle action bar item clicks here. The action bar will 72 | // automatically handle clicks on the Home/Up button, so long 73 | // as you specify a parent activity in AndroidManifest.xml. 74 | int id = item.getItemId(); 75 | 76 | //noinspection SimplifiableIfStatement 77 | if (id == R.id.action_settings) { 78 | return true; 79 | } 80 | 81 | return super.onOptionsItemSelected(item); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /demo/src/main/res/color/background_tint_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/color/foreground_tint_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 37 | 38 | 47 | 48 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /demo/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VectorCompatDemo 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | VERSION_NAME=1.0.5 21 | VERSION_CODE=1 22 | GROUP=com.wnafee 23 | 24 | POM_DESCRIPTION=A support library for VectorDrawable and AnimatedVectorDrawable introduced in Lollipop 25 | POM_URL=https://github.com/wnafee/vector-compat 26 | POM_SCM_URL=https://github.com/wnafee/vector-compat 27 | POM_SCM_CONNECTION=scm:git@github.com:wnafee/vector-compat.git 28 | POM_SCM_DEV_CONNECTION=scm:git@github.com:wnafee/vector-compat.git 29 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 30 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 31 | POM_LICENCE_DIST=repo 32 | POM_DEVELOPER_ID=wnafee 33 | POM_DEVELOPER_NAME=Wael Nafee 34 | 35 | ANDROID_BUILD_TARGET_SDK_VERSION=22 36 | ANDROID_BUILD_TOOLS_VERSION=22.0.1 37 | ANDROID_BUILD_SDK_VERSION=22 38 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wnafee/vector-compat/ac0992c18fcf23c4ca8c7c70b849b62ba98b8377/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | lintOptions { 8 | abortOnError false 9 | } 10 | 11 | defaultConfig { 12 | minSdkVersion 14 13 | targetSdkVersion 22 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: ['*.jar']) 27 | compile 'com.android.support:appcompat-v7:22.1.1' 28 | apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' 29 | } 30 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=vector-compat library 2 | POM_ARTIFACT_ID=vector-compat 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/wnafee/android-sdks/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/wnafee/vector/compat/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/MorphButton.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector; 2 | 3 | /* 4 | * Copyright (C) 2015 Wael Nafee 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import android.annotation.TargetApi; 18 | import android.content.Context; 19 | import android.content.res.ColorStateList; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Matrix; 25 | import android.graphics.PorterDuff; 26 | import android.graphics.Rect; 27 | import android.graphics.RectF; 28 | import android.graphics.drawable.Animatable; 29 | import android.graphics.drawable.Drawable; 30 | import android.os.Build; 31 | import android.os.Parcel; 32 | import android.os.Parcelable; 33 | import android.support.annotation.NonNull; 34 | import android.support.annotation.Nullable; 35 | import android.util.AttributeSet; 36 | import android.widget.CompoundButton; 37 | 38 | import com.wnafee.vector.compat.DrawableCompat; 39 | import com.wnafee.vector.compat.ResourcesCompat; 40 | import com.wnafee.vector.compat.Tintable; 41 | 42 | 43 | //TODO: attempt reversing animation if no morphEndDrawable is provided 44 | public class MorphButton extends CompoundButton { 45 | 46 | @SuppressWarnings("UnusedDeclaration") 47 | public static final String TAG = MorphButton.class.getSimpleName(); 48 | 49 | public static enum MorphState { 50 | START, 51 | END 52 | } 53 | 54 | public interface OnStateChangedListener { 55 | public void onStateChanged(MorphState changedTo, boolean isAnimating); 56 | } 57 | 58 | private static class TintInfo { 59 | ColorStateList mTintList; 60 | PorterDuff.Mode mTintMode; 61 | boolean mHasTintMode; 62 | boolean mHasTintList; 63 | } 64 | 65 | TintInfo mBackgroundTint; 66 | TintInfo mForegroundTint; 67 | 68 | MorphState mState = MorphState.START; 69 | 70 | Drawable mStartDrawable = null; 71 | Drawable mEndDrawable = null; 72 | Drawable mCurrentDrawable; 73 | 74 | int mStartDrawableWidth; 75 | int mStartDrawableHeight; 76 | 77 | int mEndDrawableWidth; 78 | int mEndDrawableHeight; 79 | 80 | int mCurrentDrawableWidth; 81 | int mCurrentDrawableHeight; 82 | 83 | boolean mStartCanMorph = false; 84 | boolean mEndCanMorph = false; 85 | 86 | boolean mIsToggling = false; 87 | boolean mHasStarted = false; 88 | 89 | boolean mCropToPadding = false; 90 | 91 | boolean mAdjustViewBounds = false; 92 | boolean mAdjustViewBoundsCompat = Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1; 93 | 94 | boolean mHaveFrame = false; 95 | 96 | private Matrix mMatrix; 97 | private Matrix mDrawMatrix = null; 98 | 99 | private ScaleType mScaleType; 100 | 101 | // Avoid allocations... 102 | private RectF mTempSrc = new RectF(); 103 | private RectF mTempDst = new RectF(); 104 | 105 | 106 | private OnStateChangedListener mStateListener; 107 | 108 | public MorphButton(Context context) { 109 | this(context, null); 110 | } 111 | 112 | public MorphButton(Context context, AttributeSet attrs) { 113 | this(context, attrs, R.attr.morphButtonStyle); 114 | } 115 | 116 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 117 | @SuppressWarnings("deprecation") 118 | public MorphButton(Context context, AttributeSet attrs, int defStyleAttr) { 119 | super(context, attrs, defStyleAttr); 120 | 121 | initMorphButton(); 122 | 123 | final Resources.Theme theme = context.getTheme(); 124 | TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.MorphButton, defStyleAttr, 0); 125 | 126 | int startResId = a.getResourceId(R.styleable.MorphButton_vc_startDrawable, -1); 127 | int endResId = a.getResourceId(R.styleable.MorphButton_vc_endDrawable, -1); 128 | boolean autoStart = a.getBoolean(R.styleable.MorphButton_vc_autoStartAnimation, false); 129 | 130 | final int st = a.getInt(R.styleable.MorphButton_android_scaleType, -1); 131 | if (st >= 0) { 132 | setScaleType(getScaleTypeFromInt(st)); 133 | } 134 | 135 | readTintAttributes(a); 136 | a.recycle(); 137 | 138 | applyBackgroundTint(); 139 | setClickable(true); 140 | 141 | setStartDrawable(startResId, false); 142 | setEndDrawable(endResId, false); 143 | 144 | setState(mState); 145 | if (autoStart) { 146 | mHasStarted = true; 147 | setState(MorphState.END, true); 148 | } 149 | } 150 | 151 | private void initMorphButton() { 152 | mMatrix = new Matrix(); 153 | mScaleType = ScaleType.FIT_CENTER; 154 | } 155 | 156 | private boolean isMorphable(Drawable d) { 157 | return d != null && d instanceof Animatable; 158 | } 159 | 160 | @SuppressWarnings("UnusedDeclaration") 161 | public void setOnStateChangedListener(OnStateChangedListener l) { 162 | 163 | //Should I invalidate something? 164 | if (l != null && l != mStateListener) { 165 | mStateListener = l; 166 | } 167 | } 168 | 169 | @Override 170 | public void toggle() { 171 | mHasStarted = true; 172 | mIsToggling = true; 173 | setState(mState == MorphState.START ? MorphState.END: MorphState.START, true); 174 | super.toggle(); 175 | mIsToggling = false; 176 | } 177 | 178 | private void updateDrawable(Drawable d, MorphState state) { 179 | Drawable oldD = state == MorphState.START ? mStartDrawable : mEndDrawable; 180 | 181 | if (oldD != null) { 182 | oldD.setCallback(null); 183 | unscheduleDrawable(oldD); 184 | } 185 | 186 | if (state == MorphState.START) { 187 | mStartDrawable = d; 188 | mStartCanMorph = isMorphable(d); 189 | } else { 190 | mEndDrawable = d; 191 | mEndCanMorph = isMorphable(d); 192 | } 193 | 194 | if (d != null) { 195 | d.setCallback(this); 196 | //TODO: Adjust layout direction 197 | // d.setLayoutDirection(getLayoutDirection()); 198 | if (d.isStateful()) { 199 | d.setState(getDrawableState()); 200 | } 201 | d.setVisible(getVisibility() == VISIBLE, true); 202 | d.setLevel(0); //TODO: not supporting layerlist drawables for now 203 | 204 | //Setting width and height; 205 | int width; 206 | int height; 207 | if (state == MorphState.START) { 208 | width = mStartDrawableWidth = d.getIntrinsicWidth(); 209 | height = mStartDrawableHeight = d.getIntrinsicHeight(); 210 | } else { 211 | width = mEndDrawableWidth = d.getIntrinsicWidth(); 212 | height = mEndDrawableHeight = d.getIntrinsicHeight(); 213 | } 214 | 215 | applyForegroundTint(d); 216 | configureBounds(d, width, height); 217 | } else { 218 | if (state == MorphState.START) { 219 | mStartDrawableWidth = mStartDrawableHeight = -1; 220 | } else { 221 | mEndDrawableWidth = mEndDrawableHeight = -1; 222 | } 223 | } 224 | 225 | } 226 | 227 | @Override 228 | public void refreshDrawableState() { 229 | super.refreshDrawableState(); 230 | refreshCurrentDrawable(); 231 | } 232 | 233 | private void refreshCurrentDrawable() { 234 | if (mCurrentDrawable != null) { 235 | mCurrentDrawable.setState(getDrawableState()); 236 | } 237 | } 238 | 239 | @Override 240 | public void jumpDrawablesToCurrentState() { 241 | super.jumpDrawablesToCurrentState(); 242 | if (mCurrentDrawable != null) { 243 | mCurrentDrawable.jumpToCurrentState(); 244 | } 245 | } 246 | 247 | @Override 248 | public void setSelected(boolean selected) { 249 | super.setSelected(selected); 250 | resizeFromDrawable(mState); 251 | } 252 | 253 | private void resizeFromDrawable(MorphState state) { 254 | int width = state == MorphState.START ? mStartDrawableWidth: mEndDrawableWidth; 255 | int height = state == MorphState.START ? mStartDrawableHeight: mEndDrawableHeight; 256 | Drawable d = state == MorphState.START ? mStartDrawable : mEndDrawable; 257 | if (d != null) { 258 | int w = d.getIntrinsicWidth(); 259 | if (w < 0) w = width; 260 | int h = d.getIntrinsicHeight(); 261 | if (h < 0) h = height; 262 | if (w != width || h != height) { 263 | if (state == MorphState.START) { 264 | mStartDrawableWidth = w; 265 | mStartDrawableHeight = h; 266 | } else { 267 | mEndDrawableWidth = w; 268 | mEndDrawableHeight = h; 269 | } 270 | requestLayout(); 271 | } 272 | } 273 | } 274 | 275 | @SuppressWarnings("UnusedDeclaration") 276 | public void setStartDrawable(int rId) { 277 | setStartDrawable(rId, true); 278 | } 279 | 280 | private void setStartDrawable(int rId, boolean refreshState) { 281 | if (rId > 0) { 282 | setStartDrawable(ResourcesCompat.getDrawable(getContext(), rId), refreshState); 283 | } 284 | } 285 | 286 | @SuppressWarnings("UnusedDeclaration") 287 | public void setStartDrawable(Drawable d) { 288 | setStartDrawable(d, true); 289 | } 290 | 291 | private void setStartDrawable(Drawable d, boolean refreshState) { 292 | if (mStartDrawable == d ) 293 | return; 294 | 295 | updateDrawable(d, MorphState.START); 296 | 297 | if (refreshState) 298 | setState(mState); 299 | } 300 | 301 | @SuppressWarnings("UnusedDeclaration") 302 | public void setEndDrawable(int rId) { 303 | setEndDrawable(rId, true); 304 | } 305 | 306 | private void setEndDrawable(int rId, boolean refreshState) { 307 | if (rId > 0) { 308 | setEndDrawable(ResourcesCompat.getDrawable(getContext(), rId), refreshState); 309 | } 310 | } 311 | 312 | @SuppressWarnings("UnusedDeclaration") 313 | public void setEndDrawable(Drawable d) { 314 | setEndDrawable(d, true); 315 | } 316 | 317 | private void setEndDrawable(Drawable d, boolean refreshState) { 318 | if (mEndDrawable == d) 319 | return; 320 | 321 | updateDrawable(d, MorphState.END); 322 | 323 | if (refreshState) 324 | setState(mState); 325 | } 326 | 327 | public MorphState getState() { 328 | return mState; 329 | } 330 | 331 | private void setCurrentDrawable(Drawable d, int width, int height) { 332 | if (mCurrentDrawable != d) { 333 | mCurrentDrawable = d; 334 | 335 | // Check that drawable has had its bounds set 336 | Rect r = d.getBounds(); 337 | int boundsWidth = r.right - r.left; 338 | int boundsHeight = r.bottom - r.top; 339 | if (mCurrentDrawableWidth != width || mCurrentDrawableHeight != height 340 | || boundsWidth != width || boundsHeight != height) { 341 | requestLayout(); 342 | } 343 | 344 | mCurrentDrawableWidth = width; 345 | mCurrentDrawableHeight = height; 346 | } 347 | 348 | } 349 | 350 | /** 351 | * Same as {@link MorphButton#setState(MorphButton.MorphState, boolean)} with no animation 352 | * 353 | * @param state requested state 354 | */ 355 | public void setState(MorphState state) { 356 | setState(state, false); 357 | } 358 | 359 | /** 360 | * Choose button state 361 | * 362 | * @param state a {@link MorphButton.MorphState} to set button to 363 | * @param animate should we animated to get to this state or not 364 | */ 365 | @SuppressWarnings("deprecation") 366 | public void setState(MorphState state, boolean animate) { 367 | boolean checked; 368 | if (state == MorphState.START) { 369 | checked = false; 370 | int w = mEndCanMorph ? mEndDrawableWidth : mStartDrawableWidth; 371 | int h = mEndCanMorph ? mEndDrawableHeight : mStartDrawableHeight; 372 | setCurrentDrawable(mEndCanMorph ? mEndDrawable : mStartDrawable, w, h); 373 | beginEndAnimation(); 374 | if (!animate) { 375 | endEndAnimation(); 376 | } 377 | } else { 378 | checked = true; 379 | int w = mStartCanMorph ? mStartDrawableWidth : mEndDrawableWidth; 380 | int h = mStartCanMorph ? mStartDrawableHeight : mEndDrawableHeight; 381 | setCurrentDrawable(mStartCanMorph ? mStartDrawable : mEndDrawable, w, h); 382 | beginStartAnimation(); 383 | if (!animate) { 384 | endStartAnimation(); 385 | } 386 | } 387 | 388 | // Only allow state listeners to change if actually changing state 389 | if (mState == state && mHasStarted) { 390 | return; 391 | } 392 | 393 | // Update checked state of button 394 | super.setChecked(checked); 395 | 396 | mState = state; 397 | if (mStateListener != null) { 398 | mStateListener.onStateChanged(state, animate); 399 | } 400 | } 401 | 402 | @Override 403 | public void setChecked(boolean checked) { 404 | if (!mIsToggling) { 405 | setState(checked ? MorphState.END : MorphState.START); 406 | } 407 | } 408 | 409 | @Override 410 | protected void onDraw(@NonNull Canvas canvas) { 411 | super.onDraw(canvas); 412 | 413 | if (mCurrentDrawable == null) { 414 | return; //not set yet 415 | } 416 | 417 | if (mCurrentDrawableWidth == 0 || mCurrentDrawableHeight == 0) { 418 | return; // nothing to draw (empty bounds) 419 | } 420 | 421 | final int paddingTop = getPaddingTop(); 422 | final int paddingLeft = getPaddingLeft(); 423 | final int paddingBottom = getPaddingBottom(); 424 | final int paddingRight = getPaddingRight(); 425 | final int top = getTop(); 426 | final int bottom = getBottom(); 427 | final int left = getLeft(); 428 | final int right = getRight(); 429 | if (mDrawMatrix == null && paddingTop == 0 && paddingLeft == 0) { 430 | mCurrentDrawable.draw(canvas); 431 | } else { 432 | int saveCount = canvas.getSaveCount(); 433 | canvas.save(); 434 | 435 | if (mCropToPadding) { 436 | final int scrollX = getScrollX(); 437 | final int scrollY = getScrollY(); 438 | canvas.clipRect(scrollX + paddingLeft, scrollY + paddingTop, 439 | scrollX + right - left - paddingRight, 440 | scrollY + bottom - top - paddingBottom); 441 | } 442 | 443 | canvas.translate(paddingLeft, paddingTop); 444 | 445 | if (mDrawMatrix != null) { 446 | canvas.concat(mDrawMatrix); 447 | } 448 | mCurrentDrawable.draw(canvas); 449 | canvas.restoreToCount(saveCount); 450 | } 451 | } 452 | 453 | 454 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 455 | @SuppressWarnings("deprecation") 456 | @Override 457 | public void setBackgroundDrawable(Drawable background) { 458 | if (ResourcesCompat.LOLLIPOP) { 459 | if (mBackgroundTint != null) { 460 | // Set tint parameters for superclass View to apply 461 | if (mBackgroundTint.mHasTintList) 462 | super.setBackgroundTintList(mBackgroundTint.mTintList); 463 | if (mBackgroundTint.mHasTintMode) 464 | super.setBackgroundTintMode(mBackgroundTint.mTintMode); 465 | } 466 | super.setBackgroundDrawable(background); 467 | } else { 468 | super.setBackgroundDrawable(background); 469 | 470 | // Need to apply tint ourselves 471 | applyBackgroundTint(); 472 | } 473 | 474 | } 475 | 476 | public ColorStateList getBackgroundTintList() { 477 | if (ResourcesCompat.LOLLIPOP) { 478 | return getBackgroundTintList(); 479 | } 480 | return mBackgroundTint != null ? mBackgroundTint.mTintList : null; 481 | } 482 | 483 | @SuppressWarnings("UnusedDeclaration") 484 | public ColorStateList getForegroundTintList() { 485 | return mForegroundTint != null ? mForegroundTint.mTintList : null; 486 | } 487 | 488 | public void setBackgroundTintList(@Nullable ColorStateList tint) { 489 | if (ResourcesCompat.LOLLIPOP) { 490 | super.setBackgroundTintList(tint); 491 | } 492 | 493 | if (mBackgroundTint == null) { 494 | mBackgroundTint = new TintInfo(); 495 | } 496 | mBackgroundTint.mTintList = tint; 497 | mBackgroundTint.mHasTintList = true; 498 | 499 | if (!ResourcesCompat.LOLLIPOP) { 500 | applyBackgroundTint(); 501 | } 502 | } 503 | 504 | @SuppressWarnings("UnusedDeclaration") 505 | public void setForegroundTintList(@Nullable ColorStateList tint) { 506 | if (mForegroundTint == null) { 507 | mForegroundTint = new TintInfo(); 508 | } 509 | 510 | mForegroundTint.mTintList = tint; 511 | mForegroundTint.mHasTintList = true; 512 | 513 | // Apply to all current foreground drawables 514 | applyForegroundTint(); 515 | 516 | } 517 | 518 | @SuppressWarnings("UnusedDeclaration") 519 | public PorterDuff.Mode getForegroundTintMode() { 520 | return mForegroundTint != null ? mForegroundTint.mTintMode : null; 521 | } 522 | 523 | public PorterDuff.Mode getBackgroundTintMode() { 524 | if (ResourcesCompat.LOLLIPOP) { 525 | return getBackgroundTintMode(); 526 | } 527 | return mBackgroundTint != null ? mBackgroundTint.mTintMode : null; 528 | } 529 | 530 | public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 531 | if (ResourcesCompat.LOLLIPOP) { 532 | super.setBackgroundTintMode(tintMode); 533 | } 534 | if (mBackgroundTint == null) { 535 | mBackgroundTint = new TintInfo(); 536 | } 537 | mBackgroundTint.mTintMode = tintMode; 538 | mBackgroundTint.mHasTintMode = true; 539 | 540 | if (!ResourcesCompat.LOLLIPOP) { 541 | applyBackgroundTint(); 542 | } 543 | } 544 | 545 | @SuppressWarnings("UnusedDeclaration") 546 | public void setForegroundTintMode(@Nullable PorterDuff.Mode tintMode) { 547 | if (mForegroundTint == null) { 548 | mForegroundTint = new TintInfo(); 549 | } 550 | mForegroundTint.mTintMode = tintMode; 551 | mForegroundTint.mHasTintMode = true; 552 | 553 | // Apply to all current foreground drawables 554 | applyForegroundTint(); 555 | } 556 | 557 | private void setDrawableColorFilter(Drawable d, int color, PorterDuff.Mode mode) { 558 | if (d != null) { 559 | d.setColorFilter(color, mode); 560 | } 561 | } 562 | 563 | @SuppressWarnings("UnusedDeclaration") 564 | public void setForegroundColorFilter(int color, PorterDuff.Mode mode) { 565 | if (mStartDrawable != null) { 566 | mStartDrawable.setColorFilter(color, mode); 567 | } 568 | 569 | if (mEndDrawable != null) { 570 | mEndDrawable.setColorFilter(color, mode); 571 | } 572 | } 573 | 574 | @SuppressWarnings("UnusedDeclaration") 575 | public void setBackgroundColorFilter(int color, PorterDuff.Mode mode) { 576 | setDrawableColorFilter(getBackground(), color, mode); 577 | } 578 | 579 | /** 580 | * Apply tint to the drawable 581 | * @param d drawable 582 | */ 583 | private void applyForegroundTint(Drawable d) { 584 | applyTint(d, mForegroundTint); 585 | } 586 | 587 | /** 588 | * Apply tint to all foreground drawables 589 | */ 590 | private void applyForegroundTint() { 591 | applyTint(mStartDrawable, mForegroundTint); 592 | applyTint(mEndDrawable, mForegroundTint); 593 | } 594 | 595 | /** 596 | * Apply tint to our background drawable 597 | */ 598 | private void applyBackgroundTint() { 599 | applyTint(getBackground(), mBackgroundTint); 600 | } 601 | 602 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 603 | private void applyTint(Drawable d, TintInfo t) { 604 | if (d != null && t != null) { 605 | if (ResourcesCompat.LOLLIPOP) { 606 | if (t.mHasTintList || t.mHasTintMode) { 607 | d = d.mutate(); 608 | if (t.mHasTintList) { 609 | d.setTintList(t.mTintList); 610 | } 611 | if (t.mHasTintMode) { 612 | d.setTintMode(t.mTintMode); 613 | } 614 | } 615 | } else if (d instanceof Tintable) { 616 | // Our VectorDrawable and AnimatedVectorDrawable implementation 617 | if (t.mHasTintList || t.mHasTintMode) { 618 | d = d.mutate(); 619 | Tintable tintable = (Tintable) d; 620 | if (t.mHasTintList) { 621 | tintable.setTintList(t.mTintList); 622 | } 623 | if (t.mHasTintMode) { 624 | tintable.setTintMode(t.mTintMode); 625 | } 626 | } 627 | } else { 628 | //TODO: Should I attempt to make "stateful" ColorFilters from mBackgroundTint? 629 | if (t.mHasTintList) { 630 | int color = t.mTintList.getColorForState(getDrawableState(), Color.TRANSPARENT); 631 | setDrawableColorFilter(d, color, PorterDuff.Mode.SRC_IN); 632 | } 633 | 634 | } 635 | } 636 | } 637 | 638 | private void readTintAttributes(TypedArray a) { 639 | mBackgroundTint = new TintInfo(); 640 | mForegroundTint = new TintInfo(); 641 | 642 | mBackgroundTint.mTintList = a.getColorStateList(R.styleable.MorphButton_vc_backgroundTint); 643 | mBackgroundTint.mHasTintList = mBackgroundTint.mTintList != null; 644 | 645 | mBackgroundTint.mTintMode = DrawableCompat.parseTintMode(a.getInt( 646 | R.styleable.MorphButton_vc_backgroundTintMode, -1), null); 647 | mBackgroundTint.mHasTintMode = mBackgroundTint.mTintMode != null; 648 | 649 | mForegroundTint.mTintList = a.getColorStateList(R.styleable.MorphButton_vc_foregroundTint); 650 | mForegroundTint.mHasTintList = mForegroundTint.mTintList != null; 651 | 652 | mForegroundTint.mTintMode = DrawableCompat.parseTintMode(a.getInt( 653 | R.styleable.MorphButton_vc_foregroundTintMode, -1), null); 654 | mForegroundTint.mHasTintMode = mForegroundTint.mTintMode != null; 655 | } 656 | 657 | static class SavedState extends BaseSavedState { 658 | MorphState state; 659 | 660 | /** 661 | * Constructor called from {@link CompoundButton#onSaveInstanceState()} 662 | */ 663 | SavedState(Parcelable superState) { 664 | super(superState); 665 | } 666 | 667 | /** 668 | * Constructor called from {@link #CREATOR} 669 | */ 670 | private SavedState(Parcel in) { 671 | super(in); 672 | state = (MorphState)in.readValue(null); 673 | } 674 | 675 | @Override 676 | public void writeToParcel(@NonNull Parcel out, int flags) { 677 | super.writeToParcel(out, flags); 678 | out.writeValue(state); 679 | } 680 | 681 | @Override 682 | public String toString() { 683 | return "MorphButton.SavedState{" 684 | + Integer.toHexString(System.identityHashCode(this)) 685 | + " state=" + state + "}"; 686 | } 687 | 688 | public static final Parcelable.Creator CREATOR 689 | = new Parcelable.Creator() { 690 | public SavedState createFromParcel(Parcel in) { 691 | return new SavedState(in); 692 | } 693 | 694 | public SavedState[] newArray(int size) { 695 | return new SavedState[size]; 696 | } 697 | }; 698 | } 699 | 700 | @NonNull 701 | @Override 702 | public Parcelable onSaveInstanceState() { 703 | Parcelable superState = super.onSaveInstanceState(); 704 | SavedState ss = new SavedState(superState); 705 | ss.state = getState(); 706 | return ss; 707 | } 708 | 709 | @Override 710 | public void onRestoreInstanceState(Parcelable state) { 711 | SavedState ss = (SavedState) state; 712 | super.onRestoreInstanceState(ss.getSuperState()); 713 | setState(ss.state, false); 714 | requestLayout(); 715 | } 716 | 717 | private boolean beginStartAnimation() { 718 | if (mStartDrawable != null && mStartCanMorph) { 719 | ((Animatable) mStartDrawable).start(); 720 | return true; 721 | } 722 | return false; 723 | } 724 | 725 | private boolean endStartAnimation() { 726 | if (mStartDrawable != null && mStartCanMorph) { 727 | ((Animatable) mStartDrawable).stop(); 728 | return true; 729 | } 730 | return false; 731 | } 732 | 733 | private boolean beginEndAnimation() { 734 | if (mEndDrawable != null && mEndCanMorph) { 735 | ((Animatable) mEndDrawable).start(); 736 | return true; 737 | } 738 | return false; 739 | } 740 | 741 | private boolean endEndAnimation() { 742 | if (mEndDrawable != null && mEndCanMorph) { 743 | ((Animatable) mEndDrawable).stop(); 744 | return true; 745 | } 746 | return false; 747 | } 748 | 749 | @Override 750 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 751 | int w; 752 | int h; 753 | 754 | // Desired aspect ratio of the view's contents (not including padding) 755 | float desiredAspect = 0.0f; 756 | 757 | // We are allowed to change the view's width 758 | boolean resizeWidth = false; 759 | 760 | // We are allowed to change the view's height 761 | boolean resizeHeight = false; 762 | 763 | final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 764 | final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 765 | 766 | if (mCurrentDrawable == null) { 767 | // If no drawable, its intrinsic size is 0. 768 | mCurrentDrawableWidth = -1; 769 | mCurrentDrawableHeight = -1; 770 | w = h = 0; 771 | } else { 772 | w = mCurrentDrawableWidth; 773 | h = mCurrentDrawableHeight; 774 | if (w <= 0) w = 1; 775 | if (h <= 0) h = 1; 776 | 777 | // We are supposed to adjust view bounds to match the aspect 778 | // ratio of our drawable. See if that is possible. 779 | if (mAdjustViewBounds) { 780 | resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; 781 | resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; 782 | 783 | desiredAspect = (float) w / (float) h; 784 | } 785 | } 786 | 787 | int pleft = getPaddingLeft(); 788 | int pright = getPaddingRight(); 789 | int ptop = getPaddingTop(); 790 | int pbottom = getPaddingBottom(); 791 | 792 | int widthSize; 793 | int heightSize; 794 | 795 | if (resizeWidth || resizeHeight) { 796 | /* If we get here, it means we want to resize to match the 797 | drawables aspect ratio, and we have the freedom to change at 798 | least one dimension. 799 | */ 800 | 801 | // Get the max possible width given our constraints 802 | widthSize = resolveAdjustedSize(w + pleft + pright, Integer.MAX_VALUE, widthMeasureSpec); 803 | 804 | // Get the max possible height given our constraints 805 | heightSize = resolveAdjustedSize(h + ptop + pbottom, Integer.MAX_VALUE, heightMeasureSpec); 806 | 807 | if (desiredAspect != 0.0f) { 808 | // See what our actual aspect ratio is 809 | float actualAspect = (float)(widthSize - pleft - pright) / 810 | (heightSize - ptop - pbottom); 811 | 812 | if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { 813 | 814 | boolean done = false; 815 | 816 | // Try adjusting width to be proportional to height 817 | if (resizeWidth) { 818 | int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + 819 | pleft + pright; 820 | 821 | // Allow the width to outgrow its original estimate if height is fixed. 822 | if (!resizeHeight && !mAdjustViewBoundsCompat) { 823 | widthSize = resolveAdjustedSize(newWidth, Integer.MAX_VALUE, widthMeasureSpec); 824 | } 825 | 826 | if (newWidth <= widthSize) { 827 | widthSize = newWidth; 828 | done = true; 829 | } 830 | } 831 | 832 | // Try adjusting height to be proportional to width 833 | if (!done && resizeHeight) { 834 | int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + 835 | ptop + pbottom; 836 | 837 | // Allow the height to outgrow its original estimate if width is fixed. 838 | if (!resizeWidth && !mAdjustViewBoundsCompat) { 839 | heightSize = resolveAdjustedSize(newHeight, Integer.MAX_VALUE, 840 | heightMeasureSpec); 841 | } 842 | 843 | if (newHeight <= heightSize) { 844 | heightSize = newHeight; 845 | } 846 | } 847 | } 848 | } 849 | } else { 850 | /* We are either don't want to preserve the drawables aspect ratio, 851 | or we are not allowed to change view dimensions. Just measure in 852 | the normal way. 853 | */ 854 | w += pleft + pright; 855 | h += ptop + pbottom; 856 | 857 | w = Math.max(w, getSuggestedMinimumWidth()); 858 | h = Math.max(h, getSuggestedMinimumHeight()); 859 | 860 | widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); 861 | heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); 862 | } 863 | 864 | setMeasuredDimension(widthSize, heightSize); 865 | } 866 | 867 | private int resolveAdjustedSize(int desiredSize, int maxSize, 868 | int measureSpec) { 869 | int result = desiredSize; 870 | int specMode = MeasureSpec.getMode(measureSpec); 871 | int specSize = MeasureSpec.getSize(measureSpec); 872 | switch (specMode) { 873 | case MeasureSpec.UNSPECIFIED: 874 | /* Parent says we can be as big as we want. Just don't be larger 875 | than max size imposed on ourselves. 876 | */ 877 | result = Math.min(desiredSize, maxSize); 878 | break; 879 | case MeasureSpec.AT_MOST: 880 | // Parent says we can be as big as we want, up to specSize. 881 | // Don't be larger than specSize, and don't be larger than 882 | // the max size imposed on ourselves. 883 | result = Math.min(Math.min(desiredSize, specSize), maxSize); 884 | break; 885 | case MeasureSpec.EXACTLY: 886 | // No choice. Do what we are told. 887 | result = specSize; 888 | break; 889 | } 890 | return result; 891 | } 892 | 893 | @Override 894 | protected boolean setFrame(int l, int t, int r, int b) { 895 | boolean changed = super.setFrame(l, t, r, b); 896 | mHaveFrame = true; 897 | configureBounds(); 898 | return changed; 899 | } 900 | 901 | private void configureBounds() { 902 | configureBounds(mCurrentDrawable, mCurrentDrawableWidth, mCurrentDrawableHeight); 903 | 904 | } 905 | 906 | private void configureBounds(Drawable d, int dwidth, int dheight) { 907 | if (d == null || !mHaveFrame) { 908 | return; 909 | } 910 | 911 | int vwidth = getWidth() - getPaddingLeft() - getPaddingRight(); 912 | int vheight = getHeight() - getPaddingTop() - getPaddingBottom(); 913 | 914 | boolean fits = (dwidth < 0 || vwidth == dwidth) && 915 | (dheight < 0 || vheight == dheight); 916 | 917 | if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { 918 | /* If the drawable has no intrinsic size, or we're told to 919 | scaletofit, then we just fill our entire view. 920 | */ 921 | d.setBounds(0, 0, vwidth, vheight); 922 | mDrawMatrix = null; 923 | } else { 924 | // We need to do the scaling ourself, so have the drawable 925 | // use its native size. 926 | d.setBounds(0, 0, dwidth, dheight); 927 | 928 | if (ScaleType.MATRIX == mScaleType) { 929 | // Use the specified matrix as-is. 930 | if (mMatrix.isIdentity()) { 931 | mDrawMatrix = null; 932 | } else { 933 | mDrawMatrix = mMatrix; 934 | } 935 | } else if (fits) { 936 | // The bitmap fits exactly, no transform needed. 937 | mDrawMatrix = null; 938 | } else if (ScaleType.CENTER == mScaleType) { 939 | // Center bitmap in view, no scaling. 940 | mDrawMatrix = mMatrix; 941 | mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f), 942 | (int) ((vheight - dheight) * 0.5f + 0.5f)); 943 | } else if (ScaleType.CENTER_CROP == mScaleType) { 944 | mDrawMatrix = mMatrix; 945 | 946 | float scale; 947 | float dx = 0, dy = 0; 948 | 949 | if (dwidth * vheight > vwidth * dheight) { 950 | scale = (float) vheight / (float) dheight; 951 | dx = (vwidth - dwidth * scale) * 0.5f; 952 | } else { 953 | scale = (float) vwidth / (float) dwidth; 954 | dy = (vheight - dheight * scale) * 0.5f; 955 | } 956 | 957 | mDrawMatrix.setScale(scale, scale); 958 | mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); 959 | } else if (ScaleType.CENTER_INSIDE == mScaleType) { 960 | mDrawMatrix = mMatrix; 961 | float scale; 962 | float dx; 963 | float dy; 964 | 965 | if (dwidth <= vwidth && dheight <= vheight) { 966 | scale = 1.0f; 967 | } else { 968 | scale = Math.min((float) vwidth / (float) dwidth, 969 | (float) vheight / (float) dheight); 970 | } 971 | 972 | dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f); 973 | dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f); 974 | 975 | mDrawMatrix.setScale(scale, scale); 976 | mDrawMatrix.postTranslate(dx, dy); 977 | } else { 978 | // Generate the required transform. 979 | mTempSrc.set(0, 0, dwidth, dheight); 980 | mTempDst.set(0, 0, vwidth, vheight); 981 | 982 | mDrawMatrix = mMatrix; 983 | mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); 984 | } 985 | } 986 | } 987 | 988 | private static final Matrix.ScaleToFit[] sS2FArray = { 989 | Matrix.ScaleToFit.FILL, 990 | Matrix.ScaleToFit.START, 991 | Matrix.ScaleToFit.CENTER, 992 | Matrix.ScaleToFit.END 993 | }; 994 | 995 | private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st) { 996 | // ScaleToFit enum to their corresponding Matrix.ScaleToFit values 997 | return sS2FArray[st.nativeInt - 1]; 998 | } 999 | 1000 | public enum ScaleType { 1001 | MATRIX (0), 1002 | FIT_XY (1), 1003 | FIT_START (2), 1004 | FIT_CENTER (3), 1005 | FIT_END (4), 1006 | CENTER (5), 1007 | CENTER_CROP (6), 1008 | CENTER_INSIDE (7); 1009 | 1010 | ScaleType(int ni) { 1011 | nativeInt = ni; 1012 | } 1013 | final int nativeInt; 1014 | } 1015 | 1016 | public ScaleType getScaleType() { 1017 | return mScaleType; 1018 | } 1019 | 1020 | /** 1021 | * Controls how the image should be resized or moved to match the size 1022 | * of this ImageView. 1023 | * 1024 | * @param scaleType The desired scaling mode. 1025 | * 1026 | */ 1027 | public void setScaleType(ScaleType scaleType) { 1028 | if (scaleType == null) { 1029 | throw new NullPointerException(); 1030 | } 1031 | 1032 | if (mScaleType != scaleType) { 1033 | mScaleType = scaleType; 1034 | 1035 | setWillNotCacheDrawing(mScaleType == ScaleType.CENTER); 1036 | 1037 | requestLayout(); 1038 | invalidate(); 1039 | } 1040 | } 1041 | 1042 | private ScaleType getScaleTypeFromInt(int i) { 1043 | switch (i) { 1044 | case 0: 1045 | return ScaleType.MATRIX; 1046 | case 1: 1047 | return ScaleType.FIT_XY; 1048 | case 2: 1049 | return ScaleType.FIT_START; 1050 | case 3: 1051 | return ScaleType.FIT_CENTER; 1052 | case 4: 1053 | return ScaleType.FIT_END; 1054 | case 5: 1055 | return ScaleType.CENTER; 1056 | case 6: 1057 | return ScaleType.CENTER_CROP; 1058 | case 7: 1059 | return ScaleType.CENTER_INSIDE; 1060 | default: 1061 | return ScaleType.FIT_CENTER; 1062 | } 1063 | } 1064 | 1065 | @Override 1066 | protected boolean verifyDrawable(Drawable who) { 1067 | return who == mStartDrawable || who == mEndDrawable || super.verifyDrawable(who); 1068 | } 1069 | 1070 | @Override 1071 | public void invalidateDrawable(@NonNull Drawable dr) { 1072 | if (dr == mStartDrawable || dr == mEndDrawable) { 1073 | /* we invalidate the whole view in this case because it's very 1074 | * hard to know where the drawable actually is. This is made 1075 | * complicated because of the offsets and transformations that 1076 | * can be applied. In theory we could get the drawable's bounds 1077 | * and run them through the transformation and offsets, but this 1078 | * is probably not worth the effort. 1079 | */ 1080 | invalidate(); 1081 | } else { 1082 | super.invalidateDrawable(dr); 1083 | } 1084 | } 1085 | } 1086 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/AnimatedVectorDrawable.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | /* 4 | * Copyright (C) 2015 Wael Nafee 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import com.wnafee.vector.R; 18 | 19 | import org.xmlpull.v1.XmlPullParser; 20 | import org.xmlpull.v1.XmlPullParserException; 21 | 22 | import android.animation.Animator; 23 | import android.animation.AnimatorInflater; 24 | import android.animation.AnimatorSet; 25 | import android.animation.ValueAnimator; 26 | import android.content.Context; 27 | import android.content.res.ColorStateList; 28 | import android.content.res.Resources; 29 | import android.content.res.Resources.Theme; 30 | import android.content.res.TypedArray; 31 | import android.graphics.Canvas; 32 | import android.graphics.ColorFilter; 33 | import android.graphics.PorterDuff; 34 | import android.graphics.Rect; 35 | import android.graphics.drawable.Animatable; 36 | import android.graphics.drawable.Drawable; 37 | import android.support.annotation.NonNull; 38 | import android.support.v4.util.ArrayMap; 39 | import android.util.AttributeSet; 40 | import android.util.Log; 41 | import android.util.Xml; 42 | 43 | import java.io.IOException; 44 | import java.util.ArrayList; 45 | 46 | //TODO: Add support for animator reversal 47 | public class AnimatedVectorDrawable extends DrawableCompat implements Animatable, Tintable { 48 | private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName(); 49 | 50 | private static final String ANIMATED_VECTOR = "animated-vector"; 51 | private static final String TARGET = "target"; 52 | 53 | private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; 54 | 55 | private AnimatedVectorDrawableState mAnimatedVectorState; 56 | 57 | private boolean mMutated; 58 | 59 | public AnimatedVectorDrawable() { 60 | mAnimatedVectorState = new AnimatedVectorDrawableState(null); 61 | } 62 | 63 | private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res, 64 | Theme theme) { 65 | mAnimatedVectorState = new AnimatedVectorDrawableState(state); 66 | if (theme != null && canApplyTheme()) { 67 | applyTheme(theme); 68 | } 69 | } 70 | 71 | @Override 72 | public Drawable mutate() { 73 | if (!mMutated && super.mutate() == this) { 74 | mAnimatedVectorState.mVectorDrawable.mutate(); 75 | mMutated = true; 76 | } 77 | return this; 78 | } 79 | 80 | @Override 81 | public Drawable.ConstantState getConstantState() { 82 | mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); 83 | return mAnimatedVectorState; 84 | } 85 | 86 | @Override 87 | public int getChangingConfigurations() { 88 | return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations; 89 | } 90 | 91 | @Override 92 | public void draw(Canvas canvas) { 93 | mAnimatedVectorState.mVectorDrawable.draw(canvas); 94 | if (isStarted()) { 95 | invalidateSelf(); 96 | } 97 | } 98 | 99 | @Override 100 | protected void onBoundsChange(Rect bounds) { 101 | mAnimatedVectorState.mVectorDrawable.setBounds(bounds); 102 | } 103 | 104 | @Override 105 | protected boolean onStateChange(int[] state) { 106 | return mAnimatedVectorState.mVectorDrawable.setState(state); 107 | } 108 | 109 | @Override 110 | protected boolean onLevelChange(int level) { 111 | return mAnimatedVectorState.mVectorDrawable.setLevel(level); 112 | } 113 | 114 | @Override 115 | public int getAlpha() { 116 | return mAnimatedVectorState.mVectorDrawable.getAlpha(); 117 | } 118 | 119 | @Override 120 | public void setAlpha(int alpha) { 121 | mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); 122 | } 123 | 124 | @Override 125 | public void setColorFilter(ColorFilter colorFilter) { 126 | mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); 127 | } 128 | 129 | @Override 130 | public void setTintList(ColorStateList tint) { 131 | mAnimatedVectorState.mVectorDrawable.setTintList(tint); 132 | } 133 | 134 | @Override 135 | public void setHotspot(float x, float y) { 136 | mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); 137 | } 138 | 139 | @Override 140 | public void setHotspotBounds(int left, int top, int right, int bottom) { 141 | mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); 142 | } 143 | 144 | @Override 145 | public void setTintMode(PorterDuff.Mode tintMode) { 146 | mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); 147 | } 148 | 149 | @Override 150 | public boolean setVisible(boolean visible, boolean restart) { 151 | mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); 152 | return super.setVisible(visible, restart); 153 | } 154 | 155 | public void setLayoutDirection(int layoutDirection) { 156 | mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); 157 | } 158 | 159 | @Override 160 | public boolean isStateful() { 161 | return mAnimatedVectorState.mVectorDrawable.isStateful(); 162 | } 163 | 164 | @Override 165 | public int getOpacity() { 166 | return mAnimatedVectorState.mVectorDrawable.getOpacity(); 167 | } 168 | 169 | @Override 170 | public int getIntrinsicWidth() { 171 | return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); 172 | } 173 | 174 | @Override 175 | public int getIntrinsicHeight() { 176 | return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); 177 | } 178 | 179 | @Override 180 | public void getOutline(@NonNull Outline outline) { 181 | mAnimatedVectorState.mVectorDrawable.getOutline(outline); 182 | } 183 | 184 | public static AnimatedVectorDrawable getDrawable(Context c, int resId) { 185 | return create(c, c.getResources(), resId); 186 | } 187 | 188 | public static AnimatedVectorDrawable create(Context c, Resources resources, int rid) { 189 | try { 190 | final XmlPullParser parser = resources.getXml(rid); 191 | final AttributeSet attrs = Xml.asAttributeSet(parser); 192 | int type; 193 | while ((type = parser.next()) != XmlPullParser.START_TAG && 194 | type != XmlPullParser.END_DOCUMENT) { 195 | // Empty loop 196 | } 197 | if (type != XmlPullParser.START_TAG) { 198 | throw new XmlPullParserException("No start tag found"); 199 | }else if (!ANIMATED_VECTOR.equals(parser.getName())) { 200 | throw new IllegalArgumentException("root node must start with: " + ANIMATED_VECTOR); 201 | } 202 | 203 | final AnimatedVectorDrawable drawable = new AnimatedVectorDrawable(); 204 | drawable.inflate(c, resources, parser, attrs, null); 205 | 206 | return drawable; 207 | } catch (XmlPullParserException e) { 208 | Log.e(LOGTAG, "parser error", e); 209 | } catch (IOException e) { 210 | Log.e(LOGTAG, "parser error", e); 211 | } 212 | return null; 213 | } 214 | 215 | public void inflate(Context c, Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) 216 | throws XmlPullParserException, IOException { 217 | 218 | int eventType = parser.getEventType(); 219 | float pathErrorScale = 1; 220 | while (eventType != XmlPullParser.END_DOCUMENT) { 221 | if (eventType == XmlPullParser.START_TAG) { 222 | final String tagName = parser.getName(); 223 | if (ANIMATED_VECTOR.equals(tagName)) { 224 | final TypedArray a = obtainAttributes(res, theme, attrs, 225 | R.styleable.AnimatedVectorDrawable); 226 | int drawableRes = a.getResourceId( 227 | R.styleable.AnimatedVectorDrawable_android_drawable, 0); 228 | if (drawableRes != 0) { 229 | VectorDrawable vectorDrawable = (VectorDrawable) VectorDrawable.create(res, drawableRes).mutate(); 230 | vectorDrawable.setAllowCaching(false); 231 | pathErrorScale = vectorDrawable.getPixelSize(); 232 | mAnimatedVectorState.mVectorDrawable = vectorDrawable; 233 | } 234 | a.recycle(); 235 | } else if (TARGET.equals(tagName)) { 236 | final TypedArray a = obtainAttributes(res, theme, attrs, 237 | R.styleable.AnimatedVectorDrawableTarget); 238 | final String target = a.getString( 239 | R.styleable.AnimatedVectorDrawableTarget_android_name); 240 | 241 | int id = a.getResourceId( 242 | R.styleable.AnimatedVectorDrawableTarget_android_animation, 0); 243 | if (id != 0) { 244 | //path animators require separate handling 245 | Animator objectAnimator; 246 | if (isPath(target)) { 247 | objectAnimator = getPathAnimator(c, res, theme, id, pathErrorScale); 248 | } else { 249 | objectAnimator = AnimatorInflater.loadAnimator(c, id); 250 | } 251 | setupAnimatorsForTarget(target, objectAnimator); 252 | } 253 | a.recycle(); 254 | } 255 | } 256 | 257 | eventType = parser.next(); 258 | } 259 | } 260 | 261 | public boolean isPath(String target) { 262 | Object o = mAnimatedVectorState.mVectorDrawable.getTargetByName(target); 263 | return (o instanceof VectorDrawable.VFullPath); 264 | } 265 | 266 | Animator getPathAnimator(Context c, Resources res, Theme theme, int id, float pathErrorScale) { 267 | return PathAnimatorInflater.loadAnimator(c, res, theme, id, pathErrorScale); 268 | } 269 | 270 | @Override 271 | public boolean canApplyTheme() { 272 | return super.canApplyTheme() || mAnimatedVectorState != null 273 | && mAnimatedVectorState.mVectorDrawable != null 274 | && mAnimatedVectorState.mVectorDrawable.canApplyTheme(); 275 | } 276 | 277 | @Override 278 | public void applyTheme(Theme t) { 279 | super.applyTheme(t); 280 | 281 | final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; 282 | if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { 283 | vectorDrawable.applyTheme(t); 284 | } 285 | } 286 | 287 | private static class AnimatedVectorDrawableState extends Drawable.ConstantState { 288 | int mChangingConfigurations; 289 | VectorDrawable mVectorDrawable; 290 | ArrayList mAnimators; 291 | ArrayMap mTargetNameMap; 292 | 293 | public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy) { 294 | if (copy != null) { 295 | mChangingConfigurations = copy.mChangingConfigurations; 296 | if (copy.mVectorDrawable != null) { 297 | mVectorDrawable = (VectorDrawable) copy.mVectorDrawable.getConstantState().newDrawable(); 298 | mVectorDrawable.mutate(); 299 | mVectorDrawable.setAllowCaching(false); 300 | mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); 301 | } 302 | if (copy.mAnimators != null) { 303 | final int numAnimators = copy.mAnimators.size(); 304 | mAnimators = new ArrayList(numAnimators); 305 | mTargetNameMap = new ArrayMap(numAnimators); 306 | for (int i = 0; i < numAnimators; ++i) { 307 | Animator anim = copy.mAnimators.get(i); 308 | Animator animClone = anim.clone(); 309 | String targetName = copy.mTargetNameMap.get(anim); 310 | Object targetObject = mVectorDrawable.getTargetByName(targetName); 311 | animClone.setTarget(targetObject); 312 | mAnimators.add(animClone); 313 | mTargetNameMap.put(animClone, targetName); 314 | } 315 | } 316 | } else { 317 | mVectorDrawable = new VectorDrawable(); 318 | } 319 | } 320 | 321 | @Override 322 | public Drawable newDrawable() { 323 | return new AnimatedVectorDrawable(this, null, null); 324 | } 325 | 326 | @Override 327 | public Drawable newDrawable(Resources res) { 328 | return new AnimatedVectorDrawable(this, res, null); 329 | } 330 | 331 | @Override 332 | public Drawable newDrawable(Resources res, Theme theme) { 333 | return new AnimatedVectorDrawable(this, res, theme); 334 | } 335 | 336 | @Override 337 | public int getChangingConfigurations() { 338 | return mChangingConfigurations; 339 | } 340 | } 341 | 342 | private void setupAnimatorsForTarget(String name, Animator animator) { 343 | Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name); 344 | animator.setTarget(target); 345 | if (mAnimatedVectorState.mAnimators == null) { 346 | mAnimatedVectorState.mAnimators = new ArrayList(); 347 | mAnimatedVectorState.mTargetNameMap = new ArrayMap(); 348 | } 349 | mAnimatedVectorState.mAnimators.add(animator); 350 | mAnimatedVectorState.mTargetNameMap.put(animator, name); 351 | if (DBG_ANIMATION_VECTOR_DRAWABLE) { 352 | Log.v(LOGTAG, "add animator for target " + name + " " + animator); 353 | } 354 | } 355 | 356 | @Override 357 | public boolean isRunning() { 358 | final ArrayList animators = mAnimatedVectorState.mAnimators; 359 | final int size = animators.size(); 360 | for (int i = 0; i < size; i++) { 361 | final Animator animator = animators.get(i); 362 | if (animator.isRunning()) { 363 | return true; 364 | } 365 | } 366 | return false; 367 | } 368 | 369 | private boolean isStarted() { 370 | final ArrayList animators = mAnimatedVectorState.mAnimators; 371 | final int size = animators.size(); 372 | for (int i = 0; i < size; i++) { 373 | final Animator animator = animators.get(i); 374 | if (animator.isStarted()) { 375 | return true; 376 | } 377 | } 378 | return false; 379 | } 380 | 381 | @Override 382 | public void start() { 383 | final ArrayList animators = mAnimatedVectorState.mAnimators; 384 | final int size = animators.size(); 385 | for (int i = 0; i < size; i++) { 386 | final Animator animator = animators.get(i); 387 | if (!animator.isStarted()) { 388 | animator.start(); 389 | } 390 | } 391 | invalidateSelf(); 392 | } 393 | 394 | @Override 395 | public void stop() { 396 | final ArrayList animators = mAnimatedVectorState.mAnimators; 397 | final int size = animators.size(); 398 | for (int i = 0; i < size; i++) { 399 | final Animator animator = animators.get(i); 400 | animator.end(); 401 | } 402 | } 403 | 404 | /** 405 | * Reverses ongoing animations or starts pending animations in reverse. 406 | *

407 | * NOTE: Only works of all animations are ValueAnimators. 408 | */ 409 | public void reverse() { 410 | final ArrayList animators = mAnimatedVectorState.mAnimators; 411 | final int size = animators.size(); 412 | for (int i = 0; i < size; i++) { 413 | final Animator animator = animators.get(i); 414 | if (canReverse(animator)) { 415 | reverse(animator); 416 | } else { 417 | Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); 418 | } 419 | } 420 | } 421 | 422 | public boolean canReverse() { 423 | final ArrayList animators = mAnimatedVectorState.mAnimators; 424 | final int size = animators.size(); 425 | for (int i = 0; i < size; i++) { 426 | final Animator animator = animators.get(i); 427 | if (!canReverse(animator)) { 428 | return false; 429 | } 430 | } 431 | return true; 432 | } 433 | 434 | public static boolean canReverse(Animator a) { 435 | if (a instanceof AnimatorSet) { 436 | final ArrayList animators = ((AnimatorSet)a).getChildAnimations(); 437 | for(Animator anim: animators) { 438 | if(!canReverse(anim)) { 439 | return false; 440 | } 441 | } 442 | } else if (a instanceof ValueAnimator) { 443 | return true; 444 | } 445 | 446 | return false; 447 | } 448 | 449 | private void reverse(Animator a) { 450 | if (a instanceof AnimatorSet) { 451 | final ArrayList animators = ((AnimatorSet)a).getChildAnimations(); 452 | for(Animator anim: animators) { 453 | reverse(anim); 454 | } 455 | } else if (a instanceof ValueAnimator) { 456 | ((ValueAnimator)a).reverse(); 457 | } 458 | } 459 | 460 | } 461 | 462 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/DrawableCompat.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | /* 4 | * Copyright (C) 2015 Wael Nafee 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import android.content.res.ColorStateList; 18 | import android.content.res.Resources; 19 | import android.content.res.TypedArray; 20 | import android.graphics.Color; 21 | import android.graphics.PorterDuff; 22 | import android.graphics.PorterDuffColorFilter; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Drawable; 25 | import android.support.annotation.NonNull; 26 | import android.util.AttributeSet; 27 | 28 | public abstract class DrawableCompat extends Drawable { 29 | 30 | int mLayoutDirection; 31 | 32 | public static abstract class ConstantStateCompat extends ConstantState { 33 | 34 | public boolean canApplyTheme() { 35 | return false; 36 | } 37 | } 38 | 39 | public boolean canApplyTheme() { 40 | return false; 41 | } 42 | 43 | protected static TypedArray obtainAttributes(Resources res, Resources.Theme theme, AttributeSet set, int[] attrs) { 44 | if (theme == null) { 45 | return res.obtainAttributes(set, attrs); 46 | } 47 | return theme.obtainStyledAttributes(set, attrs, 0, 0); 48 | } 49 | 50 | public void getOutline(@NonNull Outline outline) { 51 | outline.setRect(getBounds()); 52 | outline.setAlpha(0); 53 | } 54 | 55 | /** 56 | * Specifies the hotspot's location within the drawable. 57 | * 58 | * @param x The X coordinate of the center of the hotspot 59 | * @param y The Y coordinate of the center of the hotspot 60 | */ 61 | public void setHotspot(float x, float y) { 62 | } 63 | 64 | /** 65 | * Sets the bounds to which the hotspot is constrained, if they should be 66 | * different from the drawable bounds. 67 | * 68 | * @param left 69 | * @param top 70 | * @param right 71 | * @param bottom 72 | */ 73 | public void setHotspotBounds(int left, int top, int right, int bottom) { 74 | } 75 | 76 | public void getHotspotBounds(Rect outRect) { 77 | outRect.set(getBounds()); 78 | } 79 | 80 | public int getLayoutDirection() { 81 | return mLayoutDirection; 82 | } 83 | 84 | public void setLayoutDirection(int layoutDirection) { 85 | if (getLayoutDirection() != layoutDirection) { 86 | mLayoutDirection = layoutDirection; 87 | } 88 | } 89 | 90 | /** 91 | * Ensures the tint filter is consistent with the current tint color and 92 | * mode. 93 | */ 94 | PorterDuffColorFilter updateTintFilter(PorterDuffColorFilter tintFilter, ColorStateList tint, 95 | PorterDuff.Mode tintMode) { 96 | if (tint == null || tintMode == null) { 97 | return null; 98 | } 99 | 100 | final int color = tint.getColorForState(getState(), Color.TRANSPARENT); 101 | tintFilter = new PorterDuffColorFilter(color, tintMode); 102 | 103 | return tintFilter; 104 | } 105 | 106 | /** 107 | * Parses a {@link android.graphics.PorterDuff.Mode} from a tintMode 108 | * attribute's enum value. 109 | * 110 | * @hide 111 | */ 112 | public static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 113 | switch (value) { 114 | case 3: return PorterDuff.Mode.SRC_OVER; 115 | case 5: return PorterDuff.Mode.SRC_IN; 116 | case 9: return PorterDuff.Mode.SRC_ATOP; 117 | case 14: return PorterDuff.Mode.MULTIPLY; 118 | case 15: return PorterDuff.Mode.SCREEN; 119 | case 16: return PorterDuff.Mode.ADD; 120 | default: return defaultMode; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/Outline.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | /* 4 | * Copyright (C) 2013 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.graphics.Path; 20 | import android.graphics.Rect; 21 | import android.support.annotation.NonNull; 22 | 23 | /** 24 | * Defines a simple shape, used for bounding graphical regions. 25 | *

26 | * Can be computed for a View, or computed by a Drawable, to drive the shape of 27 | * shadows cast by a View, or to clip the contents of the View. 28 | * 29 | * @see android.view.ViewOutlineProvider 30 | * @see android.view.View#setOutlineProvider(android.view.ViewOutlineProvider) 31 | */ 32 | public final class Outline { 33 | /** @hide */ 34 | public Path mPath; 35 | 36 | /** @hide */ 37 | public Rect mRect; 38 | /** @hide */ 39 | public float mRadius; 40 | /** @hide */ 41 | public float mAlpha; 42 | 43 | /** 44 | * Constructs an empty Outline. Call one of the setter methods to make 45 | * the outline valid for use with a View. 46 | */ 47 | public Outline() {} 48 | 49 | /** 50 | * Constructs an Outline with a copy of the data in src. 51 | */ 52 | public Outline(@NonNull Outline src) { 53 | set(src); 54 | } 55 | 56 | /** 57 | * Sets the outline to be empty. 58 | * 59 | * @see #isEmpty() 60 | */ 61 | public void setEmpty() { 62 | mPath = null; 63 | mRect = null; 64 | mRadius = 0; 65 | } 66 | 67 | /** 68 | * Returns whether the Outline is empty. 69 | *

70 | * Outlines are empty when constructed, or if {@link #setEmpty()} is called, 71 | * until a setter method is called 72 | * 73 | * @see #setEmpty() 74 | */ 75 | public boolean isEmpty() { 76 | return mRect == null && mPath == null; 77 | } 78 | 79 | 80 | /** 81 | * Returns whether the outline can be used to clip a View. 82 | *

83 | * Currently, only Outlines that can be represented as a rectangle, circle, 84 | * or round rect support clipping. 85 | * 86 | * @see {@link android.view.View#setClipToOutline(boolean)} 87 | */ 88 | public boolean canClip() { 89 | return !isEmpty() && mRect != null; 90 | } 91 | 92 | /** 93 | * Sets the alpha represented by the Outline - the degree to which the 94 | * producer is guaranteed to be opaque over the Outline's shape. 95 | *

96 | * An alpha value of 0.0f either represents completely 97 | * transparent content, or content that isn't guaranteed to fill the shape 98 | * it publishes. 99 | *

100 | * Content producing a fully opaque (alpha = 1.0f) outline is 101 | * assumed by the drawing system to fully cover content beneath it, 102 | * meaning content beneath may be optimized away. 103 | */ 104 | public void setAlpha(float alpha) { 105 | mAlpha = alpha; 106 | } 107 | 108 | /** 109 | * Returns the alpha represented by the Outline. 110 | */ 111 | public float getAlpha() { 112 | return mAlpha; 113 | } 114 | 115 | /** 116 | * Replace the contents of this Outline with the contents of src. 117 | * 118 | * @param src Source outline to copy from. 119 | */ 120 | public void set(@NonNull Outline src) { 121 | if (src.mPath != null) { 122 | if (mPath == null) { 123 | mPath = new Path(); 124 | } 125 | mPath.set(src.mPath); 126 | mRect = null; 127 | } 128 | if (src.mRect != null) { 129 | if (mRect == null) { 130 | mRect = new Rect(); 131 | } 132 | mRect.set(src.mRect); 133 | } 134 | mRadius = src.mRadius; 135 | mAlpha = src.mAlpha; 136 | } 137 | 138 | /** 139 | * Sets the Outline to the rounded rect defined by the input rect, and 140 | * corner radius. 141 | */ 142 | public void setRect(int left, int top, int right, int bottom) { 143 | setRoundRect(left, top, right, bottom, 0.0f); 144 | } 145 | 146 | /** 147 | * Convenience for {@link #setRect(int, int, int, int)} 148 | */ 149 | public void setRect(@NonNull Rect rect) { 150 | setRect(rect.left, rect.top, rect.right, rect.bottom); 151 | } 152 | 153 | /** 154 | * Sets the Outline to the rounded rect defined by the input rect, and corner radius. 155 | *

156 | * Passing a zero radius is equivalent to calling {@link #setRect(int, int, int, int)} 157 | */ 158 | public void setRoundRect(int left, int top, int right, int bottom, float radius) { 159 | if (left >= right || top >= bottom) { 160 | setEmpty(); 161 | return; 162 | } 163 | 164 | if (mRect == null) mRect = new Rect(); 165 | mRect.set(left, top, right, bottom); 166 | mRadius = radius; 167 | mPath = null; 168 | } 169 | 170 | /** 171 | * Convenience for {@link #setRoundRect(int, int, int, int, float)} 172 | */ 173 | public void setRoundRect(@NonNull Rect rect, float radius) { 174 | setRoundRect(rect.left, rect.top, rect.right, rect.bottom, radius); 175 | } 176 | 177 | // /** 178 | // * Sets the outline to the oval defined by input rect. 179 | // */ 180 | // public void setOval(int left, int top, int right, int bottom) { 181 | // if (left >= right || top >= bottom) { 182 | // setEmpty(); 183 | // return; 184 | // } 185 | // 186 | // if ((bottom - top) == (right - left)) { 187 | // // represent circle as round rect, for efficiency, and to enable clipping 188 | // setRoundRect(left, top, right, bottom, (bottom - top) / 2.0f); 189 | // return; 190 | // } 191 | // 192 | // if (mPath == null) mPath = new Path(); 193 | // mPath.reset(); 194 | // mPath.addOval(left, top, right, bottom, Path.Direction.CW); 195 | // mRect = null; 196 | // } 197 | // 198 | // /** 199 | // * Convenience for {@link #setOval(int, int, int, int)} 200 | // */ 201 | // public void setOval(@NonNull Rect rect) { 202 | // setOval(rect.left, rect.top, rect.right, rect.bottom); 203 | // } 204 | // 205 | // /** 206 | // * Sets the Constructs an Outline from a 207 | // * {@link android.graphics.Path#isConvex() convex path}. 208 | // */ 209 | // public void setConvexPath(@NonNull Path convexPath) { 210 | // if (convexPath.isEmpty()) { 211 | // setEmpty(); 212 | // return; 213 | // } 214 | // 215 | // if (!convexPath.isConvex()) { 216 | // throw new IllegalArgumentException("path must be convex"); 217 | // } 218 | // if (mPath == null) mPath = new Path(); 219 | // 220 | // mPath.set(convexPath); 221 | // mRect = null; 222 | // mRadius = -1.0f; 223 | // } 224 | } 225 | 226 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/PathAnimatorInflater.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | /* 4 | * Copyright (C) 2015 Wael Nafee 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | import android.animation.Animator; 17 | import android.animation.AnimatorSet; 18 | import android.animation.ObjectAnimator; 19 | import android.animation.TypeEvaluator; 20 | import android.animation.ValueAnimator; 21 | import android.content.Context; 22 | import android.content.res.Resources; 23 | import android.content.res.TypedArray; 24 | import android.content.res.XmlResourceParser; 25 | import android.util.AttributeSet; 26 | import android.util.Log; 27 | import android.util.Xml; 28 | import android.view.InflateException; 29 | import android.view.animation.AnimationUtils; 30 | 31 | import com.wnafee.vector.R; 32 | 33 | import org.xmlpull.v1.XmlPullParser; 34 | import org.xmlpull.v1.XmlPullParserException; 35 | 36 | import java.io.IOException; 37 | import java.util.ArrayList; 38 | 39 | 40 | public class PathAnimatorInflater { 41 | 42 | private static final String TAG = "PathAnimatorInflater"; 43 | /** 44 | * These flags are used when parsing PathAnimatorSet objects 45 | */ 46 | private static final int TOGETHER = 0; 47 | private static final int SEQUENTIALLY = 1; 48 | 49 | private static final int VALUE_TYPE_PATH = 2; 50 | 51 | private static final boolean DBG_ANIMATOR_INFLATER = false; 52 | 53 | public static Animator loadAnimator(Context c, Resources resources, Resources.Theme theme, int id, 54 | float pathErrorScale) throws Resources.NotFoundException { 55 | 56 | XmlResourceParser parser = null; 57 | try { 58 | parser = resources.getAnimation(id); 59 | return createAnimatorFromXml(c, resources, theme, parser, pathErrorScale); 60 | } catch (XmlPullParserException ex) { 61 | Resources.NotFoundException rnf = 62 | new Resources.NotFoundException("Can't load animation resource ID #0x" + 63 | Integer.toHexString(id)); 64 | rnf.initCause(ex); 65 | throw rnf; 66 | } catch (IOException ex) { 67 | Resources.NotFoundException rnf = 68 | new Resources.NotFoundException("Can't load animation resource ID #0x" + 69 | Integer.toHexString(id)); 70 | rnf.initCause(ex); 71 | throw rnf; 72 | } finally { 73 | if (parser != null) parser.close(); 74 | } 75 | } 76 | 77 | private static Animator createAnimatorFromXml(Context c, Resources res, Resources.Theme theme, XmlPullParser parser, 78 | float pixelSize) 79 | throws XmlPullParserException, IOException { 80 | return createAnimatorFromXml(c, res, theme, parser, Xml.asAttributeSet(parser), null, 0, 81 | pixelSize); 82 | } 83 | 84 | private static Animator createAnimatorFromXml(Context c, Resources res, Resources.Theme theme, XmlPullParser parser, 85 | AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize) 86 | throws XmlPullParserException, IOException { 87 | 88 | Animator anim = null; 89 | ArrayList childAnims = null; 90 | 91 | // Make sure we are on a start tag. 92 | int type; 93 | int depth = parser.getDepth(); 94 | 95 | while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 96 | && type != XmlPullParser.END_DOCUMENT) { 97 | 98 | if (type != XmlPullParser.START_TAG) { 99 | continue; 100 | } 101 | 102 | String name = parser.getName(); 103 | 104 | if (name.equals("objectAnimator")) { 105 | anim = loadObjectAnimator(c, res, theme, attrs, pixelSize); 106 | } else if (name.equals("animator")) { 107 | anim = loadAnimator(c, res, theme, attrs, null, pixelSize); 108 | } else if (name.equals("set")) { 109 | anim = new AnimatorSet(); 110 | //TODO: don't care about 'set' attributes for now 111 | // TypedArray a; 112 | // if (theme != null) { 113 | // a = theme.obtainStyledAttributes(attrs, AnimatorSet, 0, 0); 114 | // } else { 115 | // a = res.obtainAttributes(attrs, AnimatorSet); 116 | // } 117 | // int ordering = a.getInt(R.styleable.AnimatorSet_ordering, 118 | // TOGETHER); 119 | createAnimatorFromXml(c, res, theme, parser, attrs, (AnimatorSet) anim, TOGETHER, 120 | pixelSize); 121 | // a.recycle(); 122 | } else { 123 | throw new RuntimeException("Unknown animator name: " + parser.getName()); 124 | } 125 | 126 | if (parent != null) { 127 | if (childAnims == null) { 128 | childAnims = new ArrayList(); 129 | } 130 | childAnims.add(anim); 131 | } 132 | } 133 | if (parent != null && childAnims != null) { 134 | Animator[] animsArray = new Animator[childAnims.size()]; 135 | int index = 0; 136 | for (Animator a : childAnims) { 137 | animsArray[index++] = a; 138 | } 139 | if (sequenceOrdering == TOGETHER) { 140 | parent.playTogether(animsArray); 141 | } else { 142 | parent.playSequentially(animsArray); 143 | } 144 | } 145 | 146 | return anim; 147 | 148 | } 149 | 150 | private static ObjectAnimator loadObjectAnimator(Context c, Resources res, Resources.Theme theme, AttributeSet attrs, 151 | float pathErrorScale) throws Resources.NotFoundException { 152 | ObjectAnimator anim = new ObjectAnimator(); 153 | 154 | loadAnimator(c, res, theme, attrs, anim, pathErrorScale); 155 | 156 | return anim; 157 | } 158 | 159 | /** 160 | * Creates a new animation whose parameters come from the specified context 161 | * and attributes set. 162 | * 163 | * @param res The resources 164 | * @param attrs The set of attributes holding the animation parameters 165 | * @param anim Null if this is a ValueAnimator, otherwise this is an 166 | * ObjectAnimator 167 | */ 168 | private static ValueAnimator loadAnimator(Context c, Resources res, Resources.Theme theme, 169 | AttributeSet attrs, ValueAnimator anim, float pathErrorScale) 170 | throws Resources.NotFoundException { 171 | 172 | TypedArray arrayAnimator = null; 173 | TypedArray arrayObjectAnimator = null; 174 | 175 | if (theme != null) { 176 | arrayAnimator = theme.obtainStyledAttributes(attrs, R.styleable.Animator, 0, 0); 177 | } else { 178 | arrayAnimator = res.obtainAttributes(attrs, R.styleable.Animator); 179 | } 180 | 181 | // If anim is not null, then it is an object animator. 182 | if (anim != null) { 183 | if (theme != null) { 184 | arrayObjectAnimator = theme.obtainStyledAttributes(attrs, 185 | R.styleable.PropertyAnimator, 0, 0); 186 | } else { 187 | arrayObjectAnimator = res.obtainAttributes(attrs, R.styleable.PropertyAnimator); 188 | } 189 | } 190 | 191 | if (anim == null) { 192 | anim = new ValueAnimator(); 193 | } 194 | 195 | parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator); 196 | 197 | final int resId = 198 | arrayAnimator.getResourceId(R.styleable.Animator_android_interpolator, 0); 199 | if (resId > 0) { 200 | anim.setInterpolator(AnimationUtils.loadInterpolator(c, resId)); 201 | } 202 | 203 | arrayAnimator.recycle(); 204 | if (arrayObjectAnimator != null) { 205 | arrayObjectAnimator.recycle(); 206 | } 207 | 208 | return anim; 209 | } 210 | 211 | /** 212 | * @param anim The animator, must not be null 213 | * @param arrayAnimator Incoming typed array for Animator's attributes. 214 | * @param arrayObjectAnimator Incoming typed array for Object Animator's 215 | * attributes. 216 | */ 217 | private static void parseAnimatorFromTypeArray(ValueAnimator anim, 218 | TypedArray arrayAnimator, TypedArray arrayObjectAnimator) { 219 | long duration = arrayAnimator.getInt(R.styleable.Animator_android_duration, 300); 220 | 221 | long startDelay = arrayAnimator.getInt(R.styleable.Animator_android_startOffset, 0); 222 | 223 | int valueType = arrayAnimator.getInt(R.styleable.Animator_vc_valueType, 0); 224 | 225 | TypeEvaluator evaluator = null; 226 | 227 | // Must be a path animator by the time I reach here 228 | if (valueType == VALUE_TYPE_PATH) { 229 | evaluator = setupAnimatorForPath(anim, arrayAnimator); 230 | } else { 231 | throw new IllegalArgumentException("target is not a pathType target"); 232 | } 233 | 234 | anim.setDuration(duration); 235 | anim.setStartDelay(startDelay); 236 | 237 | if (arrayAnimator.hasValue(R.styleable.Animator_android_repeatCount)) { 238 | anim.setRepeatCount( 239 | arrayAnimator.getInt(R.styleable.Animator_android_repeatCount, 0)); 240 | } 241 | if (arrayAnimator.hasValue(R.styleable.Animator_android_repeatMode)) { 242 | anim.setRepeatMode( 243 | arrayAnimator.getInt(R.styleable.Animator_android_repeatMode, 244 | ValueAnimator.RESTART)); 245 | } 246 | if (evaluator != null) { 247 | anim.setEvaluator(evaluator); 248 | } 249 | 250 | if (arrayObjectAnimator != null) { 251 | setupObjectAnimator(anim, arrayObjectAnimator); 252 | } 253 | } 254 | 255 | /** 256 | * Setup the Animator to achieve path morphing. 257 | * 258 | * @param anim The target Animator which will be updated. 259 | * @param arrayAnimator TypedArray for the ValueAnimator. 260 | * @return the PathDataEvaluator. 261 | */ 262 | private static TypeEvaluator setupAnimatorForPath(ValueAnimator anim, 263 | TypedArray arrayAnimator) { 264 | TypeEvaluator evaluator = null; 265 | String fromString = arrayAnimator.getString(R.styleable.Animator_android_valueFrom); 266 | String toString = arrayAnimator.getString(R.styleable.Animator_android_valueTo); 267 | PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); 268 | PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); 269 | 270 | if (nodesFrom != null) { 271 | if (nodesTo != null) { 272 | anim.setObjectValues(nodesFrom, nodesTo); 273 | if (!PathParser.canMorph(nodesFrom, nodesTo)) { 274 | throw new InflateException(arrayAnimator.getPositionDescription() 275 | + " Can't morph from " + fromString + " to " + toString); 276 | } 277 | } else { 278 | anim.setObjectValues((Object)nodesFrom); 279 | } 280 | evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); 281 | } else if (nodesTo != null) { 282 | anim.setObjectValues((Object)nodesTo); 283 | evaluator = new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); 284 | } 285 | 286 | if (DBG_ANIMATOR_INFLATER && evaluator != null) { 287 | Log.v(TAG, "create a new PathDataEvaluator here"); 288 | } 289 | 290 | return evaluator; 291 | } 292 | 293 | /** 294 | * Setup ObjectAnimator's property or values from pathData. 295 | * 296 | * @param anim The target Animator which will be updated. 297 | * @param arrayObjectAnimator TypedArray for the ObjectAnimator. 298 | * 299 | */ 300 | private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator) { 301 | ObjectAnimator oa = (ObjectAnimator) anim; 302 | String propertyName = 303 | arrayObjectAnimator.getString(R.styleable.PropertyAnimator_android_propertyName); 304 | oa.setPropertyName(propertyName); 305 | } 306 | 307 | /** 308 | * PathDataEvaluator is used to interpolate between two paths which are 309 | * represented in the same format but different control points' values. 310 | * The path is represented as an array of PathDataNode here, which is 311 | * fundamentally an array of floating point numbers. 312 | */ 313 | private static class PathDataEvaluator implements TypeEvaluator { 314 | private PathParser.PathDataNode[] mNodeArray; 315 | 316 | /** 317 | * Create a PathParser.PathDataNode[] that does not reuse the animated value. 318 | * Care must be taken when using this option because on every evaluation 319 | * a new PathParser.PathDataNode[] will be allocated. 320 | */ 321 | private PathDataEvaluator() {} 322 | 323 | /** 324 | * Create a PathDataEvaluator that reuses nodeArray for every evaluate() call. 325 | * Caution must be taken to ensure that the value returned from 326 | * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or 327 | * used across threads. The value will be modified on each evaluate() call. 328 | * 329 | * @param nodeArray The array to modify and return from evaluate. 330 | */ 331 | public PathDataEvaluator(PathParser.PathDataNode[] nodeArray) { 332 | mNodeArray = nodeArray; 333 | } 334 | 335 | @Override 336 | public PathParser.PathDataNode[] evaluate(float fraction, 337 | PathParser.PathDataNode[] startPathData, 338 | PathParser.PathDataNode[] endPathData) { 339 | if (!PathParser.canMorph(startPathData, endPathData)) { 340 | throw new IllegalArgumentException("Can't interpolate between" 341 | + " two incompatible pathData"); 342 | } 343 | 344 | if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) { 345 | mNodeArray = PathParser.deepCopyNodes(startPathData); 346 | } 347 | 348 | for (int i = 0; i < startPathData.length; i++) { 349 | mNodeArray[i].interpolatePathDataNode(startPathData[i], 350 | endPathData[i], fraction); 351 | } 352 | 353 | return mNodeArray; 354 | } 355 | } 356 | 357 | } 358 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/PathParser.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | /* 4 | * Copyright (C) 2013 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.graphics.Path; 20 | import android.util.Log; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | 25 | 26 | public class PathParser { 27 | static final String LOGTAG = PathParser.class.getSimpleName(); 28 | 29 | /** 30 | * @param pathData The string representing a path, the same as "d" string in svg file. 31 | * @return the generated Path object. 32 | */ 33 | public static Path createPathFromPathData(String pathData) { 34 | Path path = new Path(); 35 | PathDataNode[] nodes = createNodesFromPathData(pathData); 36 | if (nodes != null) { 37 | PathDataNode.nodesToPath(nodes, path); 38 | return path; 39 | } 40 | return null; 41 | } 42 | 43 | /** 44 | * @param pathData The string representing a path, the same as "d" string in svg file. 45 | * @return an array of the PathDataNode. 46 | */ 47 | public static PathDataNode[] createNodesFromPathData(String pathData) { 48 | if (pathData == null) { 49 | return null; 50 | } 51 | int start = 0; 52 | int end = 1; 53 | 54 | ArrayList list = new ArrayList(); 55 | while (end < pathData.length()) { 56 | end = nextStart(pathData, end); 57 | String s = pathData.substring(start, end).trim(); 58 | if (s.length() > 0) { 59 | float[] val = getFloats(s); 60 | addNode(list, s.charAt(0), val); 61 | } 62 | 63 | start = end; 64 | end++; 65 | } 66 | if ((end - start) == 1 && start < pathData.length()) { 67 | addNode(list, pathData.charAt(start), new float[0]); 68 | } 69 | return list.toArray(new PathDataNode[list.size()]); 70 | } 71 | 72 | /** 73 | * @param source The array of PathDataNode to be duplicated. 74 | * @return a deep copy of the source. 75 | */ 76 | public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { 77 | if (source == null) { 78 | return null; 79 | } 80 | PathDataNode[] copy = new PathDataNode[source.length]; 81 | for (int i = 0; i < source.length; i ++) { 82 | copy[i] = new PathDataNode(source[i]); 83 | } 84 | return copy; 85 | } 86 | 87 | /** 88 | * @param nodesFrom The source path represented in an array of PathDataNode 89 | * @param nodesTo The target path represented in an array of PathDataNode 90 | * @return whether the nodesFrom can morph into nodesTo 91 | */ 92 | public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) { 93 | if (nodesFrom == null || nodesTo == null) { 94 | return false; 95 | } 96 | 97 | if (nodesFrom.length != nodesTo.length) { 98 | return false; 99 | } 100 | 101 | for (int i = 0; i < nodesFrom.length; i ++) { 102 | if (nodesFrom[i].mType != nodesTo[i].mType 103 | || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) { 104 | return false; 105 | } 106 | } 107 | return true; 108 | } 109 | 110 | /** 111 | * Update the target's data to match the source. 112 | * Before calling this, make sure canMorph(target, source) is true. 113 | * 114 | * @param target The target path represented in an array of PathDataNode 115 | * @param source The source path represented in an array of PathDataNode 116 | */ 117 | public static void updateNodes(PathDataNode[] target, PathDataNode[] source) { 118 | for (int i = 0; i < source.length; i ++) { 119 | target[i].mType = source[i].mType; 120 | for (int j = 0; j < source[i].mParams.length; j ++) { 121 | target[i].mParams[j] = source[i].mParams[j]; 122 | } 123 | } 124 | } 125 | 126 | private static int nextStart(String s, int end) { 127 | char c; 128 | 129 | while (end < s.length()) { 130 | c = s.charAt(end); 131 | if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { 132 | return end; 133 | } 134 | end++; 135 | } 136 | return end; 137 | } 138 | 139 | private static void addNode(ArrayList list, char cmd, float[] val) { 140 | list.add(new PathDataNode(cmd, val)); 141 | } 142 | 143 | private static class ExtractFloatResult { 144 | // We need to return the position of the next separator and whether the 145 | // next float starts with a '-'. 146 | int mEndPosition; 147 | boolean mEndWithNegSign; 148 | } 149 | 150 | /** 151 | * Parse the floats in the string. 152 | * This is an optimized version of parseFloat(s.split(",|\\s")); 153 | * 154 | * @param s the string containing a command and list of floats 155 | * @return array of floats 156 | */ 157 | private static float[] getFloats(String s) { 158 | if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { 159 | return new float[0]; 160 | } 161 | try { 162 | float[] results = new float[s.length()]; 163 | int count = 0; 164 | int startPosition = 1; 165 | int endPosition = 0; 166 | 167 | ExtractFloatResult result = new ExtractFloatResult(); 168 | int totalLength = s.length(); 169 | 170 | // The startPosition should always be the first character of the 171 | // current number, and endPosition is the character after the current 172 | // number. 173 | while (startPosition < totalLength) { 174 | extract(s, startPosition, result); 175 | endPosition = result.mEndPosition; 176 | 177 | if (startPosition < endPosition) { 178 | results[count++] = Float.parseFloat( 179 | s.substring(startPosition, endPosition)); 180 | } 181 | 182 | if (result.mEndWithNegSign) { 183 | // Keep the '-' sign with next number. 184 | startPosition = endPosition; 185 | } else { 186 | startPosition = endPosition + 1; 187 | } 188 | } 189 | return Arrays.copyOf(results, count); 190 | } catch (NumberFormatException e) { 191 | Log.e(LOGTAG, "error in parsing \"" + s + "\""); 192 | throw e; 193 | } 194 | } 195 | 196 | /** 197 | * Calculate the position of the next comma or space or negative sign 198 | * @param s the string to search 199 | * @param start the position to start searching 200 | * @param result the result of the extraction, including the position of the 201 | * the starting position of next number, whether it is ending with a '-'. 202 | */ 203 | private static void extract(String s, int start, ExtractFloatResult result) { 204 | // Now looking for ' ', ',' or '-' from the start. 205 | int currentIndex = start; 206 | boolean foundSeparator = false; 207 | result.mEndWithNegSign = false; 208 | for (; currentIndex < s.length(); currentIndex++) { 209 | char currentChar = s.charAt(currentIndex); 210 | switch (currentChar) { 211 | case ' ': 212 | case ',': 213 | foundSeparator = true; 214 | break; 215 | case '-': 216 | if (currentIndex != start) { 217 | foundSeparator = true; 218 | result.mEndWithNegSign = true; 219 | } 220 | break; 221 | } 222 | if (foundSeparator) { 223 | break; 224 | } 225 | } 226 | // When there is nothing found, then we put the end position to the end 227 | // of the string. 228 | result.mEndPosition = currentIndex; 229 | } 230 | 231 | /** 232 | * Each PathDataNode represents one command in the "d" attribute of the svg 233 | * file. 234 | * An array of PathDataNode can represent the whole "d" attribute. 235 | */ 236 | public static class PathDataNode { 237 | private char mType; 238 | private float[] mParams; 239 | 240 | private PathDataNode(char type, float[] params) { 241 | mType = type; 242 | mParams = params; 243 | } 244 | 245 | private PathDataNode(PathDataNode n) { 246 | mType = n.mType; 247 | mParams = Arrays.copyOf(n.mParams, n.mParams.length); 248 | } 249 | 250 | /** 251 | * Convert an array of PathDataNode to Path. 252 | * 253 | * @param node The source array of PathDataNode. 254 | * @param path The target Path object. 255 | */ 256 | public static void nodesToPath(PathDataNode[] node, Path path) { 257 | float[] current = new float[4]; 258 | char previousCommand = 'm'; 259 | for (int i = 0; i < node.length; i++) { 260 | addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); 261 | previousCommand = node[i].mType; 262 | } 263 | } 264 | 265 | /** 266 | * The current PathDataNode will be interpolated between the 267 | * nodeFrom and nodeTo according to the 268 | * fraction. 269 | * 270 | * @param nodeFrom The start value as a PathDataNode. 271 | * @param nodeTo The end value as a PathDataNode 272 | * @param fraction The fraction to interpolate. 273 | */ 274 | public void interpolatePathDataNode(PathDataNode nodeFrom, 275 | PathDataNode nodeTo, float fraction) { 276 | for (int i = 0; i < nodeFrom.mParams.length; i++) { 277 | mParams[i] = nodeFrom.mParams[i] * (1 - fraction) 278 | + nodeTo.mParams[i] * fraction; 279 | } 280 | } 281 | 282 | private static void addCommand(Path path, float[] current, 283 | char previousCmd, char cmd, float[] val) { 284 | 285 | int incr = 2; 286 | float currentX = current[0]; 287 | float currentY = current[1]; 288 | float ctrlPointX = current[2]; 289 | float ctrlPointY = current[3]; 290 | float reflectiveCtrlPointX; 291 | float reflectiveCtrlPointY; 292 | 293 | switch (cmd) { 294 | case 'z': 295 | case 'Z': 296 | path.close(); 297 | return; 298 | case 'm': 299 | case 'M': 300 | case 'l': 301 | case 'L': 302 | case 't': 303 | case 'T': 304 | incr = 2; 305 | break; 306 | case 'h': 307 | case 'H': 308 | case 'v': 309 | case 'V': 310 | incr = 1; 311 | break; 312 | case 'c': 313 | case 'C': 314 | incr = 6; 315 | break; 316 | case 's': 317 | case 'S': 318 | case 'q': 319 | case 'Q': 320 | incr = 4; 321 | break; 322 | case 'a': 323 | case 'A': 324 | incr = 7; 325 | break; 326 | } 327 | for (int k = 0; k < val.length; k += incr) { 328 | switch (cmd) { 329 | case 'm': // moveto - Start a new sub-path (relative) 330 | path.rMoveTo(val[k + 0], val[k + 1]); 331 | currentX += val[k + 0]; 332 | currentY += val[k + 1]; 333 | break; 334 | case 'M': // moveto - Start a new sub-path 335 | path.moveTo(val[k + 0], val[k + 1]); 336 | currentX = val[k + 0]; 337 | currentY = val[k + 1]; 338 | break; 339 | case 'l': // lineto - Draw a line from the current point (relative) 340 | path.rLineTo(val[k + 0], val[k + 1]); 341 | currentX += val[k + 0]; 342 | currentY += val[k + 1]; 343 | break; 344 | case 'L': // lineto - Draw a line from the current point 345 | path.lineTo(val[k + 0], val[k + 1]); 346 | currentX = val[k + 0]; 347 | currentY = val[k + 1]; 348 | break; 349 | case 'z': // closepath - Close the current subpath 350 | case 'Z': // closepath - Close the current subpath 351 | path.close(); 352 | break; 353 | case 'h': // horizontal lineto - Draws a horizontal line (relative) 354 | path.rLineTo(val[k + 0], 0); 355 | currentX += val[k + 0]; 356 | break; 357 | case 'H': // horizontal lineto - Draws a horizontal line 358 | path.lineTo(val[k + 0], currentY); 359 | currentX = val[k + 0]; 360 | break; 361 | case 'v': // vertical lineto - Draws a vertical line from the current point (r) 362 | path.rLineTo(0, val[k + 0]); 363 | currentY += val[k + 0]; 364 | break; 365 | case 'V': // vertical lineto - Draws a vertical line from the current point 366 | path.lineTo(currentX, val[k + 0]); 367 | currentY = val[k + 0]; 368 | break; 369 | case 'c': // curveto - Draws a cubic Bézier curve (relative) 370 | path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 371 | val[k + 4], val[k + 5]); 372 | 373 | ctrlPointX = currentX + val[k + 2]; 374 | ctrlPointY = currentY + val[k + 3]; 375 | currentX += val[k + 4]; 376 | currentY += val[k + 5]; 377 | 378 | break; 379 | case 'C': // curveto - Draws a cubic Bézier curve 380 | path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], 381 | val[k + 4], val[k + 5]); 382 | currentX = val[k + 4]; 383 | currentY = val[k + 5]; 384 | ctrlPointX = val[k + 2]; 385 | ctrlPointY = val[k + 3]; 386 | break; 387 | case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) 388 | reflectiveCtrlPointX = 0; 389 | reflectiveCtrlPointY = 0; 390 | if (previousCmd == 'c' || previousCmd == 's' 391 | || previousCmd == 'C' || previousCmd == 'S') { 392 | reflectiveCtrlPointX = currentX - ctrlPointX; 393 | reflectiveCtrlPointY = currentY - ctrlPointY; 394 | } 395 | path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 396 | val[k + 0], val[k + 1], 397 | val[k + 2], val[k + 3]); 398 | 399 | ctrlPointX = currentX + val[k + 0]; 400 | ctrlPointY = currentY + val[k + 1]; 401 | currentX += val[k + 2]; 402 | currentY += val[k + 3]; 403 | break; 404 | case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) 405 | reflectiveCtrlPointX = currentX; 406 | reflectiveCtrlPointY = currentY; 407 | if (previousCmd == 'c' || previousCmd == 's' 408 | || previousCmd == 'C' || previousCmd == 'S') { 409 | reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 410 | reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 411 | } 412 | path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 413 | val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 414 | ctrlPointX = val[k + 0]; 415 | ctrlPointY = val[k + 1]; 416 | currentX = val[k + 2]; 417 | currentY = val[k + 3]; 418 | break; 419 | case 'q': // Draws a quadratic Bézier (relative) 420 | path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 421 | ctrlPointX = currentX + val[k + 0]; 422 | ctrlPointY = currentY + val[k + 1]; 423 | currentX += val[k + 2]; 424 | currentY += val[k + 3]; 425 | break; 426 | case 'Q': // Draws a quadratic Bézier 427 | path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); 428 | ctrlPointX = val[k + 0]; 429 | ctrlPointY = val[k + 1]; 430 | currentX = val[k + 2]; 431 | currentY = val[k + 3]; 432 | break; 433 | case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) 434 | reflectiveCtrlPointX = 0; 435 | reflectiveCtrlPointY = 0; 436 | if (previousCmd == 'q' || previousCmd == 't' 437 | || previousCmd == 'Q' || previousCmd == 'T') { 438 | reflectiveCtrlPointX = currentX - ctrlPointX; 439 | reflectiveCtrlPointY = currentY - ctrlPointY; 440 | } 441 | path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 442 | val[k + 0], val[k + 1]); 443 | ctrlPointX = currentX + reflectiveCtrlPointX; 444 | ctrlPointY = currentY + reflectiveCtrlPointY; 445 | currentX += val[k + 0]; 446 | currentY += val[k + 1]; 447 | break; 448 | case 'T': // Draws a quadratic Bézier curve (reflective control point) 449 | reflectiveCtrlPointX = currentX; 450 | reflectiveCtrlPointY = currentY; 451 | if (previousCmd == 'q' || previousCmd == 't' 452 | || previousCmd == 'Q' || previousCmd == 'T') { 453 | reflectiveCtrlPointX = 2 * currentX - ctrlPointX; 454 | reflectiveCtrlPointY = 2 * currentY - ctrlPointY; 455 | } 456 | path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, 457 | val[k + 0], val[k + 1]); 458 | ctrlPointX = reflectiveCtrlPointX; 459 | ctrlPointY = reflectiveCtrlPointY; 460 | currentX = val[k + 0]; 461 | currentY = val[k + 1]; 462 | break; 463 | case 'a': // Draws an elliptical arc 464 | // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) 465 | drawArc(path, 466 | currentX, 467 | currentY, 468 | val[k + 5] + currentX, 469 | val[k + 6] + currentY, 470 | val[k + 0], 471 | val[k + 1], 472 | val[k + 2], 473 | val[k + 3] != 0, 474 | val[k + 4] != 0); 475 | currentX += val[k + 5]; 476 | currentY += val[k + 6]; 477 | ctrlPointX = currentX; 478 | ctrlPointY = currentY; 479 | break; 480 | case 'A': // Draws an elliptical arc 481 | drawArc(path, 482 | currentX, 483 | currentY, 484 | val[k + 5], 485 | val[k + 6], 486 | val[k + 0], 487 | val[k + 1], 488 | val[k + 2], 489 | val[k + 3] != 0, 490 | val[k + 4] != 0); 491 | currentX = val[k + 5]; 492 | currentY = val[k + 6]; 493 | ctrlPointX = currentX; 494 | ctrlPointY = currentY; 495 | break; 496 | } 497 | previousCmd = cmd; 498 | } 499 | current[0] = currentX; 500 | current[1] = currentY; 501 | current[2] = ctrlPointX; 502 | current[3] = ctrlPointY; 503 | } 504 | 505 | private static void drawArc(Path p, 506 | float x0, 507 | float y0, 508 | float x1, 509 | float y1, 510 | float a, 511 | float b, 512 | float theta, 513 | boolean isMoreThanHalf, 514 | boolean isPositiveArc) { 515 | 516 | /* Convert rotation angle from degrees to radians */ 517 | double thetaD = Math.toRadians(theta); 518 | /* Pre-compute rotation matrix entries */ 519 | double cosTheta = Math.cos(thetaD); 520 | double sinTheta = Math.sin(thetaD); 521 | /* Transform (x0, y0) and (x1, y1) into unit space */ 522 | /* using (inverse) rotation, followed by (inverse) scale */ 523 | double x0p = (x0 * cosTheta + y0 * sinTheta) / a; 524 | double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; 525 | double x1p = (x1 * cosTheta + y1 * sinTheta) / a; 526 | double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; 527 | 528 | /* Compute differences and averages */ 529 | double dx = x0p - x1p; 530 | double dy = y0p - y1p; 531 | double xm = (x0p + x1p) / 2; 532 | double ym = (y0p + y1p) / 2; 533 | /* Solve for intersecting unit circles */ 534 | double dsq = dx * dx + dy * dy; 535 | if (dsq == 0.0) { 536 | Log.w(LOGTAG, " Points are coincident"); 537 | return; /* Points are coincident */ 538 | } 539 | double disc = 1.0 / dsq - 1.0 / 4.0; 540 | if (disc < 0.0) { 541 | Log.w(LOGTAG, "Points are too far apart " + dsq); 542 | float adjust = (float) (Math.sqrt(dsq) / 1.99999); 543 | drawArc(p, x0, y0, x1, y1, a * adjust, 544 | b * adjust, theta, isMoreThanHalf, isPositiveArc); 545 | return; /* Points are too far apart */ 546 | } 547 | double s = Math.sqrt(disc); 548 | double sdx = s * dx; 549 | double sdy = s * dy; 550 | double cx; 551 | double cy; 552 | if (isMoreThanHalf == isPositiveArc) { 553 | cx = xm - sdy; 554 | cy = ym + sdx; 555 | } else { 556 | cx = xm + sdy; 557 | cy = ym - sdx; 558 | } 559 | 560 | double eta0 = Math.atan2((y0p - cy), (x0p - cx)); 561 | 562 | double eta1 = Math.atan2((y1p - cy), (x1p - cx)); 563 | 564 | double sweep = (eta1 - eta0); 565 | if (isPositiveArc != (sweep >= 0)) { 566 | if (sweep > 0) { 567 | sweep -= 2 * Math.PI; 568 | } else { 569 | sweep += 2 * Math.PI; 570 | } 571 | } 572 | 573 | cx *= a; 574 | cy *= b; 575 | double tcx = cx; 576 | cx = cx * cosTheta - cy * sinTheta; 577 | cy = tcx * sinTheta + cy * cosTheta; 578 | 579 | arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); 580 | } 581 | 582 | /** 583 | * Converts an arc to cubic Bezier segments and records them in p. 584 | * 585 | * @param p The target for the cubic Bezier segments 586 | * @param cx The x coordinate center of the ellipse 587 | * @param cy The y coordinate center of the ellipse 588 | * @param a The radius of the ellipse in the horizontal direction 589 | * @param b The radius of the ellipse in the vertical direction 590 | * @param e1x E(eta1) x coordinate of the starting point of the arc 591 | * @param e1y E(eta2) y coordinate of the starting point of the arc 592 | * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane 593 | * @param start The start angle of the arc on the ellipse 594 | * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse 595 | */ 596 | private static void arcToBezier(Path p, 597 | double cx, 598 | double cy, 599 | double a, 600 | double b, 601 | double e1x, 602 | double e1y, 603 | double theta, 604 | double start, 605 | double sweep) { 606 | // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html 607 | // and http://www.spaceroots.org/documents/ellipse/node22.html 608 | 609 | // Maximum of 45 degrees per cubic Bezier segment 610 | int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); 611 | 612 | double eta1 = start; 613 | double cosTheta = Math.cos(theta); 614 | double sinTheta = Math.sin(theta); 615 | double cosEta1 = Math.cos(eta1); 616 | double sinEta1 = Math.sin(eta1); 617 | double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); 618 | double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); 619 | 620 | double anglePerSegment = sweep / numSegments; 621 | for (int i = 0; i < numSegments; i++) { 622 | double eta2 = eta1 + anglePerSegment; 623 | double sinEta2 = Math.sin(eta2); 624 | double cosEta2 = Math.cos(eta2); 625 | double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); 626 | double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); 627 | double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; 628 | double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; 629 | double tanDiff2 = Math.tan((eta2 - eta1) / 2); 630 | double alpha = 631 | Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; 632 | double q1x = e1x + alpha * ep1x; 633 | double q1y = e1y + alpha * ep1y; 634 | double q2x = e2x - alpha * ep2x; 635 | double q2y = e2y - alpha * ep2y; 636 | 637 | p.cubicTo((float) q1x, 638 | (float) q1y, 639 | (float) q2x, 640 | (float) q2y, 641 | (float) e2x, 642 | (float) e2y); 643 | eta1 = eta2; 644 | e1x = e2x; 645 | e1y = e2y; 646 | ep1x = ep2x; 647 | ep1y = ep2y; 648 | } 649 | } 650 | } 651 | } 652 | 653 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/ResourcesCompat.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Build; 8 | 9 | /** 10 | * Copyright (C) 2015 Wael Nafee 11 | *

12 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 13 | * in compliance with the License. You may obtain a copy of the License at 14 | *

15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | *

17 | * Unless required by applicable law or agreed to in writing, software distributed under the License 18 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 19 | * or implied. See the License for the specific language governing permissions and limitations under 20 | * the License. 21 | */ 22 | public class ResourcesCompat { 23 | 24 | public static final boolean LOLLIPOP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 25 | 26 | @SuppressWarnings("deprecation") 27 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 28 | public static Drawable getDrawable(Context c, int resId) { 29 | Drawable d; 30 | try { 31 | if (LOLLIPOP) { 32 | d = c.getResources().getDrawable(resId, c.getTheme()); 33 | } else { 34 | d = c.getResources().getDrawable(resId); 35 | } 36 | } catch (Resources.NotFoundException e) { 37 | 38 | try { 39 | d = VectorDrawable.getDrawable(c, resId); 40 | } catch (IllegalArgumentException e1) { 41 | 42 | //We're not a VectorDrawable, try AnimatedVectorDrawable 43 | try { 44 | d = AnimatedVectorDrawable.getDrawable(c, resId); 45 | } catch (IllegalArgumentException e2) { 46 | //Throw NotFoundException 47 | throw e; 48 | } 49 | } 50 | } 51 | return d; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/src/main/java/com/wnafee/vector/compat/Tintable.java: -------------------------------------------------------------------------------- 1 | package com.wnafee.vector.compat; 2 | 3 | import android.content.res.ColorStateList; 4 | import android.graphics.PorterDuff; 5 | 6 | /** 7 | * Copyright (C) 2015 Wael Nafee 8 | *

9 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 10 | * in compliance with the License. You may obtain a copy of the License at 11 | *

12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | *

14 | * Unless required by applicable law or agreed to in writing, software distributed under the License 15 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 16 | * or implied. See the License for the specific language governing permissions and limitations under 17 | * the License. 18 | */ 19 | public interface Tintable { 20 | public void setTintMode(PorterDuff.Mode tintMode); 21 | public void setTintList(ColorStateList tint); 22 | } 23 | -------------------------------------------------------------------------------- /library/src/main/res/anim/arrow_to_drawer_path.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/arrow_to_drawer_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/drawer_to_arrow_path.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/anim/drawer_to_arrow_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/pause_to_play_path.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/pause_to_play_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/play_to_pause_path.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/play_to_pause_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/anim/play_to_stop_path.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/play_to_stop_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/anim/stop_to_play_path.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/anim/stop_to_play_rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_arrow_to_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_arrow_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_drawer_to_arrow.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_drawer_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_pause_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_pause_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_play_to_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_play_to_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_play_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_stop_to_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/ic_stop_vector.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /library/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 54 | 55 | 57 | 58 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 102 | 104 | 105 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 151 | 152 | 153 | 154 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 233 | 234 | 235 | 236 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /library/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 200 4 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | M 3,6 L 3,8 L 21,8 L 21,6 L 3,6 z M 3,11 L 3,13 L 21,13 L 21, 12 L 21,11 L 3,11 z M 3,18 L 3,16 L 21,16 L 21,18 L 3,18 z 4 | M 12, 4 L 10.59,5.41 L 16.17,11 L 18.99,11 L 12,4 z M 4, 11 L 4, 13 L 18.99, 13 L 20, 12 L 18.99, 11 L 4, 11 z M 12,20 L 10.59, 18.59 L 16.17, 13 L 18.99, 13 L 12, 20z 5 | M 6,19 L 10,19 L 10,5 L 6,5 L 6,19 z M 14,19 L 18,19 L 18,5 L 14,5 L 14,19 z 6 | M 5, 18.06 L 12,18.06 L 12,5.94 L 12,5.94 L 5,18.06 z M 12,18.06 L 19,18.06 L 12,5.94 L 12,5.94 L 12,18.06 z 7 | M 6,18 L 12,18 L 12,6 L 6,6 L 6,18 z M 12,18 L 18,18 L 18,6 L 12,6 L 12,18z 8 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':library' 2 | --------------------------------------------------------------------------------